From 3b5bf6bb5ee383f15001308b2861341fffc9ca66 Mon Sep 17 00:00:00 2001 From: tidwall Date: Sat, 29 Jun 2019 15:23:32 -0700 Subject: [PATCH] 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. --- gjson.go | 188 +++++++++++++++++++++++++++++++++++++++++++++++--- gjson_test.go | 49 +++++++++++++ 2 files changed, 227 insertions(+), 10 deletions(-) diff --git a/gjson.go b/gjson.go index 9757d4f..8221cd9 100644 --- a/gjson.go +++ b/gjson.go @@ -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,22 +1701,84 @@ 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 !DisableModifiers { - if len(path) > 1 && path[0] == '@' { - // possible modifier + if len(path) > 1 { + if !DisableModifiers { + if path[0] == '@' { + // possible modifier + var ok bool + var rjson string + path, rjson, ok = execModifier(json, path) + if ok { + if len(path) > 0 && (path[0] == '|' || path[0] == '.') { + res := Get(rjson, path[1:]) + res.Index = 0 + return res + } + return Parse(rjson) + } + } + } + if path[0] == '[' || path[0] == '{' { + // using a subselector path + kind := path[0] var ok bool - var rjson string - path, rjson, ok = execModifier(json, path) + var subs []subSelector + subs, path, ok = parseSubSelectors(path) if ok { - if len(path) > 0 && (path[0] == '|' || path[0] == '.') { - res := Get(rjson, path[1:]) + 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 } - return Parse(rjson) } } } + var i int var c = &parseContext{json: json} if len(path) >= 2 && path[0] == '.' && path[1] == '.' { diff --git a/gjson_test.go b/gjson_test.go index 1cf382c..a871591 100644 --- a/gjson_test.go +++ b/gjson_test.go @@ -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]]`) +}