Added Subselectors

It now possible to select multiple independent paths and join
their results into a single JSON document.

For example, given the following JSON

    {
      "info": {
        "friends": [
          {"first": "Dale", "last": "Murphy", "age": 44},
          {"first": "Roger", "last": "Craig", "age": 68},
          {"first": "Jane", "last": "Murphy", "age": 47}
        ]
      }
    }

The path `[info.friends.0.first,info.friends.1.last]` returns

    ["Dale","Craig"]

Or path `{info.friends.0.first,info.friends.1.last}` returns

    {"first":"Dale","last":"Craig"}

You can also rename Object members such as

`{"alt1":info.friends.0.first,"alt2":info.friends.1.last}` returns

    {"alt1":"Dale","alt2":"Craig"}

Finally you can combine this with any GJSON component

`info.friends.[0.first,1.age]` returns

    ["Dale",68]

This feature was request by @errashe in issue #113.
This commit is contained in:
tidwall 2019-06-29 15:23:32 -07:00
parent 001444ea45
commit 3b5bf6bb5e
2 changed files with 227 additions and 10 deletions

174
gjson.go
View File

@ -865,10 +865,12 @@ func parseObjectPath(path string) (r objectPathResult) {
return
}
if path[i] == '.' {
// peek at the next byte and see if it's a '@' modifier.
// peek at the next byte and see if it's a '@', '[', or '{'.
r.part = path[:i]
if !DisableModifiers &&
i < len(path)-1 && path[i+1] == '@' {
i < len(path)-1 &&
(path[i+1] == '@' ||
path[i+1] == '[' || path[i+1] == '{') {
r.pipe = path[i+1:]
r.piped = true
} else {
@ -1552,6 +1554,110 @@ func ForEachLine(json string, iterator func(line Result) bool) {
}
}
type subSelector struct {
name string
path string
}
// parseSubSelectors returns the subselectors belonging to a '[path1,path2]' or
// '{"field1":path1,"field2":path2}' type subSelection. It's expected that the
// first character in path is either '[' or '{', and has already been checked
// prior to calling this function.
func parseSubSelectors(path string) (sels []subSelector, out string, ok bool) {
depth := 1
colon := 0
start := 1
i := 1
pushSel := func() {
var sel subSelector
if colon == 0 {
sel.path = path[start:i]
} else {
sel.name = path[start:colon]
sel.path = path[colon+1 : i]
}
sels = append(sels, sel)
colon = 0
start = i + 1
}
for ; i < len(path); i++ {
switch path[i] {
case '\\':
i++
case ':':
if depth == 1 {
colon = i
}
case ',':
if depth == 1 {
pushSel()
}
case '"':
i++
loop:
for ; i < len(path); i++ {
switch path[i] {
case '\\':
i++
case '"':
break loop
}
}
case '[', '(', '{':
depth++
case ']', ')', '}':
depth--
if depth == 0 {
pushSel()
path = path[i+1:]
return sels, path, true
}
}
}
return
}
// nameOfLast returns the name of the last component
func nameOfLast(path string) string {
for i := len(path) - 1; i >= 0; i-- {
if path[i] == '|' || path[i] == '.' {
if i > 0 {
if path[i-1] == '\\' {
continue
}
}
return path[i+1:]
}
}
return path
}
func isSimpleName(component string) bool {
for i := 0; i < len(component); i++ {
if component[i] < ' ' {
return false
}
switch component[i] {
case '[', ']', '{', '}', '(', ')', '#', '|':
return false
}
}
return true
}
func appendJSONString(dst []byte, s string) []byte {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 {
d, _ := json.Marshal(s)
return append(dst, string(d)...)
}
}
dst = append(dst, '"')
dst = append(dst, s...)
dst = append(dst, '"')
return dst
}
type parseContext struct {
json string
value Result
@ -1595,8 +1701,9 @@ type parseContext struct {
// If you are consuming JSON from an unpredictable source then you may want to
// use the Valid function first.
func Get(json, path string) Result {
if len(path) > 1 {
if !DisableModifiers {
if len(path) > 1 && path[0] == '@' {
if path[0] == '@' {
// possible modifier
var ok bool
var rjson string
@ -1611,6 +1718,67 @@ func Get(json, path string) Result {
}
}
}
if path[0] == '[' || path[0] == '{' {
// using a subselector path
kind := path[0]
var ok bool
var subs []subSelector
subs, path, ok = parseSubSelectors(path)
if ok {
if len(path) == 0 || (path[0] == '|' || path[0] == '.') {
var b []byte
b = append(b, kind)
var i int
for _, sub := range subs {
res := Get(json, sub.path)
if res.Exists() {
if i > 0 {
b = append(b, ',')
}
if kind == '{' {
if len(sub.name) > 0 {
if sub.name[0] == '"' && Valid(sub.name) {
b = append(b, sub.name...)
} else {
b = appendJSONString(b, sub.name)
}
} else {
last := nameOfLast(sub.path)
if isSimpleName(last) {
b = appendJSONString(b, last)
} else {
b = appendJSONString(b, "_")
}
}
b = append(b, ':')
}
var raw string
if len(res.Raw) == 0 {
raw = res.String()
if len(raw) == 0 {
raw = "null"
}
} else {
raw = res.Raw
}
b = append(b, raw...)
i++
}
}
b = append(b, kind+2)
var res Result
res.Raw = string(b)
res.Type = JSON
if len(path) > 0 {
res = res.Get(path[1:])
}
res.Index = 0
return res
}
}
}
}
var i int
var c = &parseContext{json: json}
if len(path) >= 2 && path[0] == '.' && path[1] == '.' {

View File

@ -1862,3 +1862,52 @@ func TestParenQueries(t *testing.T) {
assert(t, Get(json, "friends.#(a>10)#|#").Int() == 3)
assert(t, Get(json, "friends.#(a>40)#|#").Int() == 0)
}
func TestSubSelectors(t *testing.T) {
json := `{
"info": {
"friends": [
{
"first": "Dale", "last": "Murphy", "kind": "Person",
"cust1": true,
"extra": [10,20,30],
"details": {
"city": "Tempe",
"state": "Arizona"
}
},
{
"first": "Roger", "last": "Craig", "kind": "Person",
"cust2": false,
"extra": [40,50,60],
"details": {
"city": "Phoenix",
"state": "Arizona"
}
}
]
}
}`
assert(t, Get(json, "[]").String() == "[]")
assert(t, Get(json, "{}").String() == "{}")
res := Get(json, `{`+
`abc:info.friends.0.first,`+
`info.friends.1.last,`+
`"a`+"\r"+`a":info.friends.0.kind,`+
`"abc":info.friends.1.kind,`+
`{123:info.friends.1.cust2},`+
`[info.friends.#[details.city="Phoenix"]#|#]`+
`}.@pretty.@ugly`).String()
// println(res)
// {"abc":"Dale","last":"Craig","\"a\ra\"":"Person","_":{"123":false},"_":[1]}
assert(t, Get(res, "abc").String() == "Dale")
assert(t, Get(res, "last").String() == "Craig")
assert(t, Get(res, "\"a\ra\"").String() == "Person")
assert(t, Get(res, "@reverse.abc").String() == "Person")
assert(t, Get(res, "_.123").String() == "false")
assert(t, Get(res, "@reverse._.0").String() == "1")
assert(t, Get(json, "info.friends.[0.first,1.extra.0]").String() ==
`["Dale",40]`)
assert(t, Get(json, "info.friends.#.[first,extra.0]").String() ==
`[["Dale",10],["Roger",40]]`)
}