123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- // Package sjson provides setting json values.
- package yu_json
- import (
- jsongo "encoding/json"
- yu_fast "gogs.qqck.cn/s/tools/fast"
- yu_strconv "gogs.qqck.cn/s/tools/strconv"
- "sort"
- "strconv"
- )
- 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
- }
- // 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 Result
- var found bool
- if del {
- if paths[0].part == "-1" && !paths[0].force {
- res = Get(jstr, "#")
- if res.Int() > 0 {
- res = Get(jstr, yu_strconv.FormatInt64(res.Int()-1))
- found = true
- }
- }
- }
- if !found {
- res = 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 := Parse(jstr)
- if jsres.Type != JSON {
- if numeric {
- jstr = "[]"
- } else {
- jstr = "{}"
- }
- jsres = 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{}) (v string) {
- v, _ = SetOptions(json, path, value, nil)
- return
- }
- // SetErr 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 SetErr(json, path string, value interface{}) (string, error) {
- return SetOptions(json, path, value, nil)
- }
- // SetBytes sets a json value for the specified path.
- func SetBytes(json []byte, path string, value interface{}) (v []byte) {
- v, _ = SetBytesOptions(json, path, value, nil)
- return
- }
- // SetBytesErr sets a json value for the specified path.
- func SetBytesErr(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) (v string) {
- v, _ = SetRawOptions(json, path, value, nil)
- return
- }
- // SetRawErr 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 SetRawErr(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) (v []byte) {
- v, _ = SetRawBytesOptions(json, path, value, nil)
- return
- }
- // SetRawBytesErr sets a raw json value for the specified path.
- // If working with bytes, this method preferred over
- // SetRawErr(string(data), path, value)
- func SetRawBytesErr(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 SetErr(json, path, dtype{})
- }
- // DeleteBytes deletes a value from json for the specified path.
- func DeleteBytes(json []byte, path string) ([]byte, error) {
- return SetBytesErr(json, path, dtype{})
- }
- func set(jstr, path, raw string, stringify, del, optimistic, inplace bool) ([]byte, error) {
- if path == "" {
- if inplace {
- return yu_fast.S2B(jstr), &errorType{"path cannot be empty"}
- }
- return []byte(jstr), &errorType{"path cannot be empty"}
- }
- if !del && optimistic && isOptimisticPath(path) {
- res := 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) {
- jbytes := yu_fast.S2B(jstr)
- if stringify {
- jbytes[res.Index] = '"'
- copy(jbytes[res.Index+1:], 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:], 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 {
- if inplace {
- return yu_fast.S2B(jstr), &errorType{"cannot delete value from a complex path"}
- }
- 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 {
- if inplace {
- return yu_fast.S2B(jstr), err
- }
- return []byte(jstr), err
- }
- return njson, nil
- }
- func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) {
- res := 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 Result
- }
- vals := make([]val, 0, len(res.Indexes))
- res.ForEach(func(_, vres 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
- }
- }
- res, err := SetBytesOptions(yu_fast.S2B(json), 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 := yu_fast.B2S(json)
- var res []byte
- var err error
- switch v := value.(type) {
- default:
- b, merr := jsongo.Marshal(value)
- if merr != nil {
- return nil, merr
- }
- res, err = set(jstr, path, yu_fast.B2S(b), 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:
- res, err = set(jstr, path, yu_fast.B2S(v), 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, yu_strconv.FormatInt8(v),
- false, false, optimistic, inplace)
- case int16:
- res, err = set(jstr, path, yu_strconv.FormatInt16(v),
- false, false, optimistic, inplace)
- case int32:
- res, err = set(jstr, path, yu_strconv.FormatInt32(v),
- false, false, optimistic, inplace)
- case int64:
- res, err = set(jstr, path, yu_strconv.FormatInt64(v),
- false, false, optimistic, inplace)
- case uint8:
- res, err = set(jstr, path, yu_strconv.FormatUint8(v),
- false, false, optimistic, inplace)
- case uint16:
- res, err = set(jstr, path, yu_strconv.FormatUint16(v),
- false, false, optimistic, inplace)
- case uint32:
- res, err = set(jstr, path, yu_strconv.FormatUint32(v),
- false, false, optimistic, inplace)
- case uint64:
- res, err = set(jstr, path, yu_strconv.FormatUint64(v),
- 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) {
- var optimistic, inplace bool
- if opts != nil {
- optimistic = opts.Optimistic
- inplace = opts.ReplaceInPlace
- }
- res, err := set(yu_fast.B2S(json), path, yu_fast.B2S(value), false, false, optimistic, inplace)
- if err == errNoChange {
- return json, nil
- }
- return res, err
- }
|