mirror of https://github.com/tidwall/sjson.git
Allow for updating values using queries and wildcards
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 (https://github.com/tidwall/gjson/pull/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.
This commit is contained in:
parent
a2a89c2f1e
commit
0bc94ab89f
114
sjson.go
114
sjson.go
|
@ -3,6 +3,8 @@ package sjson
|
|||
|
||||
import (
|
||||
jsongo "encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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]...)
|
||||
|
@ -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 {
|
||||
if r, err = parsePath(r.path); err != nil {
|
||||
return nil, err
|
||||
r, simple = parsePath(r.path)
|
||||
if !simple {
|
||||
break
|
||||
}
|
||||
paths = append(paths, r)
|
||||
}
|
||||
|
||||
}
|
||||
if !simple {
|
||||
if del {
|
||||
return []byte(jstr),
|
||||
errors.New("cannot delete value from a complex path")
|
||||
}
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
|
@ -335,5 +302,38 @@ func TestIssue36(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIndexes(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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue