noirc launch

This commit is contained in:
jeremyd
2025-09-18 10:52:11 -07:00
committed by jeremyd
parent 68faf82787
commit 7c8784ecdc
28 changed files with 2327 additions and 680 deletions

View File

@@ -1,7 +1,9 @@
<p align="center">
<img
src="logo.png"
width="240" height="78" border="0" alt="GJSON">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/.github/images/logo-dark.png">
<source media="(prefers-color-scheme: light)" srcset="/.github/images/logo-light.png">
<img src="/.github/images/logo-light.png" width="240" alt="GJSON" >
</picture>
<br>
<a href="https://godoc.org/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
<a href="https://tidwall.com/gjson-play"><img src="https://img.shields.io/badge/%F0%9F%8F%90-playground-9900cc.svg?style=flat-square" alt="GJSON Playground"></a>
@@ -176,7 +178,7 @@ The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits,
```go
result.Int() int64 // -9223372036854775808 to 9223372036854775807
result.Uint() int64 // 0 to 18446744073709551615
result.Uint() uint64 // 0 to 18446744073709551615
```
## Modifiers and path chaining
@@ -211,6 +213,7 @@ There are currently the following built-in modifiers:
- `@tostr`: Converts json to a string. Wraps a json string.
- `@fromstr`: Converts a string from json. Unwraps a json string.
- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db).
- `@dig`: Search for a value without providing its entire path. See [e8e87f2](https://github.com/tidwall/gjson/commit/e8e87f2a00dc41f3aba5631094e21f59a8cf8cbf).
### Modifier arguments
@@ -426,16 +429,6 @@ if result.Index > 0 {
This is a best-effort no allocation sub slice of the original json. This method utilizes the `result.Index` field, which is the position of the raw data in the original json. It's possible that the value of `result.Index` equals zero, in which case the `result.Raw` is converted to a `[]byte`.
## Get multiple values at once
The `GetMany` function can be used to get multiple values at the same time.
```go
results := gjson.GetMany(json, "name.first", "name.last", "age")
```
The return value is a `[]Result`, which will always contain exactly the same number of items as the input paths.
## Performance
Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/),
@@ -445,15 +438,15 @@ Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/js
and [json-iterator](https://github.com/json-iterator/go)
```
BenchmarkGJSONGet-16 11644512 311 ns/op 0 B/op 0 allocs/op
BenchmarkGJSONUnmarshalMap-16 1122678 3094 ns/op 1920 B/op 26 allocs/op
BenchmarkJSONUnmarshalMap-16 516681 6810 ns/op 2944 B/op 69 allocs/op
BenchmarkJSONUnmarshalStruct-16 697053 5400 ns/op 928 B/op 13 allocs/op
BenchmarkJSONDecoder-16 330450 10217 ns/op 3845 B/op 160 allocs/op
BenchmarkFFJSONLexer-16 1424979 2585 ns/op 880 B/op 8 allocs/op
BenchmarkEasyJSONLexer-16 3000000 729 ns/op 501 B/op 5 allocs/op
BenchmarkJSONParserGet-16 3000000 366 ns/op 21 B/op 0 allocs/op
BenchmarkJSONIterator-16 3000000 869 ns/op 693 B/op 14 allocs/op
BenchmarkGJSONGet-10 17893731 202.1 ns/op 0 B/op 0 allocs/op
BenchmarkGJSONUnmarshalMap-10 1663548 2157 ns/op 1920 B/op 26 allocs/op
BenchmarkJSONUnmarshalMap-10 832236 4279 ns/op 2920 B/op 68 allocs/op
BenchmarkJSONUnmarshalStruct-10 1076475 3219 ns/op 920 B/op 12 allocs/op
BenchmarkJSONDecoder-10 585729 6126 ns/op 3845 B/op 160 allocs/op
BenchmarkFFJSONLexer-10 2508573 1391 ns/op 880 B/op 8 allocs/op
BenchmarkEasyJSONLexer-10 3000000 537.9 ns/op 501 B/op 5 allocs/op
BenchmarkJSONParserGet-10 13707510 263.9 ns/op 21 B/op 0 allocs/op
BenchmarkJSONIterator-10 3000000 561.2 ns/op 693 B/op 14 allocs/op
```
JSON document used:
@@ -494,4 +487,6 @@ widget.image.hOffset
widget.text.onMouseUp
```
*These benchmarks were run on a MacBook Pro 16" 2.4 GHz Intel Core i9 using Go 1.17 and can be found [here](https://github.com/tidwall/gjson-benchmarks).*
**
*These benchmarks were run on a MacBook Pro M1 Max using Go 1.22 and can be found [here](https://github.com/tidwall/gjson-benchmarks).*

View File

@@ -1,6 +1,6 @@
# GJSON Path Syntax
A GJSON Path is a text string syntax that describes a search pattern for quickly retreiving values from a JSON payload.
A GJSON Path is a text string syntax that describes a search pattern for quickly retrieving values from a JSON payload.
This document is designed to explain the structure of a GJSON Path through examples.
@@ -15,12 +15,12 @@ This document is designed to explain the structure of a GJSON Path through examp
- [Multipaths](#multipaths)
- [Literals](#literals)
The definitive implemenation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson).
The definitive implementation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson).
Use the [GJSON Playground](https://gjson.dev) to experiment with the syntax online.
## Path structure
A GJSON Path is intended to be easily expressed as a series of components seperated by a `.` character.
A GJSON Path is intended to be easily expressed as a series of components separated by a `.` character.
Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, `!`, and `?`.
@@ -46,7 +46,7 @@ The following GJSON Paths evaluate to the accompanying values.
### Basic
In many cases you'll just want to retreive values by object name or array index.
In many cases you'll just want to retrieve values by object name or array index.
```go
name.last "Anderson"
@@ -137,12 +137,21 @@ next major release.*
The `~` (tilde) operator will convert a value to a boolean before comparison.
Supported tilde comparison type are:
```
~true Converts true-ish values to true
~false Converts false-ish and non-existent values to true
~null Converts null and non-existent values to true
~* Converts any existing value to true
```
For example, using the following JSON:
```json
{
"vals": [
{ "a": 1, "b": true },
{ "a": 1, "b": "data" },
{ "a": 2, "b": true },
{ "a": 3, "b": false },
{ "a": 4, "b": "0" },
@@ -157,15 +166,23 @@ For example, using the following JSON:
}
```
You can now query for all true(ish) or false(ish) values:
To query for all true-ish or false-ish values:
```
vals.#(b==~true)#.a >> [1,2,6,7,8]
vals.#(b==~true)#.a >> [2,6,7,8]
vals.#(b==~false)#.a >> [3,4,5,9,10,11]
```
The last value which was non-existent is treated as `false`
To query for null and explicit value existence:
```
vals.#(b==~null)#.a >> [10,11]
vals.#(b==~*)#.a >> [1,2,3,4,5,6,7,8,9,10]
vals.#(b!=~*)#.a >> [11]
```
### Dot vs Pipe
The `.` is standard separator, but it's also possible to use a `|`.
@@ -241,6 +258,7 @@ There are currently the following built-in modifiers:
- `@tostr`: Converts json to a string. Wraps a json string.
- `@fromstr`: Converts a string from json. Unwraps a json string.
- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db).
- `@dig`: Search for a value without providing its entire path. See [e8e87f2](https://github.com/tidwall/gjson/commit/e8e87f2a00dc41f3aba5631094e21f59a8cf8cbf).
#### Modifier arguments

View File

@@ -645,9 +645,9 @@ func tostr(json string) (raw string, str string) {
// Exists returns true if value exists.
//
// if gjson.Get(json, "name.last").Exists(){
// println("value exists")
// }
// if gjson.Get(json, "name.last").Exists(){
// println("value exists")
// }
func (t Result) Exists() bool {
return t.Type != Null || len(t.Raw) != 0
}
@@ -661,7 +661,6 @@ func (t Result) Exists() bool {
// nil, for JSON null
// map[string]interface{}, for JSON objects
// []interface{}, for JSON arrays
//
func (t Result) Value() interface{} {
if t.Type == String {
return t.Str
@@ -826,19 +825,28 @@ func parseArrayPath(path string) (r arrayPathResult) {
}
// splitQuery takes a query and splits it into three parts:
// path, op, middle, and right.
//
// path, op, middle, and right.
//
// So for this query:
// #(first_name=="Murphy").last
//
// #(first_name=="Murphy").last
//
// Becomes
// first_name # path
// =="Murphy" # middle
// .last # right
//
// first_name # path
// =="Murphy" # middle
// .last # right
//
// Or,
// #(service_roles.#(=="one")).cap
//
// #(service_roles.#(=="one")).cap
//
// Becomes
// service_roles.#(=="one") # path
// # middle
// .cap # right
//
// service_roles.#(=="one") # path
// # middle
// .cap # right
func parseQuery(query string) (
path, op, value, remain string, i int, vesc, ok bool,
) {
@@ -1009,8 +1017,8 @@ func parseObjectPath(path string) (r objectPathResult) {
r.piped = true
} else {
r.path = path[i+1:]
r.more = true
}
r.more = true
return
} else if path[i] == '|' {
r.part = string(epart)
@@ -1032,6 +1040,10 @@ func parseObjectPath(path string) (r objectPathResult) {
return
}
var vchars = [256]byte{
'"': 2, '{': 3, '(': 3, '[': 3, '}': 1, ')': 1, ']': 1,
}
func parseSquash(json string, i int) (int, string) {
// expects that the lead character is a '[' or '{' or '('
// squash the value, ignoring all nested arrays and objects.
@@ -1039,43 +1051,137 @@ func parseSquash(json string, i int) (int, string) {
s := i
i++
depth := 1
for ; i < len(json); i++ {
if json[i] >= '"' && json[i] <= '}' {
switch json[i] {
case '"':
var c byte
for i < len(json) {
for i < len(json)-8 {
jslice := json[i : i+8]
c = vchars[jslice[0]]
if c != 0 {
i += 0
goto token
}
c = vchars[jslice[1]]
if c != 0 {
i += 1
goto token
}
c = vchars[jslice[2]]
if c != 0 {
i += 2
goto token
}
c = vchars[jslice[3]]
if c != 0 {
i += 3
goto token
}
c = vchars[jslice[4]]
if c != 0 {
i += 4
goto token
}
c = vchars[jslice[5]]
if c != 0 {
i += 5
goto token
}
c = vchars[jslice[6]]
if c != 0 {
i += 6
goto token
}
c = vchars[jslice[7]]
if c != 0 {
i += 7
goto token
}
i += 8
}
c = vchars[json[i]]
if c == 0 {
i++
continue
}
token:
if c == 2 {
// '"' string
i++
s2 := i
nextquote:
for i < len(json)-8 {
jslice := json[i : i+8]
if jslice[0] == '"' {
i += 0
goto strchkesc
}
if jslice[1] == '"' {
i += 1
goto strchkesc
}
if jslice[2] == '"' {
i += 2
goto strchkesc
}
if jslice[3] == '"' {
i += 3
goto strchkesc
}
if jslice[4] == '"' {
i += 4
goto strchkesc
}
if jslice[5] == '"' {
i += 5
goto strchkesc
}
if jslice[6] == '"' {
i += 6
goto strchkesc
}
if jslice[7] == '"' {
i += 7
goto strchkesc
}
i += 8
}
goto strchkstd
strchkesc:
if json[i-1] != '\\' {
i++
s2 := i
for ; i < len(json); i++ {
if json[i] > '\\' {
continue
}
if json[i] == '"' {
// look for an escaped slash
if json[i-1] == '\\' {
n := 0
for j := i - 2; j > s2-1; j-- {
if json[j] != '\\' {
break
}
n++
}
if n%2 == 0 {
continue
}
}
break
}
}
case '{', '[', '(':
depth++
case '}', ']', ')':
depth--
if depth == 0 {
continue
}
strchkstd:
for i < len(json) {
if json[i] > '\\' || json[i] != '"' {
i++
return i, json[s:i]
continue
}
// look for an escaped slash
if json[i-1] == '\\' {
n := 0
for j := i - 2; j > s2-1; j-- {
if json[j] != '\\' {
break
}
n++
}
if n%2 == 0 {
i++
goto nextquote
}
}
break
}
} else {
// '{', '[', '(', '}', ']', ')'
// open close tokens
depth += int(c) - 2
if depth == 0 {
i++
return i, json[s:i]
}
}
i++
}
return i, json[s:]
}
@@ -1244,22 +1350,81 @@ func parseObject(c *parseContext, i int, path string) (int, bool) {
}
// matchLimit will limit the complexity of the match operation to avoid ReDos
// attacks from arbritary inputs.
// attacks from arbitrary inputs.
// See the github.com/tidwall/match.MatchLimit function for more information.
func matchLimit(str, pattern string) bool {
matched, _ := match.MatchLimit(str, pattern, 10000)
return matched
}
func falseish(t Result) bool {
switch t.Type {
case Null:
return true
case False:
return true
case String:
b, err := strconv.ParseBool(strings.ToLower(t.Str))
if err != nil {
return false
}
return !b
case Number:
return t.Num == 0
default:
return false
}
}
func trueish(t Result) bool {
switch t.Type {
case True:
return true
case String:
b, err := strconv.ParseBool(strings.ToLower(t.Str))
if err != nil {
return false
}
return b
case Number:
return t.Num != 0
default:
return false
}
}
func nullish(t Result) bool {
return t.Type == Null
}
func queryMatches(rp *arrayPathResult, value Result) bool {
rpv := rp.query.value
if len(rpv) > 0 && rpv[0] == '~' {
// convert to bool
rpv = rpv[1:]
if value.Bool() {
value = Result{Type: True}
} else {
value = Result{Type: False}
if len(rpv) > 0 {
if rpv[0] == '~' {
// convert to bool
rpv = rpv[1:]
var ish, ok bool
switch rpv {
case "*":
ish, ok = value.Exists(), true
case "null":
ish, ok = nullish(value), true
case "true":
ish, ok = trueish(value), true
case "false":
ish, ok = falseish(value), true
}
if ok {
rpv = "true"
if ish {
value = Result{Type: True}
} else {
value = Result{Type: False}
}
} else {
rpv = ""
value = Result{}
}
}
}
if !value.Exists() {
@@ -1850,6 +2015,16 @@ func appendHex16(dst []byte, x uint16) []byte {
)
}
// DisableEscapeHTML will disable the automatic escaping of certain
// "problamatic" HTML characters when encoding to JSON.
// These character include '>', '<' and '&', which get escaped to \u003e,
// \u0026, and \u003c respectively.
//
// This is a global flag and will affect all further gjson operations.
// Ideally, if used, it should be set one time before other gjson functions
// are called.
var DisableEscapeHTML = false
// AppendJSONString is a convenience function that converts the provided string
// to a valid JSON string and appends it to dst.
func AppendJSONString(dst []byte, s string) []byte {
@@ -1859,6 +2034,10 @@ func AppendJSONString(dst []byte, s string) []byte {
if s[i] < ' ' {
dst = append(dst, '\\')
switch s[i] {
case '\b':
dst = append(dst, 'b')
case '\f':
dst = append(dst, 'f')
case '\n':
dst = append(dst, 'n')
case '\r':
@@ -1869,7 +2048,8 @@ func AppendJSONString(dst []byte, s string) []byte {
dst = append(dst, 'u')
dst = appendHex16(dst, uint16(s[i]))
}
} else if s[i] == '>' || s[i] == '<' || s[i] == '&' {
} else if !DisableEscapeHTML &&
(s[i] == '>' || s[i] == '<' || s[i] == '&') {
dst = append(dst, '\\', 'u')
dst = appendHex16(dst, uint16(s[i]))
} else if s[i] == '\\' {
@@ -1918,23 +2098,23 @@ type parseContext struct {
// the '#' character.
// The dot and wildcard character can be escaped with '\'.
//
// {
// "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" >> ["Sara","Alex","Jack"]
// "children.#" >> 3
// "children.1" >> "Alex"
// "child*.2" >> "Jack"
// "c?ildren.0" >> "Sara"
// "friends.#.first" >> ["James","Roger"]
// {
// "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" >> ["Sara","Alex","Jack"]
// "children.#" >> 3
// "children.1" >> "Alex"
// "child*.2" >> "Jack"
// "c?ildren.0" >> "Sara"
// "friends.#.first" >> ["James","Roger"]
//
// 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.
@@ -2123,11 +2303,10 @@ func unescape(json string) string {
}
// Less return true if a token is less than another token.
// The caseSensitive paramater is used when the tokens are Strings.
// The caseSensitive parameter is used when the tokens are Strings.
// The order when comparing two different type is:
//
// Null < False < Number < String < True < JSON
//
// Null < False < Number < String < True < JSON
func (t Result) Less(token Result, caseSensitive bool) bool {
if t.Type < token.Type {
return true
@@ -2556,11 +2735,10 @@ func validnull(data []byte, i int) (outi int, ok bool) {
// Valid returns true if the input is valid json.
//
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
//
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
func Valid(json string) bool {
_, ok := validpayload(stringBytes(json), 0)
return ok
@@ -2568,13 +2746,12 @@ func Valid(json string) bool {
// ValidBytes returns true if the input is valid json.
//
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
//
// If working with bytes, this method preferred over ValidBytes(string(data))
//
func ValidBytes(json []byte) bool {
_, ok := validpayload(json, 0)
return ok
@@ -2690,6 +2867,7 @@ func execModifier(json, path string) (pathOut, res string, ok bool) {
var parsedArgs bool
switch pathOut[0] {
case '{', '[', '"':
// json arg
res := Parse(pathOut)
if res.Exists() {
args = squash(pathOut)
@@ -2698,14 +2876,20 @@ func execModifier(json, path string) (pathOut, res string, ok bool) {
}
}
if !parsedArgs {
idx := strings.IndexByte(pathOut, '|')
if idx == -1 {
args = pathOut
pathOut = ""
} else {
args = pathOut[:idx]
pathOut = pathOut[idx:]
// simple arg
i := 0
for ; i < len(pathOut); i++ {
if pathOut[i] == '|' {
break
}
switch pathOut[i] {
case '{', '[', '"', '(':
s := squash(pathOut[i:])
i += len(s) - 1
}
}
args = pathOut[:i]
pathOut = pathOut[i:]
}
}
return pathOut, fn(json, args), true
@@ -2725,19 +2909,24 @@ func unwrap(json string) string {
// DisableModifiers will disable the modifier syntax
var DisableModifiers = false
var modifiers = map[string]func(json, arg string) string{
"pretty": modPretty,
"ugly": modUgly,
"reverse": modReverse,
"this": modThis,
"flatten": modFlatten,
"join": modJoin,
"valid": modValid,
"keys": modKeys,
"values": modValues,
"tostr": modToStr,
"fromstr": modFromStr,
"group": modGroup,
var modifiers map[string]func(json, arg string) string
func init() {
modifiers = map[string]func(json, arg string) string{
"pretty": modPretty,
"ugly": modUgly,
"reverse": modReverse,
"this": modThis,
"flatten": modFlatten,
"join": modJoin,
"valid": modValid,
"keys": modKeys,
"values": modValues,
"tostr": modToStr,
"fromstr": modFromStr,
"group": modGroup,
"dig": modDig,
}
}
// AddModifier binds a custom modifier command to the GJSON syntax.
@@ -2848,9 +3037,13 @@ func modReverse(json, arg string) string {
}
// @flatten an array with child arrays.
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]]
//
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]]
//
// The {"deep":true} arg can be provide for deep flattening.
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7]
//
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7]
//
// The original json is returned when the json is not an array.
func modFlatten(json, arg string) string {
res := Parse(json)
@@ -2895,7 +3088,8 @@ func modFlatten(json, arg string) string {
}
// @keys extracts the keys from an object.
// {"first":"Tom","last":"Smith"} -> ["first","last"]
//
// {"first":"Tom","last":"Smith"} -> ["first","last"]
func modKeys(json, arg string) string {
v := Parse(json)
if !v.Exists() {
@@ -2922,7 +3116,8 @@ func modKeys(json, arg string) string {
}
// @values extracts the values from an object.
// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"]
//
// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"]
func modValues(json, arg string) string {
v := Parse(json)
if !v.Exists() {
@@ -2947,11 +3142,17 @@ func modValues(json, arg string) string {
}
// @join multiple objects into a single object.
// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"}
//
// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"}
//
// The arg can be "true" to specify that duplicate keys should be preserved.
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41}
//
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41}
//
// Without preserved keys:
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41}
//
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41}
//
// The original json is returned when the json is not an object.
func modJoin(json, arg string) string {
res := Parse(json)
@@ -3024,7 +3225,8 @@ func modValid(json, arg string) string {
}
// @fromstr converts a string to json
// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"}
//
// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"}
func modFromStr(json, arg string) string {
if !Valid(json) {
return ""
@@ -3033,7 +3235,8 @@ func modFromStr(json, arg string) string {
}
// @tostr converts a string to json
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
//
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
func modToStr(str, arg string) string {
return string(AppendJSONString(nil, str))
}
@@ -3210,11 +3413,11 @@ func revSquash(json string) string {
// Paths returns the original GJSON paths for a Result where the Result came
// from a simple query path that returns an array, like:
//
// gjson.Get(json, "friends.#.first")
// gjson.Get(json, "friends.#.first")
//
// The returned value will be in the form of a JSON array:
//
// ["friends.0.first","friends.1.first","friends.2.first"]
// ["friends.0.first","friends.1.first","friends.2.first"]
//
// The param 'json' must be the original JSON used when calling Get.
//
@@ -3239,11 +3442,11 @@ func (t Result) Paths(json string) []string {
// Path returns the original GJSON path for a Result where the Result came
// from a simple path that returns a single value, like:
//
// gjson.Get(json, "friends.#(last=Murphy)")
// gjson.Get(json, "friends.#(last=Murphy)")
//
// The returned value will be in the form of a JSON string:
//
// "friends.0"
// "friends.0"
//
// The param 'json' must be the original JSON used when calling Get.
//
@@ -3259,7 +3462,7 @@ func (t Result) Path(json string) string {
goto fail
}
if !strings.HasPrefix(json[t.Index:], t.Raw) {
// Result is not at the JSON index as exepcted.
// Result is not at the JSON index as expected.
goto fail
}
for ; i >= 0; i-- {
@@ -3320,7 +3523,7 @@ func (t Result) Path(json string) string {
if !rcomp.Exists() {
goto fail
}
comp := escapeComp(rcomp.String())
comp := Escape(rcomp.String())
path = append(path, '.')
path = append(path, comp...)
}
@@ -3335,17 +3538,31 @@ fail:
// isSafePathKeyChar returns true if the input character is safe for not
// needing escaping.
func isSafePathKeyChar(c byte) bool {
return c <= ' ' || c > '~' || c == '_' || c == '-' || c == ':' ||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9')
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c <= ' ' || c > '~' || c == '_' ||
c == '-' || c == ':'
}
// escapeComp escaped a path compontent, making it safe for generating a
// path for later use.
func escapeComp(comp string) string {
// Escape returns an escaped path component.
//
// json := `{
// "user":{
// "first.name": "Janet",
// "last.name": "Prichard"
// }
// }`
// user := gjson.Get(json, "user")
// println(user.Get(gjson.Escape("first.name"))
// println(user.Get(gjson.Escape("last.name"))
// // Output:
// // Janet
// // Prichard
func Escape(comp string) string {
for i := 0; i < len(comp); i++ {
if !isSafePathKeyChar(comp[i]) {
ncomp := []byte(comp[:i])
ncomp := make([]byte, len(comp)+1)
copy(ncomp, comp[:i])
ncomp = ncomp[:i]
for ; i < len(comp); i++ {
if !isSafePathKeyChar(comp[i]) {
ncomp = append(ncomp, '\\')
@@ -3357,3 +3574,30 @@ func escapeComp(comp string) string {
}
return comp
}
func parseRecursiveDescent(all []Result, parent Result, path string) []Result {
if res := parent.Get(path); res.Exists() {
all = append(all, res)
}
if parent.IsArray() || parent.IsObject() {
parent.ForEach(func(_, val Result) bool {
all = parseRecursiveDescent(all, val, path)
return true
})
}
return all
}
func modDig(json, arg string) string {
all := parseRecursiveDescent(nil, Parse(json), arg)
var out []byte
out = append(out, '[')
for i, res := range all {
if i > 0 {
out = append(out, ',')
}
out = append(out, res.Raw...)
}
out = append(out, ']')
return string(out)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB