Add @dig modifier for recursive descent searches

This commit adds the "@dig" modifier, which allows for searching
for values in deep or arbitrarily nested json documents

For example, using the following json:

```
{ "something": {
    "anything": {
      "abcdefg": {
          "finally": {
            "important": {
                "secret": "password"
            }
        }
      }
    }
  }
}
```

```
@dig:secret  ->  ["password"]
```

See #130
This commit is contained in:
tidwall 2023-08-09 14:28:31 -07:00
parent 8d2c36ffa4
commit e8e87f2a00
2 changed files with 124 additions and 20 deletions

View File

@ -2754,6 +2754,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)
@ -2762,14 +2763,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
@ -2789,19 +2796,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.
@ -3435,3 +3447,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)
}

View File

@ -2636,3 +2636,68 @@ func TestIssue301(t *testing.T) {
assert(t, Get(json, `fav\.movie.[1]`).String() == "[]")
}
func TestModDig(t *testing.T) {
json := `
{
"group": {
"issues": [
{
"fields": {
"labels": [
"milestone_1",
"group:foo",
"plan:a",
"plan:b"
]
},
"refid": "123"
},{
"fields": {
"labels": [
"milestone_2",
"group:foo",
"plan:a",
"plan"
]
},
"refid": "456"
},[
{"extra_deep":[{
"fields": {
"labels": [
"milestone_3",
"group:foo",
"plan:a",
"plan"
]
},
"refid": "789"
}]
}]
]
}
}
`
assert(t, Get(json, "group.@dig:#(refid=123)|0.fields.labels.0").String() == "milestone_1")
assert(t, Get(json, "group.@dig:#(refid=456)|0.fields.labels.0").String() == "milestone_2")
assert(t, Get(json, "group.@dig:#(refid=789)|0.fields.labels.0").String() == "milestone_3")
json = `
{ "something": {
"anything": {
"abcdefg": {
"finally": {
"important": {
"secret": "password",
"name": "jake"
}
},
"name": "melinda"
}
}
}
}`
assert(t, Get(json, "@dig:name").String() == `["melinda","jake"]`)
assert(t, Get(json, "@dig:secret").String() == `["password"]`)
}