forked from mirror/gjson
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:
parent
001444ea45
commit
3b5bf6bb5e
174
gjson.go
174
gjson.go
|
@ -865,10 +865,12 @@ func parseObjectPath(path string) (r objectPathResult) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if path[i] == '.' {
|
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]
|
r.part = path[:i]
|
||||||
if !DisableModifiers &&
|
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.pipe = path[i+1:]
|
||||||
r.piped = true
|
r.piped = true
|
||||||
} else {
|
} 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 {
|
type parseContext struct {
|
||||||
json string
|
json string
|
||||||
value Result
|
value Result
|
||||||
|
@ -1595,8 +1701,9 @@ type parseContext struct {
|
||||||
// If you are consuming JSON from an unpredictable source then you may want to
|
// If you are consuming JSON from an unpredictable source then you may want to
|
||||||
// use the Valid function first.
|
// use the Valid function first.
|
||||||
func Get(json, path string) Result {
|
func Get(json, path string) Result {
|
||||||
|
if len(path) > 1 {
|
||||||
if !DisableModifiers {
|
if !DisableModifiers {
|
||||||
if len(path) > 1 && path[0] == '@' {
|
if path[0] == '@' {
|
||||||
// possible modifier
|
// possible modifier
|
||||||
var ok bool
|
var ok bool
|
||||||
var rjson string
|
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 i int
|
||||||
var c = &parseContext{json: json}
|
var c = &parseContext{json: json}
|
||||||
if len(path) >= 2 && path[0] == '.' && path[1] == '.' {
|
if len(path) >= 2 && path[0] == '.' && path[1] == '.' {
|
||||||
|
|
|
@ -1862,3 +1862,52 @@ func TestParenQueries(t *testing.T) {
|
||||||
assert(t, Get(json, "friends.#(a>10)#|#").Int() == 3)
|
assert(t, Get(json, "friends.#(a>10)#|#").Int() == 3)
|
||||||
assert(t, Get(json, "friends.#(a>40)#|#").Int() == 0)
|
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]]`)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue