Skip to content

Commit

Permalink
Allow for updating values using queries and wildcards
Browse files Browse the repository at this point in the history
This commit allows for updating values for more "complex" paths like:

	friends.#(last="Murphy")#.last

This is allowed because GJSON now tracks the origin positions of all
results (tidwall/gjson#222).

This new ability is limited to updating values only. Setting new
values that previously did not exist, or deleting values will
return an error.
  • Loading branch information
tidwall committed Sep 4, 2021
1 parent a2a89c2 commit 0bc94ab
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 62 deletions.
118 changes: 90 additions & 28 deletions sjson.go
Expand Up @@ -3,6 +3,8 @@ package sjson

import (
jsongo "encoding/json"
"errors"
"sort"
"strconv"
"unsafe"

Expand Down Expand Up @@ -41,7 +43,16 @@ type pathResult struct {
more bool // there is more path to parse
}

func parsePath(path string) (pathResult, error) {
func isSimpleChar(ch byte) bool {
switch ch {
case '|', '#', '@', '*', '?':
return false
default:
return true
}
}

func parsePath(path string) (res pathResult, simple bool) {
var r pathResult
if len(path) > 0 && path[0] == ':' {
r.force = true
Expand All @@ -53,12 +64,10 @@ func parsePath(path string) (pathResult, error) {
r.gpart = path[:i]
r.path = path[i+1:]
r.more = true
return r, nil
return r, true
}
if path[i] == '*' || path[i] == '?' {
return r, &errorType{"wildcard characters not allowed in path"}
} else if path[i] == '#' {
return r, &errorType{"array access character not allowed in path"}
if !isSimpleChar(path[i]) {
return r, false
}
if path[i] == '\\' {
// go into escape mode. this is a slower path that
Expand All @@ -84,13 +93,9 @@ func parsePath(path string) (pathResult, error) {
r.gpart = string(gpart)
r.path = path[i+1:]
r.more = true
return r, nil
} else if path[i] == '*' || path[i] == '?' {
return r, &errorType{
"wildcard characters not allowed in path"}
} else if path[i] == '#' {
return r, &errorType{
"array access character not allowed in path"}
return r, true
} else if !isSimpleChar(path[i]) {
return r, false
}
epart = append(epart, path[i])
gpart = append(gpart, path[i])
Expand All @@ -99,12 +104,12 @@ func parsePath(path string) (pathResult, error) {
// append the last part
r.part = string(epart)
r.gpart = string(gpart)
return r, nil
return r, true
}
}
r.part = path
r.gpart = path
return r, nil
return r, true
}

func mustMarshalString(s string) bool {
Expand Down Expand Up @@ -502,7 +507,7 @@ type sliceHeader struct {
func set(jstr, path, raw string,
stringify, del, optimistic, inplace bool) ([]byte, error) {
if path == "" {
return nil, &errorType{"path cannot be empty"}
return []byte(jstr), &errorType{"path cannot be empty"}
}
if !del && optimistic && isOptimisticPath(path) {
res := gjson.Get(jstr, path)
Expand Down Expand Up @@ -530,7 +535,7 @@ func set(jstr, path, raw string,
}
return jbytes[:sz], nil
}
return nil, nil
return []byte(jstr), nil
}
buf := make([]byte, 0, sz)
buf = append(buf, jstr[:res.Index]...)
Expand All @@ -545,26 +550,83 @@ func set(jstr, path, raw string,
}
// parse the path, make sure that it does not contain invalid characters
// such as '#', '?', '*'
paths := make([]pathResult, 0, 4)
r, err := parsePath(path)
if err != nil {
return nil, err
var paths []pathResult
r, simple := parsePath(path)
if simple {
paths = append(paths, r)
for r.more {
r, simple = parsePath(r.path)
if !simple {
break
}
paths = append(paths, r)
}
}
paths = append(paths, r)
for r.more {
if r, err = parsePath(r.path); err != nil {
return nil, err
if !simple {
if del {
return []byte(jstr),
errors.New("cannot delete value from a complex path")
}
paths = append(paths, r)
return setComplexPath(jstr, path, raw, stringify)
}

njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del)
if err != nil {
return nil, err
return []byte(jstr), err
}
return njson, nil
}

func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) {
res := gjson.Get(jstr, path)
if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) {
return []byte(jstr), errors.New("no values found at path")
}
if res.Index != 0 {
njson := []byte(jstr[:res.Index])
if stringify {
njson = appendStringify(njson, raw)
} else {
njson = append(njson, raw...)
}
njson = append(njson, jstr[res.Index+len(res.Raw):]...)
jstr = string(njson)
}
if len(res.Indexes) > 0 {
type val struct {
index int
res gjson.Result
}
vals := make([]val, 0, len(res.Indexes))
res.ForEach(func(_, vres gjson.Result) bool {
vals = append(vals, val{res: vres})
return true
})
if len(res.Indexes) != len(vals) {
return []byte(jstr),
errors.New("could not set value due to index mismatch")
}
for i := 0; i < len(res.Indexes); i++ {
vals[i].index = res.Indexes[i]
}
sort.SliceStable(vals, func(i, j int) bool {
return vals[i].index > vals[j].index
})
for _, val := range vals {
vres := val.res
index := val.index
njson := []byte(jstr[:index])
if stringify {
njson = appendStringify(njson, raw)
} else {
njson = append(njson, raw...)
}
njson = append(njson, jstr[index+len(vres.Raw):]...)
jstr = string(njson)
}
}
return []byte(jstr), nil
}

// SetOptions sets a json value for the specified path with options.
// A path is in dot syntax, such as "name.last" or "age".
// This function expects that the json is well-formed, and does not validate.
Expand Down
68 changes: 34 additions & 34 deletions sjson_test.go
Expand Up @@ -7,43 +7,10 @@ import (
"testing"
"time"

"github.com/tidwall/pretty"

"github.com/tidwall/gjson"
"github.com/tidwall/pretty"
)

func TestInvalidPaths(t *testing.T) {
var err error
_, err = SetRaw(`{"hello":"world"}`, "", `"planet"`)
if err == nil || err.Error() != "path cannot be empty" {
t.Fatalf("expecting '%v', got '%v'", "path cannot be empty", err)
}
_, err = SetRaw("", "name.last.#", "")
if err == nil || err.Error() != "array access character not allowed in path" {
t.Fatalf("expecting '%v', got '%v'", "array access character not allowed in path", err)
}
_, err = SetRaw("", "name.last.\\1#", "")
if err == nil || err.Error() != "array access character not allowed in path" {
t.Fatalf("expecting '%v', got '%v'", "array access character not allowed in path", err)
}
_, err = SetRaw("", "name.las?t", "")
if err == nil || err.Error() != "wildcard characters not allowed in path" {
t.Fatalf("expecting '%v', got '%v'", "wildcard characters not allowed in path", err)
}
_, err = SetRaw("", "name.la\\s?t", "")
if err == nil || err.Error() != "wildcard characters not allowed in path" {
t.Fatalf("expecting '%v', got '%v'", "wildcard characters not allowed in path", err)
}
_, err = SetRaw("", "name.las*t", "")
if err == nil || err.Error() != "wildcard characters not allowed in path" {
t.Fatalf("expecting '%v', got '%v'", "wildcard characters not allowed in path", err)
}
_, err = SetRaw("", "name.las\\a*t", "")
if err == nil || err.Error() != "wildcard characters not allowed in path" {
t.Fatalf("expecting '%v', got '%v'", "wildcard characters not allowed in path", err)
}
}

const (
setRaw = 1
setBool = 2
Expand Down Expand Up @@ -335,5 +302,38 @@ func TestIssue36(t *testing.T) {
}
}

var example = `
{
"name": {"first": "Tom", "last": "Anderson"},
"age":37,
"children": ["Sara","Alex","Jack"],
"fav.movie": "Deer Hunter",
"friends": [
{"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
]
}
`

func TestIndex(t *testing.T) {
path := `friends.#(last="Murphy").last`
json, err := Set(example, path, "Johnson")
if err != nil {
t.Fatal(err)
}
if gjson.Get(json, "friends.#.last").String() != `["Johnson","Craig","Murphy"]` {
t.Fatal("mismatch")
}
}

func TestIndexes(t *testing.T) {
path := `friends.#(last="Murphy")#.last`
json, err := Set(example, path, "Johnson")
if err != nil {
t.Fatal(err)
}
if gjson.Get(json, "friends.#.last").String() != `["Johnson","Craig","Johnson"]` {
t.Fatal("mismatch")
}
}

0 comments on commit 0bc94ab

Please sign in to comment.