forked from mirror/sjson
473 lines
11 KiB
Go
473 lines
11 KiB
Go
// Package sjson provides setting json values.
|
|
package sjson
|
|
|
|
import (
|
|
jsongo "encoding/json"
|
|
"strconv"
|
|
|
|
"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
|
|
path string // remaining path
|
|
force bool // force a string key
|
|
more bool // there is more path to parse
|
|
}
|
|
|
|
func parsePath(path string) (pathResult, error) {
|
|
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.path = path[i+1:]
|
|
r.more = true
|
|
return r, nil
|
|
}
|
|
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 path[i] == '\\' {
|
|
// go into escape mode. this is a slower path that
|
|
// strips off the escape character from the part.
|
|
epart := []byte(path[:i])
|
|
i++
|
|
if i < len(path) {
|
|
epart = append(epart, path[i])
|
|
i++
|
|
for ; i < len(path); i++ {
|
|
if path[i] == '\\' {
|
|
i++
|
|
if i < len(path) {
|
|
epart = append(epart, path[i])
|
|
}
|
|
continue
|
|
} else if path[i] == '.' {
|
|
r.part = string(epart)
|
|
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"}
|
|
}
|
|
epart = append(epart, path[i])
|
|
}
|
|
}
|
|
// append the last part
|
|
r.part = string(epart)
|
|
return r, nil
|
|
}
|
|
}
|
|
r.part = path
|
|
return r, nil
|
|
}
|
|
|
|
func mustMarshalString(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] < ' ' || s[i] > 0x7f || 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 && 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].part)
|
|
}
|
|
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 '{':
|
|
buf = append(buf, '{')
|
|
buf = appendBuild(buf, false, paths, raw, stringify)
|
|
if comma {
|
|
buf = append(buf, ',')
|
|
}
|
|
buf = append(buf, jsres.Raw[1:]...)
|
|
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{})
|
|
}
|
|
|
|
// DeleteBytes deletes a value from json for the specified path.
|
|
func DeleteBytes(json []byte, path string) ([]byte, error) {
|
|
return SetBytes(json, path, dtype{})
|
|
}
|