Support subqueries

It's now possible to do a query like

  topology.instances.#(service_roles.#(=="one"))#.service_version

On a JSON document such as

  {
    "topology": {
      "instances": [{
        "service_version": "1.2.3",
        "service_roles": ["one", "two"]
      },{
        "service_version": "1.2.4",
        "service_roles": ["three", "four"]
      },{
        "service_version": "1.2.2",
        "service_roles": ["one"]
      }]
    }
  }

Resulting in

  ["1.2.3","1.2.2"]
This commit is contained in:
tidwall 2019-07-12 06:14:45 -07:00
parent 90ca17622f
commit 1e964df7d9
2 changed files with 286 additions and 83 deletions

300
gjson.go
View File

@ -736,106 +736,121 @@ func parseArrayPath(path string) (r arrayPathResult) {
r.alogkey = path[2:] r.alogkey = path[2:]
r.path = path[:1] r.path = path[:1]
} else if path[1] == '[' || path[1] == '(' { } else if path[1] == '[' || path[1] == '(' {
var end byte
if path[1] == '[' {
end = ']'
} else {
end = ')'
}
r.query.on = true
// query // query
i += 2 r.query.on = true
// whitespace if true {
for ; i < len(path); i++ { qpath, op, value, _, fi, ok := parseQuery(path[i:])
if path[i] > ' ' { if !ok {
// bad query, end now
break break
} }
} r.query.path = qpath
s := i r.query.op = op
for ; i < len(path); i++ { r.query.value = value
if path[i] <= ' ' || i = fi - 1
path[i] == '!' || if i+1 < len(path) && path[i+1] == '#' {
path[i] == '=' || r.query.all = true
path[i] == '<' ||
path[i] == '>' ||
path[i] == '%' ||
path[i] == end {
break
} }
} } else {
r.query.path = path[s:i] var end byte
// whitespace if path[1] == '[' {
for ; i < len(path); i++ { end = ']'
if path[i] > ' ' { } else {
break end = ')'
} }
} i += 2
if i < len(path) {
s = i
if path[i] == '!' {
if i < len(path)-1 && (path[i+1] == '=' ||
path[i+1] == '%') {
i++
}
} else if path[i] == '<' || path[i] == '>' {
if i < len(path)-1 && path[i+1] == '=' {
i++
}
} else if path[i] == '=' {
if i < len(path)-1 && path[i+1] == '=' {
s++
i++
}
}
i++
r.query.op = path[s:i]
// whitespace // whitespace
for ; i < len(path); i++ { for ; i < len(path); i++ {
if path[i] > ' ' { if path[i] > ' ' {
break break
} }
} }
s = i s := i
for ; i < len(path); i++ { for ; i < len(path); i++ {
if path[i] == '"' { if path[i] <= ' ' ||
i++ path[i] == '!' ||
s2 := i path[i] == '=' ||
for ; i < len(path); i++ { path[i] == '<' ||
if path[i] > '\\' { path[i] == '>' ||
continue path[i] == '%' ||
} path[i] == end {
if path[i] == '"' {
// look for an escaped slash
if path[i-1] == '\\' {
n := 0
for j := i - 2; j > s2-1; j-- {
if path[j] != '\\' {
break
}
n++
}
if n%2 == 0 {
continue
}
}
break
}
}
} else if path[i] == end {
if i+1 < len(path) && path[i+1] == '#' {
r.query.all = true
}
break break
} }
} }
if i > len(path) { r.query.path = path[s:i]
i = len(path) // whitespace
for ; i < len(path); i++ {
if path[i] > ' ' {
break
}
} }
v := path[s:i] if i < len(path) {
for len(v) > 0 && v[len(v)-1] <= ' ' { s = i
v = v[:len(v)-1] if path[i] == '!' {
if i < len(path)-1 && (path[i+1] == '=' ||
path[i+1] == '%') {
i++
}
} else if path[i] == '<' || path[i] == '>' {
if i < len(path)-1 && path[i+1] == '=' {
i++
}
} else if path[i] == '=' {
if i < len(path)-1 && path[i+1] == '=' {
s++
i++
}
}
i++
r.query.op = path[s:i]
// whitespace
for ; i < len(path); i++ {
if path[i] > ' ' {
break
}
}
s = i
for ; i < len(path); i++ {
if path[i] == '"' {
i++
s2 := i
for ; i < len(path); i++ {
if path[i] > '\\' {
continue
}
if path[i] == '"' {
// look for an escaped slash
if path[i-1] == '\\' {
n := 0
for j := i - 2; j > s2-1; j-- {
if path[j] != '\\' {
break
}
n++
}
if n%2 == 0 {
continue
}
}
break
}
}
} else if path[i] == end {
if i+1 < len(path) && path[i+1] == '#' {
r.query.all = true
}
break
}
}
if i > len(path) {
i = len(path)
}
v := path[s:i]
for len(v) > 0 && v[len(v)-1] <= ' ' {
v = v[:len(v)-1]
}
r.query.value = v
} }
r.query.value = v
} }
} }
} }
@ -847,6 +862,115 @@ func parseArrayPath(path string) (r arrayPathResult) {
return return
} }
// splitQuery takes a query and splits it into three parts:
// path, op, middle, and right.
// So for this query:
// #(first_name=="Murphy").last
// Becomes
// first_name # path
// =="Murphy" # middle
// .last # right
// Or,
// #(service_roles.#(=="one")).cap
// Becomes
// service_roles.#(=="one") # path
// # middle
// .cap # right
func parseQuery(query string) (
path, op, value, remain string, i int, ok bool,
) {
if len(query) < 2 || query[0] != '#' ||
(query[1] != '(' && query[1] != '[') {
return "", "", "", "", i, false
}
i = 2
j := 0 // start of value part
depth := 1
for ; i < len(query); i++ {
if depth == 1 && j == 0 {
switch query[i] {
case '!', '=', '<', '>', '%':
// start of the value part
j = i
continue
}
}
if query[i] == '\\' {
i++
} else if query[i] == '[' || query[i] == '(' {
depth++
} else if query[i] == ']' || query[i] == ')' {
depth--
if depth == 0 {
break
}
} else if query[i] == '"' {
// inside selector string, balance quotes
i++
for ; i < len(query); i++ {
if query[i] == '\\' {
i++
} else if query[i] == '"' {
break
}
}
}
}
if depth > 0 {
return "", "", "", "", i, false
}
if j > 0 {
path = trim(query[2:j])
value = trim(query[j:i])
remain = query[i+1:]
// parse the compare op from the value
var opsz int
switch {
case len(value) == 1:
opsz = 1
case value[0] == '!' && value[1] == '=':
opsz = 2
case value[0] == '!' && value[1] == '%':
opsz = 2
case value[0] == '<' && value[1] == '=':
opsz = 2
case value[0] == '>' && value[1] == '=':
opsz = 2
case value[0] == '=' && value[1] == '=':
value = value[1:]
opsz = 1
case value[0] == '<':
opsz = 1
case value[0] == '>':
opsz = 1
case value[0] == '=':
opsz = 1
case value[0] == '%':
opsz = 1
}
op = value[:opsz]
value = trim(value[opsz:])
} else {
path = trim(query[2:i])
remain = query[i+1:]
}
return path, op, value, remain, i + 1, true
}
func trim(s string) string {
left:
if len(s) > 0 && s[0] <= ' ' {
s = s[1:]
goto left
}
right:
if len(s) > 0 && s[len(s)-1] <= ' ' {
s = s[:len(s)-1]
goto right
}
return s
}
type objectPathResult struct { type objectPathResult struct {
part string part string
path string path string
@ -1135,6 +1259,16 @@ func queryMatches(rp *arrayPathResult, value Result) bool {
if len(rpv) > 2 && rpv[0] == '"' && rpv[len(rpv)-1] == '"' { if len(rpv) > 2 && rpv[0] == '"' && rpv[len(rpv)-1] == '"' {
rpv = rpv[1 : len(rpv)-1] rpv = rpv[1 : len(rpv)-1]
} }
if !value.Exists() {
return false
}
if rp.query.op == "" {
// the query is only looking for existence, such as:
// friends.#(name)
// which makes sure that the array "friends" has an element of
// "name" that exists
return true
}
switch value.Type { switch value.Type {
case String: case String:
switch rp.query.op { switch rp.query.op {

View File

@ -1915,3 +1915,72 @@ func TestSubSelectors(t *testing.T) {
func TestArrayCountRawOutput(t *testing.T) { func TestArrayCountRawOutput(t *testing.T) {
assert(t, Get(`[1,2,3,4]`, "#").Raw == "4") assert(t, Get(`[1,2,3,4]`, "#").Raw == "4")
} }
func TestParseQuery(t *testing.T) {
var path, op, value, remain string
var ok bool
path, op, value, remain, _, ok =
parseQuery(`#(service_roles.#(=="one").()==asdf).cap`)
assert(t, ok &&
path == `service_roles.#(=="one").()` &&
op == "=" &&
value == `asdf` &&
remain == `.cap`)
path, op, value, remain, _, ok = parseQuery(`#(first_name%"Murphy").last`)
assert(t, ok &&
path == `first_name` &&
op == `%` &&
value == `"Murphy"` &&
remain == `.last`)
path, op, value, remain, _, ok = parseQuery(`#( first_name !% "Murphy" ).last`)
assert(t, ok &&
path == `first_name` &&
op == `!%` &&
value == `"Murphy"` &&
remain == `.last`)
path, op, value, remain, _, ok = parseQuery(`#(service_roles.#(=="one"))`)
assert(t, ok &&
path == `service_roles.#(=="one")` &&
op == `` &&
value == `` &&
remain == ``)
path, op, value, remain, _, ok =
parseQuery(`#(a\("\"(".#(=="o\"(ne")%"ab\")").remain`)
assert(t, ok &&
path == `a\("\"(".#(=="o\"(ne")` &&
op == "%" &&
value == `"ab\")"` &&
remain == `.remain`)
}
func TestParentSubQuery(t *testing.T) {
var json = `{
"topology": {
"instances": [
{
"service_version": "1.2.3",
"service_locale": {"lang": "en"},
"service_roles": ["one", "two"]
},
{
"service_version": "1.2.4",
"service_locale": {"lang": "th"},
"service_roles": ["three", "four"]
},
{
"service_version": "1.2.2",
"service_locale": {"lang": "en"},
"service_roles": ["one"]
}
]
}
}`
res := Get(json, `topology.instances.#( service_roles.#(=="one"))#.service_version`)
// should return two instances
assert(t, res.String() == `["1.2.3","1.2.2"]`)
}