pax_global_header00006660000000000000000000000064142730675170014526gustar00rootroot0000000000000052 comment=b279807a1bad9fa155988667d97a3d2fff4061b5 sjson-1.2.5/000077500000000000000000000000001427306751700126675ustar00rootroot00000000000000sjson-1.2.5/.github/000077500000000000000000000000001427306751700142275ustar00rootroot00000000000000sjson-1.2.5/.github/workflows/000077500000000000000000000000001427306751700162645ustar00rootroot00000000000000sjson-1.2.5/.github/workflows/go.yml000066400000000000000000000012221427306751700174110ustar00rootroot00000000000000name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.x uses: actions/setup-go@v2 with: go-version: ^1.13 - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Get dependencies run: | go get -v -t -d ./... if [ -f Gopkg.toml ]; then curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh dep ensure fi - name: Build run: go build -v . - name: Test run: go test -v . sjson-1.2.5/LICENSE000066400000000000000000000020661427306751700137000ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Josh Baker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sjson-1.2.5/README.md000066400000000000000000000144511427306751700141530ustar00rootroot00000000000000

SJSON
GoDoc

set a json value quickly

SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). For a command line interface check out [JJ](https://github.com/tidwall/jj). Getting Started =============== Installing ---------- To start using SJSON, install Go and run `go get`: ```sh $ go get -u github.com/tidwall/sjson ``` This will retrieve the library. Set a value ----------- Set sets the 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 validated. Invalid json will not panic, but it may return back unexpected results. Invalid paths may return an error. ```go package main import "github.com/tidwall/sjson" const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` func main() { value, _ := sjson.Set(json, "name.last", "Anderson") println(value) } ``` This will print: ```json {"name":{"first":"Janet","last":"Anderson"},"age":47} ``` Path syntax ----------- A path is a series of keys separated by a dot. The dot and colon characters can be escaped with ``\``. ```json { "name": {"first": "Tom", "last": "Anderson"}, "age":37, "children": ["Sara","Alex","Jack"], "fav.movie": "Deer Hunter", "friends": [ {"first": "James", "last": "Murphy"}, {"first": "Roger", "last": "Craig"} ] } ``` ``` "name.last" >> "Anderson" "age" >> 37 "children.1" >> "Alex" "friends.1.last" >> "Craig" ``` The `-1` key can be used to append a value to an existing array: ``` "children.-1" >> appends a new value to the end of the children array ``` Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: ```json { "users":{ "2313":{"name":"Sara"}, "7839":{"name":"Andy"} } } ``` A colon path would look like: ``` "users.:2313.name" >> "Sara" ``` Supported types --------------- Pretty much any type is supported: ```go sjson.Set(`{"key":true}`, "key", nil) sjson.Set(`{"key":true}`, "key", false) sjson.Set(`{"key":true}`, "key", 1) sjson.Set(`{"key":true}`, "key", 10.5) sjson.Set(`{"key":true}`, "key", "hello") sjson.Set(`{"key":true}`, "key", []string{"hello", "world"}) sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) ``` When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. Examples -------- Set a value from empty document: ```go value, _ := sjson.Set("", "name", "Tom") println(value) // Output: // {"name":"Tom"} ``` Set a nested value from empty document: ```go value, _ := sjson.Set("", "name.last", "Anderson") println(value) // Output: // {"name":{"last":"Anderson"}} ``` Set a new value: ```go value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") println(value) // Output: // {"name":{"first":"Sara","last":"Anderson"}} ``` Update an existing value: ```go value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") println(value) // Output: // {"name":{"last":"Smith"}} ``` Set a new array value: ```go value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") println(value) // Output: // {"friends":["Andy","Carol","Sara"] ``` Append an array value by using the `-1` key in a path: ```go value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") println(value) // Output: // {"friends":["Andy","Carol","Sara"] ``` Append an array value that is past the end: ```go value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") println(value) // Output: // {"friends":["Andy","Carol",null,null,"Sara"] ``` Delete a value: ```go value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") println(value) // Output: // {"name":{"last":"Anderson"}} ``` Delete an array value: ```go value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") println(value) // Output: // {"friends":["Andy"]} ``` Delete the last array value: ```go value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") println(value) // Output: // {"friends":["Andy"]} ``` ## Performance Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), [ffjson](https://github.com/pquerna/ffjson), [EasyJSON](https://github.com/mailru/easyjson), and [Gabs](https://github.com/Jeffail/gabs) ``` Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op ``` JSON document used: ```json { "widget": { "debug": "on", "window": { "title": "Sample Konfabulator Widget", "name": "main_window", "width": 500, "height": 500 }, "image": { "src": "Images/Sun.png", "hOffset": 250, "vOffset": 250, "alignment": "center" }, "text": { "data": "Click Here", "size": 36, "style": "bold", "vOffset": 100, "alignment": "center", "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" } } } ``` Each operation was rotated though one of the following search paths: ``` widget.window.name widget.image.hOffset widget.text.onMouseUp ``` *These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7 and can be be found [here](https://github.com/tidwall/sjson-benchmarks)*. ## Contact Josh Baker [@tidwall](http://twitter.com/tidwall) ## License SJSON source code is available under the MIT [License](/LICENSE). sjson-1.2.5/go.mod000066400000000000000000000001721427306751700137750ustar00rootroot00000000000000module github.com/tidwall/sjson go 1.14 require ( github.com/tidwall/gjson v1.14.2 github.com/tidwall/pretty v1.2.0 ) sjson-1.2.5/go.sum000066400000000000000000000007711427306751700140270ustar00rootroot00000000000000github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= sjson-1.2.5/logo.png000066400000000000000000000407521427306751700143450ustar00rootroot00000000000000PNG  IHDR S?ytEXtSoftwareAdobe ImageReadyqe<iTXtXML:com.adobe.xmp ̪=IDATxoLUwqzsBc.p\B3 - > CbFԜcr<ɭ@M>IhObpIjcBId o^ı'_X ޛ~+~K?S D<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<@<yp/?Lz?LyxnFNAṱ;h@ F{z>\.==%?b3EW+ohCwck>-#K ?lկs6lD](k5rPBznnƦMr,rmXB~!.N. ۷{x x`ɡ7543;~%HZ[ 3+V{c==ܙ>%I Jaa^Yy򝓢\_ \*rrڐ  ] FFf^8L(`9[=u}'+ ÿ>_ ,==c[p^ZN"oax͵m#"1@"צG ^rU_*)2yh $Y_:&.Lew2WB/3y4JÉ3->tg.$܀1 `pMDS${nkqikD,47@&~)XώnOɝZӁAX>=4P]FڋfL&$.?JPЁAU"y1bQ#$[GhɮL|BfLiE3:It xJ6`KIS3v Oh꿶ͪg?f{-XGKxd(_ 1ej$Qޕ<XW490&%`8w;:F %!  l&OB "-ghIGZUUHl`Dj&顗_<٠ 7.,@KKXݛW#Ȼq/#S@9ev@BA_s3κ*JaXX]'3l M0`j*J7/TiתB8^:P *¢ۗ.dil lz\4kW _XIH03311]EuׇA61D#xR---c?<ۦA<f1O\/[KbZ`d}.K,B'ĉH$2z~ɒzz^З*->O/'̏̉F-˳măyeY`Vy9^ZKBPr131m$ kg}}w;:YBpQ2Olyo$5<$e¯޸)('@R/lQ ~w>Hz>E{ˎ<׀'ʓ7X0hw|%RZSsrl콁k&s54qK.,lj9tѰst/' ht08L]ן`Lxݒ# d|oġJ,z_Vaၖ): `T4sڬrj:;8Do@<5|R?| $vB!hȤd+RzZ I"._V1n4X1nlhr6&xx#FIu?^HxdI[TVd" OBɱdL _V1sa'ўw#{x?)?BmSu}]QފXN@lO 4ߩa!OsBcg*&ă)ݽ6n|ؠ]+T(|fjiMq$_?2xG'۶߿kVcƵ31 i^0и**ƄsTibǛhg}A3㋂AE%oظQ7޶/<̭;X3I%!2YyOc5mo+^(7eA;s>J$ʎ':X=<Ӻm./_}Wuu jտd)sQ᜛k#sf&N< JB ,UXߤ]2Ujܝ I[V5|E55Ё@t ,y[4bEVTЇg9Rҁ ap=Z<#ݨxЇz~Yˆx`G^o;G'GK;s&>IEŋ-&y Sݎf^MCӝ $*pn>lp3cEow1~ն.^>*udO3Gn~I7lg1]ѨWK6nJ?DFX5}3F{z@ XMӼnf#V^F`6B\4.rpxE*w7NPxTt =鑛ף%c]hC^˞tX];Qkw'ˉPQ@ X%h/^_[W|HHe.XYu"/x:oץS=RקY$ } \ְ…rB K6?؏+WU)Lkt bgBBc -[JRUh^H1AgoNϫ#X"h=ϣXTT>WT}ffg]c>⣎LH AVTHN)E=6VQEZ 3&șG':v?(i Aۭ yy}MM4V*af| * ⁝y$L \8pyN쇐'kk? Mi)t N옖m?ٰ)HH3s6/Ɖj ȅާ)0=rh {! O1T:1](#+Ё@RXx` 7VyHף%WFRmn%vk'XQ0OF}C]C4 :G]LmABx B0qVE#8jM%G.TuABBɱ1 aW醹@@/ʎ3!!05pv@0vןZ 4 $>@@u@H=!)8!A6m%ck= ҏh./]cvʎiVzўN<ڂ=:"_# r;B nmM`#//@Qǘg ~ 3 7ZЭS j]6aLn!ِ8sVw@HЬm~?457sX7'ABBFNܭSLS+lIPkW~ۭ3#vOrO}rv@!aq Tyݎ&!(oIzEھF񍦹:XOnv\ɡ+ZZ KuE <Tކ54w jm%-3VA]j=DqUݬᑻZPZW(@Ov?2?۰qsS_dtl 0e+P=E[elbH]].;鑛%mLKHYX4Srf1 >v ̌nm좽nHNpyZPk鴄,Wwk{|sԻ4B\46dSa㦒#}7% oZ Jqc,^lDqgZKDUUG]-!vFS[^Y)܁i$7-!r;C(`%Vۑ F۪Fg2weۧ.$xsRmjsh-`2c._V1}-bֺKZW4_R; @b||ՏhEF򙣳af|| n&C<@CeÍƙ!A-; !EU~$rrlL;FOfgc)1=ivbB_v Zw ɈBބs_n;%$=(ui;:xxEÅgvWd2:) $bRhR[+_>sI'gsyg/]^SPyƶ&g+yo`ľXJL:8@^敋6遯~rhOi| !*B,ޕ#GױrJ]F1X$b)ST8ļ.`Q/ ׇ`ġ>}D8+^Υ9 J&vP&@WBx938@KOXH׋Rjj!ๆh kdbBOfg}V]x;(tCwφ_Lv7ȟB.`O?΅jKx'~}S3ʻXH[Zhs$>hhG,e^ܺ 8;w`k-?`tҥ4ov\$s~QIQu\zr>G"e7-k]hzfWh$I G-R; 2Y==käugW͐o眿oA嵹S*5˟/vCSw@ 8e苏|z 4e_}Y]Zc롆<%QѬv3ⲉepWUkt 2=rsty+?z8y8F1QO c*bUx, 1f$#R kiC O%c3)F~ZṰl{%ivt#(Z* y/JYXȬIr/BUkC 7~|WO}鑛ʏ?#BUMMӊ n)KȏDU"'”m$uH#l&okߧzleb OANA4' E̓ {6/ıF'Y<e'xUXȩ!X+IÊV4=rz$=hγڗj")@3N@ ˆUцi!A9$S6w[ &(?SkX3b#Ё@2ZnJ\6lcAJ(ekkPл 77HTWR k7F=XSTI -Ρ2k r W[{#z^rB K9R|ׁ#ZRnR#M[[N#AvNNjt9 9I(5k׭xS 0x>*󙤾a!Vtvjj8_x駟~r_7dṰ;濶\I#_?p- 3w7lokۿ-m)_\^o~l]gf7ˠ[sY$%Ò\rM޾t=p֐TfmH>.8qB.d,$WK_sF#oSccod,49Nm?=yJNg9R_N:9 >=r3U]xo}W/O\z{ 4JO i3*WB\9*G^;c==Fo,]fF8:cR>=rEŔPz.tRw6EO9$,^xLޑäY^ S_eh\XNr/GmK`l$jbWŠ2L-}YcA{0;1!߭xs򽁁P4`\@5k ~]wp/}R!/oo``_6X\MƂ :_~-1i} }D".rЁ@<0p6!F2/ ͯ9Êl<55؏a}HrDH=PA2P>aAbm<`꼷(("Ё@<ЎaE6`)L\qҟx"i٤ĉʂBRp0>oމj ]Н}\F%JJY2z}}QCLfJ 2 n3C +I[;#s˥}T|IW4ʰ"k5|!FF_3._l;: }J)}XQ¬f&f Ϗ55ђYWʃ|?K#-F@oOLq퐺>dWм (Z‡f)bkЁ@nKSAd4xv76vg}Q* նDx! ao`w4p=ԑҳxϰ6rR3Y >M7K$EFn;݆f*Xɰ<Ӂ@0n߱ˏDF&K?BFڦGn ߾Yu5=N/ա'}MM}g3` g"tOٚ$%m}4եq6/'w Af&XMa\0kFpwc5ͥ5}ԏwEׂ ,==VTf{xn߇G'hRקmKi%sEEă:rivtpw?+>|`@H x !x@<Й05pU*{1eQ~$9ar%!ܽr$8`|hh]z|V쬫sg( v \lNH]_Ljmi<[p%gIT uHvU.!Cpr3xXsMI*.=ЗiMGhyLfOܢz{*.LQ/ :pȧVN7yhT̶O ~a2uD_:'sۛ RקqR z]_kN/|~g3^_Y|̉5/wx,^6^@<0z?OYG~~)glO}j&0 /{{1Fτ'/<5,셙;[JիѶIMG~/ܑ?[,ʝ^2?-1{ꕐҍJxjynYP\ @<L ϻ/̍>                     X 0rT&IENDB`sjson-1.2.5/sjson.go000066400000000000000000000443601427306751700143610ustar00rootroot00000000000000// 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{}) } // DeleteBytes deletes a value from json for the specified path. func DeleteBytes(json []byte, path string) ([]byte, error) { return SetBytes(json, path, dtype{}) } 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 } sjson-1.2.5/sjson_test.go000066400000000000000000000233671427306751700154240ustar00rootroot00000000000000package 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() } }