forked from mirror/gjson
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:
parent
90ca17622f
commit
1e964df7d9
300
gjson.go
300
gjson.go
|
@ -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 {
|
||||||
|
|
|
@ -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"]`)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue