commit 23241c5ce0d8c39586a55b6957fd64e07fb05f8b Author: upstream Date: Thu Sep 3 14:27:28 2026 +0200 Upstream tidwall/sjson state of the touched files before 29fee40be0 diff --git a/sjson.go b/sjson.go new file mode 100644 index 0000000..9cf3f93 --- /dev/null +++ b/sjson.go @@ -0,0 +1,747 @@ +// Package sjson provides setting json values. +package sjson + +import ( + jsongo "encoding/json" + "sort" + "strconv" + "unsafe" + + "github.com/tidwall/gjson" +) + +type errorType struct { + msg string +} + +func (err *errorType) Error() string { + return err.msg +} + +// Options represents additional options for the Set and Delete functions. +type Options struct { + // Optimistic is a hint that the value likely exists which + // allows for the sjson to perform a fast-track search and replace. + Optimistic bool + // ReplaceInPlace is a hint to replace the input json rather than + // allocate a new json byte slice. When this field is specified + // the input json will not longer be valid and it should not be used + // In the case when the destination slice doesn't have enough free + // bytes to replace the data in place, a new bytes slice will be + // created under the hood. + // The Optimistic flag must be set to true and the input must be a + // byte slice in order to use this field. + ReplaceInPlace bool +} + +type pathResult struct { + part string // current key part + gpart string // gjson get part + path string // remaining path + force bool // force a string key + more bool // there is more path to parse +} + +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 + path = path[1:] + } + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.gpart = path[:i] + r.path = path[i+1:] + r.more = true + return r, true + } + if !isSimpleChar(path[i]) { + return r, false + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + gpart := []byte(path[:i+1]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + gpart = append(gpart, '\\') + i++ + if i < len(path) { + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + r.gpart = string(gpart) + r.path = path[i+1:] + r.more = true + return r, true + } else if !isSimpleChar(path[i]) { + return r, false + } + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + } + } + // append the last part + r.part = string(epart) + r.gpart = string(gpart) + return r, true + } + } + r.part = path + r.gpart = path + return r, true +} + +func mustMarshalString(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' { + return true + } + } + return false +} + +// appendStringify makes a json string and appends to buf. +func appendStringify(buf []byte, s string) []byte { + if mustMarshalString(s) { + b, _ := jsongo.Marshal(s) + return append(buf, b...) + } + buf = append(buf, '"') + buf = append(buf, s...) + buf = append(buf, '"') + return buf +} + +// appendBuild builds a json block from a json path. +func appendBuild(buf []byte, array bool, paths []pathResult, raw string, + stringify bool) []byte { + if !array { + buf = appendStringify(buf, paths[0].part) + buf = append(buf, ':') + } + if len(paths) > 1 { + n, numeric := atoui(paths[1]) + if numeric || (!paths[1].force && paths[1].part == "-1") { + buf = append(buf, '[') + buf = appendRepeat(buf, "null,", n) + buf = appendBuild(buf, true, paths[1:], raw, stringify) + buf = append(buf, ']') + } else { + buf = append(buf, '{') + buf = appendBuild(buf, false, paths[1:], raw, stringify) + buf = append(buf, '}') + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + return buf +} + +// atoui does a rip conversion of string -> unigned int. +func atoui(r pathResult) (n int, ok bool) { + if r.force { + return 0, false + } + for i := 0; i < len(r.part); i++ { + if r.part[i] < '0' || r.part[i] > '9' { + return 0, false + } + n = n*10 + int(r.part[i]-'0') + } + return n, true +} + +// appendRepeat repeats string "n" times and appends to buf. +func appendRepeat(buf []byte, s string, n int) []byte { + for i := 0; i < n; i++ { + buf = append(buf, s...) + } + return buf +} + +// trim does a rip trim +func trim(s string) string { + for len(s) > 0 { + if s[0] <= ' ' { + s = s[1:] + continue + } + break + } + for len(s) > 0 { + if s[len(s)-1] <= ' ' { + s = s[:len(s)-1] + continue + } + break + } + return s +} + +// deleteTailItem deletes the previous key or comma. +func deleteTailItem(buf []byte) ([]byte, bool) { +loop: + for i := len(buf) - 1; i >= 0; i-- { + // look for either a ',',':','[' + switch buf[i] { + case '[': + return buf, true + case ',': + return buf[:i], false + case ':': + // delete tail string + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + if i >= 0 && buf[i] == '\\' { + i-- + continue + } + for ; i >= 0; i-- { + // look for either a ',','{' + switch buf[i] { + case '{': + return buf[:i+1], true + case ',': + return buf[:i], false + } + } + } + } + break + } + } + break loop + } + } + return buf, false +} + +var errNoChange = &errorType{"no change"} + +func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, + stringify, del bool) ([]byte, error) { + var err error + var res gjson.Result + var found bool + if del { + if paths[0].part == "-1" && !paths[0].force { + res = gjson.Get(jstr, "#") + if res.Int() > 0 { + res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) + found = true + } + } + } + if !found { + res = gjson.Get(jstr, paths[0].gpart) + } + if res.Index > 0 { + if len(paths) > 1 { + buf = append(buf, jstr[:res.Index]...) + buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, + stringify, del) + if err != nil { + return nil, err + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + buf = append(buf, jstr[:res.Index]...) + var exidx int // additional forward stripping + if del { + var delNextComma bool + buf, delNextComma = deleteTailItem(buf) + if delNextComma { + i, j := res.Index+len(res.Raw), 0 + for ; i < len(jstr); i, j = i+1, j+1 { + if jstr[i] <= ' ' { + continue + } + if jstr[i] == ',' { + exidx = j + 1 + } + break + } + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) + return buf, nil + } + if del { + return nil, errNoChange + } + n, numeric := atoui(paths[0]) + isempty := true + for i := 0; i < len(jstr); i++ { + if jstr[i] > ' ' { + isempty = false + break + } + } + if isempty { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + } + jsres := gjson.Parse(jstr) + if jsres.Type != gjson.JSON { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + jsres = gjson.Parse(jstr) + } + var comma bool + for i := 1; i < len(jsres.Raw); i++ { + if jsres.Raw[i] <= ' ' { + continue + } + if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { + break + } + comma = true + break + } + switch jsres.Raw[0] { + default: + return nil, &errorType{"json must be an object or array"} + case '{': + end := len(jsres.Raw) - 1 + for ; end > 0; end-- { + if jsres.Raw[end] == '}' { + break + } + } + buf = append(buf, jsres.Raw[:end]...) + if comma { + buf = append(buf, ',') + } + buf = appendBuild(buf, false, paths, raw, stringify) + buf = append(buf, '}') + return buf, nil + case '[': + var appendit bool + if !numeric { + if paths[0].part == "-1" && !paths[0].force { + appendit = true + } else { + return nil, &errorType{ + "cannot set array element for non-numeric key '" + + paths[0].part + "'"} + } + } + if appendit { + njson := trim(jsres.Raw) + if njson[len(njson)-1] == ']' { + njson = njson[:len(njson)-1] + } + buf = append(buf, njson...) + if comma { + buf = append(buf, ',') + } + + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } + buf = append(buf, '[') + ress := jsres.Array() + for i := 0; i < len(ress); i++ { + if i > 0 { + buf = append(buf, ',') + } + buf = append(buf, ress[i].Raw...) + } + if len(ress) == 0 { + buf = appendRepeat(buf, "null,", n-len(ress)) + } else { + buf = appendRepeat(buf, ",null", n-len(ress)) + if comma { + buf = append(buf, ',') + } + } + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } +} + +func isOptimisticPath(path string) bool { + for i := 0; i < len(path); i++ { + if path[i] < '.' || path[i] > 'z' { + return false + } + if path[i] > '9' && path[i] < 'A' { + return false + } + if path[i] > 'z' { + return false + } + } + return true +} + +// Set sets a json value for the specified path. +// 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. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +// +// A path is a series of keys separated by a dot. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children.1" >> "Alex" +// +func Set(json, path string, value interface{}) (string, error) { + return SetOptions(json, path, value, nil) +} + +// SetBytes sets a json value for the specified path. +// If working with bytes, this method preferred over +// Set(string(data), path, value) +func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { + return SetBytesOptions(json, path, value, nil) +} + +// SetRaw sets a raw json value for the specified path. +// This function works the same as Set except that the value is set as a +// raw block of json. This allows for setting premarshalled json objects. +func SetRaw(json, path, value string) (string, error) { + return SetRawOptions(json, path, value, nil) +} + +// SetRawOptions sets a raw json value for the specified path with options. +// This furnction works the same as SetOptions except that the value is set +// as a raw block of json. This allows for setting premarshalled json objects. +func SetRawOptions(json, path, value string, opts *Options) (string, error) { + var optimistic bool + if opts != nil { + optimistic = opts.Optimistic + } + res, err := set(json, path, value, false, false, optimistic, false) + if err == errNoChange { + return json, nil + } + return string(res), err +} + +// SetRawBytes sets a raw json value for the specified path. +// If working with bytes, this method preferred over +// SetRaw(string(data), path, value) +func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { + return SetRawBytesOptions(json, path, value, nil) +} + +type dtype struct{} + +// Delete deletes a value from json for the specified path. +func Delete(json, path string) (string, error) { + return Set(json, path, dtype{}) +} + +// DeleteOptions deletes a value from json for the specified path, with the given options. +func DeleteOptions(json, path string, options *Options) (string, error) { + return SetOptions(json, path, dtype{}, options) +} + +// DeleteBytes deletes a value from json for the specified path. +func DeleteBytes(json []byte, path string) ([]byte, error) { + return SetBytes(json, path, dtype{}) +} + +// DeleteBytesOptions deletes a value from json for the specified path, with the given options. +func DeleteBytesOptions(json []byte, path string, options *Options) ([]byte, error) { + return SetBytesOptions(json, path, dtype{}, options) +} + +type stringHeader struct { + data unsafe.Pointer + len int +} + +type sliceHeader struct { + data unsafe.Pointer + len int + cap int +} + +func set(jstr, path, raw string, + stringify, del, optimistic, inplace bool) ([]byte, error) { + if path == "" { + return []byte(jstr), &errorType{"path cannot be empty"} + } + if !del && optimistic && isOptimisticPath(path) { + res := gjson.Get(jstr, path) + if res.Exists() && res.Index > 0 { + sz := len(jstr) - len(res.Raw) + len(raw) + if stringify { + sz += 2 + } + if inplace && sz <= len(jstr) { + if !stringify || !mustMarshalString(raw) { + jsonh := *(*stringHeader)(unsafe.Pointer(&jstr)) + jsonbh := sliceHeader{ + data: jsonh.data, len: jsonh.len, cap: jsonh.len} + jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) + if stringify { + jbytes[res.Index] = '"' + copy(jbytes[res.Index+1:], []byte(raw)) + jbytes[res.Index+1+len(raw)] = '"' + copy(jbytes[res.Index+1+len(raw)+1:], + jbytes[res.Index+len(res.Raw):]) + } else { + copy(jbytes[res.Index:], []byte(raw)) + copy(jbytes[res.Index+len(raw):], + jbytes[res.Index+len(res.Raw):]) + } + return jbytes[:sz], nil + } + return []byte(jstr), nil + } + buf := make([]byte, 0, sz) + buf = append(buf, jstr[:res.Index]...) + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + } + 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) + } + } + if !simple { + if del { + return []byte(jstr), + &errorType{"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 []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), errNoChange + } + 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), errNoChange + } + 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. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +func SetOptions(json, path string, value interface{}, + opts *Options) (string, error) { + if opts != nil { + if opts.ReplaceInPlace { + // it's not safe to replace bytes in-place for strings + // copy the Options and set options.ReplaceInPlace to false. + nopts := *opts + opts = &nopts + opts.ReplaceInPlace = false + } + } + jsonh := *(*stringHeader)(unsafe.Pointer(&json)) + jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len} + jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) + res, err := SetBytesOptions(jsonb, path, value, opts) + return string(res), err +} + +// SetBytesOptions sets a json value for the specified path with options. +// If working with bytes, this method preferred over +// SetOptions(string(data), path, value) +func SetBytesOptions(json []byte, path string, value interface{}, + opts *Options) ([]byte, error) { + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + jstr := *(*string)(unsafe.Pointer(&json)) + var res []byte + var err error + switch v := value.(type) { + default: + b, merr := jsongo.Marshal(value) + if merr != nil { + return nil, merr + } + raw := *(*string)(unsafe.Pointer(&b)) + res, err = set(jstr, path, raw, false, false, optimistic, inplace) + case dtype: + res, err = set(jstr, path, "", false, true, optimistic, inplace) + case string: + res, err = set(jstr, path, v, true, false, optimistic, inplace) + case []byte: + raw := *(*string)(unsafe.Pointer(&v)) + res, err = set(jstr, path, raw, true, false, optimistic, inplace) + case bool: + if v { + res, err = set(jstr, path, "true", false, false, optimistic, inplace) + } else { + res, err = set(jstr, path, "false", false, false, optimistic, inplace) + } + case int8: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int16: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int32: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int64: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case uint8: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint16: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint32: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint64: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case float32: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + case float64: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + } + if err == errNoChange { + return json, nil + } + return res, err +} + +// SetRawBytesOptions sets a raw json value for the specified path with options. +// If working with bytes, this method preferred over +// SetRawOptions(string(data), path, value, opts) +func SetRawBytesOptions(json []byte, path string, value []byte, + opts *Options) ([]byte, error) { + jstr := *(*string)(unsafe.Pointer(&json)) + vstr := *(*string)(unsafe.Pointer(&value)) + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + res, err := set(jstr, path, vstr, false, false, optimistic, inplace) + if err == errNoChange { + return json, nil + } + return res, err +} diff --git a/sjson_test.go b/sjson_test.go new file mode 100644 index 0000000..aa3d968 --- /dev/null +++ b/sjson_test.go @@ -0,0 +1,353 @@ +package sjson + +import ( + "encoding/hex" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/tidwall/gjson" + "github.com/tidwall/pretty" +) + +const ( + setRaw = 1 + setBool = 2 + setInt = 3 + setFloat = 4 + setString = 5 + setDelete = 6 +) + +func sortJSON(json string) string { + opts := pretty.Options{SortKeys: true} + return string(pretty.Ugly(pretty.PrettyOptions([]byte(json), &opts))) +} + +func testRaw(t *testing.T, kind int, expect, json, path string, value interface{}) { + t.Helper() + expect = sortJSON(expect) + var json2 string + var err error + switch kind { + default: + json2, err = Set(json, path, value) + case setRaw: + json2, err = SetRaw(json, path, value.(string)) + case setDelete: + json2, err = Delete(json, path) + } + + if err != nil { + t.Fatal(err) + } + json2 = sortJSON(json2) + if json2 != expect { + t.Fatalf("expected '%v', got '%v'", expect, json2) + } + var json3 []byte + switch kind { + default: + json3, err = SetBytes([]byte(json), path, value) + case setRaw: + json3, err = SetRawBytes([]byte(json), path, []byte(value.(string))) + case setDelete: + json3, err = DeleteBytes([]byte(json), path) + } + json3 = []byte(sortJSON(string(json3))) + if err != nil { + t.Fatal(err) + } else if string(json3) != expect { + t.Fatalf("expected '%v', got '%v'", expect, string(json3)) + } +} +func TestBasic(t *testing.T) { + testRaw(t, setRaw, `[{"hiw":"planet","hi":"world"}]`, `[{"hi":"world"}]`, "0.hiw", `"planet"`) + testRaw(t, setRaw, `[true]`, ``, "0", `true`) + testRaw(t, setRaw, `[null,true]`, ``, "1", `true`) + testRaw(t, setRaw, `[1,null,true]`, `[1]`, "2", `true`) + testRaw(t, setRaw, `[1,true,false]`, `[1,null,false]`, "1", `true`) + testRaw(t, setRaw, + `[1,{"hello":"when","this":[0,null,2]},false]`, + `[1,{"hello":"when","this":[0,1,2]},false]`, + "1.this.1", `null`) + testRaw(t, setRaw, + `{"a":1,"b":{"hello":"when","this":[0,null,2]},"c":false}`, + `{"a":1,"b":{"hello":"when","this":[0,1,2]},"c":false}`, + "b.this.1", `null`) + testRaw(t, setRaw, + `{"a":1,"b":{"hello":"when","this":[0,null,2,null,4]},"c":false}`, + `{"a":1,"b":{"hello":"when","this":[0,null,2]},"c":false}`, + "b.this.4", `4`) + testRaw(t, setRaw, + `{"b":{"this":[null,null,null,null,4]}}`, + ``, + "b.this.4", `4`) + testRaw(t, setRaw, + `[null,{"this":[null,null,null,null,4]}]`, + ``, + "1.this.4", `4`) + testRaw(t, setRaw, + `{"1":{"this":[null,null,null,null,4]}}`, + ``, + ":1.this.4", `4`) + testRaw(t, setRaw, + `{":1":{"this":[null,null,null,null,4]}}`, + ``, + "\\:1.this.4", `4`) + testRaw(t, setRaw, + `{":\\1":{"this":[null,null,null,null,{".HI":4}]}}`, + ``, + "\\:\\\\1.this.4.\\.HI", `4`) + testRaw(t, setRaw, + `{"app.token":"cde"}`, + `{"app.token":"abc"}`, + "app\\.token", `"cde"`) + testRaw(t, setRaw, + `{"b":{"this":{"😇":""}}}`, + ``, + "b.this.😇", `""`) + testRaw(t, setRaw, + `[ 1,2 ,3]`, + ` [ 1,2 ] `, + "-1", `3`) + testRaw(t, setInt, `[1234]`, ``, `0`, int64(1234)) + testRaw(t, setFloat, `[1234.5]`, ``, `0`, float64(1234.5)) + testRaw(t, setString, `["1234.5"]`, ``, `0`, "1234.5") + testRaw(t, setBool, `[true]`, ``, `0`, true) + testRaw(t, setBool, `[null]`, ``, `0`, nil) + testRaw(t, setString, `{"arr":[1]}`, ``, `arr.-1`, 1) + testRaw(t, setString, `{"a":"\\"}`, ``, `a`, "\\") + testRaw(t, setString, `{"a":"C:\\Windows\\System32"}`, ``, `a`, `C:\Windows\System32`) +} + +func TestDelete(t *testing.T) { + testRaw(t, setDelete, `[456]`, `[123,456]`, `0`, nil) + testRaw(t, setDelete, `[123,789]`, `[123,456,789]`, `1`, nil) + testRaw(t, setDelete, `[123,456]`, `[123,456,789]`, `-1`, nil) + testRaw(t, setDelete, `{"a":[123,456]}`, `{"a":[123,456,789]}`, `a.-1`, nil) + testRaw(t, setDelete, `{"and":"another"}`, `{"this":"that","and":"another"}`, `this`, nil) + testRaw(t, setDelete, `{"this":"that"}`, `{"this":"that","and":"another"}`, `and`, nil) + testRaw(t, setDelete, `{}`, `{"and":"another"}`, `and`, nil) + testRaw(t, setDelete, `{"1":"2"}`, `{"1":"2"}`, `3`, nil) +} + +// TestRandomData is a fuzzing test that throws random data at SetRaw +// function looking for panics. +func TestRandomData(t *testing.T) { + var lstr string + defer func() { + if v := recover(); v != nil { + println("'" + hex.EncodeToString([]byte(lstr)) + "'") + println("'" + lstr + "'") + panic(v) + } + }() + rand.Seed(time.Now().UnixNano()) + b := make([]byte, 200) + for i := 0; i < 2000000; i++ { + n, err := rand.Read(b[:rand.Int()%len(b)]) + if err != nil { + t.Fatal(err) + } + lstr = string(b[:n]) + SetRaw(lstr, "zzzz.zzzz.zzzz", "123") + } +} + +func TestDeleteIssue21(t *testing.T) { + json := `{"country_code_from":"NZ","country_code_to":"SA","date_created":"2018-09-13T02:56:11.25783Z","date_updated":"2018-09-14T03:15:16.67356Z","disabled":false,"last_edited_by":"Developers","id":"a3e...bc454","merchant_id":"f2b...b91abf","signed_date":"2018-02-01T00:00:00Z","start_date":"2018-03-01T00:00:00Z","url":"https://www.google.com"}` + res1 := gjson.Get(json, "date_updated") + var err error + json, err = Delete(json, "date_updated") + if err != nil { + t.Fatal(err) + } + res2 := gjson.Get(json, "date_updated") + res3 := gjson.Get(json, "date_created") + if !res1.Exists() || res2.Exists() || !res3.Exists() { + t.Fatal("bad news") + } + + // We change the number of characters in this to make the section of the string before the section that we want to delete a certain length + + //--------------------------- + lenBeforeToDeleteIs307AsBytes := `{"1":"","0":"012345678901234567890123456789012345678901234567890123456789012345678901234567","to_delete":"0","2":""}` + + expectedForLenBefore307AsBytes := `{"1":"","0":"012345678901234567890123456789012345678901234567890123456789012345678901234567","2":""}` + //--------------------------- + + //--------------------------- + lenBeforeToDeleteIs308AsBytes := `{"1":"","0":"0123456789012345678901234567890123456789012345678901234567890123456789012345678","to_delete":"0","2":""}` + + expectedForLenBefore308AsBytes := `{"1":"","0":"0123456789012345678901234567890123456789012345678901234567890123456789012345678","2":""}` + //--------------------------- + + //--------------------------- + lenBeforeToDeleteIs309AsBytes := `{"1":"","0":"01234567890123456789012345678901234567890123456789012345678901234567890123456","to_delete":"0","2":""}` + + expectedForLenBefore309AsBytes := `{"1":"","0":"01234567890123456789012345678901234567890123456789012345678901234567890123456","2":""}` + //--------------------------- + + var data = []struct { + desc string + input string + expected string + }{ + { + desc: "len before \"to_delete\"... = 307", + input: lenBeforeToDeleteIs307AsBytes, + expected: expectedForLenBefore307AsBytes, + }, + { + desc: "len before \"to_delete\"... = 308", + input: lenBeforeToDeleteIs308AsBytes, + expected: expectedForLenBefore308AsBytes, + }, + { + desc: "len before \"to_delete\"... = 309", + input: lenBeforeToDeleteIs309AsBytes, + expected: expectedForLenBefore309AsBytes, + }, + } + + for i, d := range data { + result, err := Delete(d.input, "to_delete") + + if err != nil { + t.Error(fmtErrorf(testError{ + unexpected: "error", + desc: d.desc, + i: i, + lenInput: len(d.input), + input: d.input, + expected: d.expected, + result: result, + })) + } + if result != d.expected { + t.Error(fmtErrorf(testError{ + unexpected: "result", + desc: d.desc, + i: i, + lenInput: len(d.input), + input: d.input, + expected: d.expected, + result: result, + })) + } + } +} + +type testError struct { + unexpected string + desc string + i int + lenInput int + input interface{} + expected interface{} + result interface{} +} + +func fmtErrorf(e testError) string { + return fmt.Sprintf( + "Unexpected %s:\n\t"+ + "for=%q\n\t"+ + "i=%d\n\t"+ + "len(input)=%d\n\t"+ + "input=%v\n\t"+ + "expected=%v\n\t"+ + "result=%v", + e.unexpected, e.desc, e.i, e.lenInput, e.input, e.expected, e.result, + ) +} + +func TestSetDotKeyIssue10(t *testing.T) { + json := `{"app.token":"abc"}` + json, _ = Set(json, `app\.token`, "cde") + if json != `{"app.token":"cde"}` { + t.Fatalf("expected '%v', got '%v'", `{"app.token":"cde"}`, json) + } +} +func TestDeleteDotKeyIssue19(t *testing.T) { + json := []byte(`{"data":{"key1":"value1","key2.something":"value2"}}`) + json, _ = DeleteBytes(json, `data.key2\.something`) + if string(json) != `{"data":{"key1":"value1"}}` { + t.Fatalf("expected '%v', got '%v'", `{"data":{"key1":"value1"}}`, json) + } +} + +func TestIssue36(t *testing.T) { + var json = ` + { + "size": 1000 + } +` + var raw = ` + { + "sample": "hello" + } +` + _ = raw + if true { + json, _ = SetRaw(json, "aggs", raw) + } + if !gjson.Valid(json) { + t.Fatal("invalid json") + } + res := gjson.Get(json, "aggs.sample").String() + if res != "hello" { + t.Fatal("unexpected result") + } +} + +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") + } +} + +func TestIssue61(t *testing.T) { + json := `{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "@vocab": "http://schema.org/", + "sh": "http://www.w3.org/ns/shacl#" + } + }` + json1, _ := Set(json, "@context.@vocab", "newval") + if gjson.Get(json1, "@context.@vocab").String() != "newval" { + t.Fail() + } +}