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

138
gjson.go
View File

@ -736,14 +736,28 @@ func parseArrayPath(path string) (r arrayPathResult) {
r.alogkey = path[2:]
r.path = path[:1]
} else if path[1] == '[' || path[1] == '(' {
// query
r.query.on = true
if true {
qpath, op, value, _, fi, ok := parseQuery(path[i:])
if !ok {
// bad query, end now
break
}
r.query.path = qpath
r.query.op = op
r.query.value = value
i = fi - 1
if i+1 < len(path) && path[i+1] == '#' {
r.query.all = true
}
} else {
var end byte
if path[1] == '[' {
end = ']'
} else {
end = ')'
}
r.query.on = true
// query
i += 2
// whitespace
for ; i < len(path); i++ {
@ -839,6 +853,7 @@ func parseArrayPath(path string) (r arrayPathResult) {
}
}
}
}
continue
}
}
@ -847,6 +862,115 @@ func parseArrayPath(path string) (r arrayPathResult) {
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 {
part string
path string
@ -1135,6 +1259,16 @@ func queryMatches(rp *arrayPathResult, value Result) bool {
if len(rpv) > 2 && rpv[0] == '"' && rpv[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 {
case String:
switch rp.query.op {

View File

@ -1915,3 +1915,72 @@ func TestSubSelectors(t *testing.T) {
func TestArrayCountRawOutput(t *testing.T) {
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"]`)
}