forked from mirror/sjson
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 (
|
import (
|
||||||
jsongo "encoding/json"
|
jsongo "encoding/json"
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
@ -41,7 +43,16 @@ type pathResult struct {
|
||||||
more bool // there is more path to parse
|
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
|
var r pathResult
|
||||||
if len(path) > 0 && path[0] == ':' {
|
if len(path) > 0 && path[0] == ':' {
|
||||||
r.force = true
|
r.force = true
|
||||||
|
@ -53,12 +64,10 @@ func parsePath(path string) (pathResult, error) {
|
||||||
r.gpart = path[:i]
|
r.gpart = path[:i]
|
||||||
r.path = path[i+1:]
|
r.path = path[i+1:]
|
||||||
r.more = true
|
r.more = true
|
||||||
return r, nil
|
return r, true
|
||||||
}
|
}
|
||||||
if path[i] == '*' || path[i] == '?' {
|
if !isSimpleChar(path[i]) {
|
||||||
return r, &errorType{"wildcard characters not allowed in path"}
|
return r, false
|
||||||
} else if path[i] == '#' {
|
|
||||||
return r, &errorType{"array access character not allowed in path"}
|
|
||||||
}
|
}
|
||||||
if path[i] == '\\' {
|
if path[i] == '\\' {
|
||||||
// go into escape mode. this is a slower path that
|
// 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.gpart = string(gpart)
|
||||||
r.path = path[i+1:]
|
r.path = path[i+1:]
|
||||||
r.more = true
|
r.more = true
|
||||||
return r, nil
|
return r, true
|
||||||
} else if path[i] == '*' || path[i] == '?' {
|
} else if !isSimpleChar(path[i]) {
|
||||||
return r, &errorType{
|
return r, false
|
||||||
"wildcard characters not allowed in path"}
|
|
||||||
} else if path[i] == '#' {
|
|
||||||
return r, &errorType{
|
|
||||||
"array access character not allowed in path"}
|
|
||||||
}
|
}
|
||||||
epart = append(epart, path[i])
|
epart = append(epart, path[i])
|
||||||
gpart = append(gpart, path[i])
|
gpart = append(gpart, path[i])
|
||||||
|
@ -99,12 +104,12 @@ func parsePath(path string) (pathResult, error) {
|
||||||
// append the last part
|
// append the last part
|
||||||
r.part = string(epart)
|
r.part = string(epart)
|
||||||
r.gpart = string(gpart)
|
r.gpart = string(gpart)
|
||||||
return r, nil
|
return r, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.part = path
|
r.part = path
|
||||||
r.gpart = path
|
r.gpart = path
|
||||||
return r, nil
|
return r, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustMarshalString(s string) bool {
|
func mustMarshalString(s string) bool {
|
||||||
|
@ -502,7 +507,7 @@ type sliceHeader struct {
|
||||||
func set(jstr, path, raw string,
|
func set(jstr, path, raw string,
|
||||||
stringify, del, optimistic, inplace bool) ([]byte, error) {
|
stringify, del, optimistic, inplace bool) ([]byte, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, &errorType{"path cannot be empty"}
|
return []byte(jstr), &errorType{"path cannot be empty"}
|
||||||
}
|
}
|
||||||
if !del && optimistic && isOptimisticPath(path) {
|
if !del && optimistic && isOptimisticPath(path) {
|
||||||
res := gjson.Get(jstr, path)
|
res := gjson.Get(jstr, path)
|
||||||
|
@ -530,7 +535,7 @@ func set(jstr, path, raw string,
|
||||||
}
|
}
|
||||||
return jbytes[:sz], nil
|
return jbytes[:sz], nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return []byte(jstr), nil
|
||||||
}
|
}
|
||||||
buf := make([]byte, 0, sz)
|
buf := make([]byte, 0, sz)
|
||||||
buf = append(buf, jstr[:res.Index]...)
|
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
|
// parse the path, make sure that it does not contain invalid characters
|
||||||
// such as '#', '?', '*'
|
// such as '#', '?', '*'
|
||||||
paths := make([]pathResult, 0, 4)
|
var paths []pathResult
|
||||||
r, err := parsePath(path)
|
r, simple := parsePath(path)
|
||||||
if err != nil {
|
if simple {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
paths = append(paths, r)
|
paths = append(paths, r)
|
||||||
for r.more {
|
for r.more {
|
||||||
if r, err = parsePath(r.path); err != nil {
|
r, simple = parsePath(r.path)
|
||||||
return nil, err
|
if !simple {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
paths = append(paths, r)
|
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)
|
njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return []byte(jstr), err
|
||||||
}
|
}
|
||||||
return njson, nil
|
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.
|
// SetOptions sets a json value for the specified path with options.
|
||||||
// A path is in dot syntax, such as "name.last" or "age".
|
// 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.
|
// This function expects that the json is well-formed, and does not validate.
|
||||||
|
|
|
@ -7,43 +7,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/pretty"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"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 (
|
const (
|
||||||
setRaw = 1
|
setRaw = 1
|
||||||
setBool = 2
|
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