get nested array values, fixes #4

To get all the friends last names use:

"friends.#.last"

{
  "friends": [
    {"first": "James", "last": "Murphy"},
    {"first": "Roger", "last": "Craig"}
  ]
}
This commit is contained in:
Josh Baker 2016-08-21 06:51:36 -07:00
parent 260ef19a2e
commit 0757a4d1e7
3 changed files with 117 additions and 11 deletions

View File

@ -51,14 +51,18 @@ Prichard
A path is a series of keys separated by a dot. A path is a series of keys separated by a dot.
A key may contain special wildcard characters '\*' and '?'. A key may contain special wildcard characters '\*' and '?'.
To access an array value use the index as the key. To access an array value use the index as the key.
To get the number of elements in an array use the '#' character. To get the number of elements in an array or to access a child path, use the '#' character.
The dot and wildcard characters can be escaped with '\'. The dot and wildcard characters can be escaped with '\'.
``` ```
{ {
"name": {"first": "Tom", "last": "Anderson"}, "name": {"first": "Tom", "last": "Anderson"},
"age":37, "age":37,
"children": ["Sara","Alex","Jack"], "children": ["Sara","Alex","Jack"],
"fav.movie": "Deer Hunter" "fav.movie": "Deer Hunter",
"friends": [
{"first": "James", "last": "Murphy"},
{"first": "Roger", "last": "Craig"}
]
} }
"name.last" >> "Anderson" "name.last" >> "Anderson"
"age" >> 37 "age" >> 37
@ -67,8 +71,12 @@ The dot and wildcard characters can be escaped with '\'.
"child*.2" >> "Jack" "child*.2" >> "Jack"
"c?ildren.0" >> "Sara" "c?ildren.0" >> "Sara"
"fav\.movie" >> "Deer Hunter" "fav\.movie" >> "Deer Hunter"
"friends.#.first" >> [ "James", "Roger" ]
``` ```
## Result Type ## Result Type
GJSON supports the json types `string`, `number`, `bool`, and `null`. GJSON supports the json types `string`, `number`, `bool`, and `null`.

View File

@ -19,6 +19,8 @@ const (
True True
// JSON is a raw block of JSON // JSON is a raw block of JSON
JSON JSON
// Multi is a subset of results
Multi
) )
// Result represents a json value that is returned from Get(). // Result represents a json value that is returned from Get().
@ -31,6 +33,8 @@ type Result struct {
Str string Str string
// Num is the json number // Num is the json number
Num float64 Num float64
// Multi is the subset of results
Multi []Result
} }
// String returns a string representation of the value. // String returns a string representation of the value.
@ -44,6 +48,15 @@ func (t Result) String() string {
return strconv.FormatFloat(t.Num, 'f', -1, 64) return strconv.FormatFloat(t.Num, 'f', -1, 64)
case String: case String:
return t.Str return t.Str
case Multi:
var str string
for i, res := range t.Multi {
if i > 0 {
str += ","
}
str += res.String()
}
return str
case JSON: case JSON:
return t.Raw return t.Raw
case True: case True:
@ -78,6 +91,12 @@ func (t Result) Value() interface{} {
return t.Num return t.Num
case String: case String:
return t.Str return t.Str
case Multi:
var res = make([]interface{}, len(t.Multi))
for i, v := range t.Multi {
res[i] = v.Value()
}
return res
case JSON: case JSON:
return t.Raw return t.Raw
case True: case True:
@ -105,13 +124,17 @@ type frame struct {
// A path is a series of keys seperated by a dot. // A path is a series of keys seperated by a dot.
// A key may contain special wildcard characters '*' and '?'. // A key may contain special wildcard characters '*' and '?'.
// To access an array value use the index as the key. // To access an array value use the index as the key.
// To get the number of elements in an array use the '#' character. // To get the number of elements in an array or to access a child path, use the '#' character.
// The dot and wildcard character can be escaped with '\'. // The dot and wildcard character can be escaped with '\'.
// //
// { // {
// "name": {"first": "Tom", "last": "Anderson"}, // "name": {"first": "Tom", "last": "Anderson"},
// "age":37, // "age":37,
// "children": ["Sara","Alex","Jack"] // "children": ["Sara","Alex","Jack"],
// "friends": [
// {"first": "James", "last": "Murphy"},
// {"first": "Roger", "last": "Craig"}
// ]
// } // }
// "name.last" >> "Anderson" // "name.last" >> "Anderson"
// "age" >> 37 // "age" >> 37
@ -119,11 +142,16 @@ type frame struct {
// "children.1" >> "Alex" // "children.1" >> "Alex"
// "child*.2" >> "Jack" // "child*.2" >> "Jack"
// "c?ildren.0" >> "Sara" // "c?ildren.0" >> "Sara"
// "friends.#.first" >> [ "James", "Roger" ]
// //
func Get(json string, path string) Result { func Get(json string, path string) Result {
var s int // starting index variable var s int // starting index variable
var wild bool // wildcard indicator var wild bool // wildcard indicator
var parts = make([]part, 0, 4) // parsed path parts var parts = make([]part, 0, 4) // parsed path parts
var arrch bool
var alogok bool
var alogkey string
var alog []int
if len(path) == 0 { if len(path) == 0 {
// do nothing when no path specified and return an empty result. // do nothing when no path specified and return an empty result.
@ -148,6 +176,13 @@ func Get(json string, path string) Result {
} else if path[i] == '*' || path[i] == '?' { } else if path[i] == '*' || path[i] == '?' {
// set the wild flag to indicate that the part is a wildcard. // set the wild flag to indicate that the part is a wildcard.
wild = true wild = true
} else if path[i] == '#' {
arrch = true
if s == i && i+1 < len(path) && path[i+1] == '.' {
alogok = true
alogkey = path[i+2:]
path = path[:i+1]
}
} else if path[i] == '\\' { } else if path[i] == '\\' {
// go into escape mode. this is a slower path that // go into escape mode. this is a slower path that
// strips off the escape character from the part. // strips off the escape character from the part.
@ -224,6 +259,10 @@ read_key:
// key for the 10th value. // key for the 10th value.
f.key = strconv.FormatInt(int64(f.count), 10) f.key = strconv.FormatInt(int64(f.count), 10)
f.count++ f.count++
if alogok && depth == len(parts) {
alog = append(alog, i)
}
//f.alog = append(f.alog, i)
} else { } else {
// for objects we must parse the next string. this string will // for objects we must parse the next string. this string will
// become the key that is compared against the path parts. // become the key that is compared against the path parts.
@ -501,8 +540,7 @@ proc_val:
} }
return value return value
} else { } else {
f.stype = vc f = frame{stype: vc}
f.count = 0
stack = append(stack, f) stack = append(stack, f)
depth++ depth++
goto read_key goto read_key
@ -539,8 +577,19 @@ proc_val:
for ; i < len(json); i++ { for ; i < len(json); i++ {
switch json[i] { switch json[i] {
case '}', ']': case '}', ']':
if parts[depth-1].key == "#" { if arrch && parts[depth-1].key == "#" {
return Result{Type: Number, Num: float64(f.count)} if alogok {
result := Result{Type: Multi}
for j := 0; j < len(alog); j++ {
res := Get(json[alog[j]:], alogkey)
if res.Exists() {
result.Multi = append(result.Multi, res)
}
}
return result
} else {
return Result{Type: Number, Num: float64(f.count)}
}
} }
// step the stack back // step the stack back
depth-- depth--
@ -621,7 +670,7 @@ func unescape(json string) string { //, error) {
// The caseSensitive paramater is used when the tokens are Strings. // The caseSensitive paramater is used when the tokens are Strings.
// The order when comparing two different type is: // The order when comparing two different type is:
// //
// Null < False < Number < String < True < JSON // Null < False < Number < String < True < JSON < Multi
// //
func (t Result) Less(token Result, caseSensitive bool) bool { func (t Result) Less(token Result, caseSensitive bool) bool {
if t.Type < token.Type { if t.Type < token.Type {
@ -638,6 +687,16 @@ func (t Result) Less(token Result, caseSensitive bool) bool {
return t.Str < token.Str return t.Str < token.Str
} }
return stringLessInsensitive(t.Str, token.Str) return stringLessInsensitive(t.Str, token.Str)
case Multi:
for i := 0; i < len(t.Multi) && i < len(token.Multi); i++ {
if t.Multi[i].Less(token.Multi[i], caseSensitive) {
return true
}
if token.Multi[i].Less(t.Multi[i], caseSensitive) {
return false
}
}
return len(t.Multi) < len(token.Multi)
case Number: case Number:
return t.Num < token.Num return t.Num < token.Num
} }

View File

@ -15,7 +15,7 @@ import (
fflib "github.com/pquerna/ffjson/fflib/v1" fflib "github.com/pquerna/ffjson/fflib/v1"
) )
// TestRandomData is a fuzzing test that throughs random data at the Parse // TestRandomData is a fuzzing test that throws random data at the Parse
// function looking for panics. // function looking for panics.
func TestRandomData(t *testing.T) { func TestRandomData(t *testing.T) {
var lstr string var lstr string
@ -99,10 +99,49 @@ var basicJSON = `{"age":100, "name":{"here":"B\\\"R"},
"happy":true,"immortal":false, "happy":true,"immortal":false,
"items":[1,2,3,{"tags":[1,2,3],"points":[[1,2],[3,4]]},4,5,6,7], "items":[1,2,3,{"tags":[1,2,3],"points":[[1,2],[3,4]]},4,5,6,7],
"arr":["1",2,"3",{"hello":"world"},"4",5], "arr":["1",2,"3",{"hello":"world"},"4",5],
"vals":[1,2,3,{"sadf":sdf"asdf"}],"name":{"first":"tom","last":null}}` "vals":[1,2,3,{"sadf":sdf"asdf"}],"name":{"first":"tom","last":null},
"loggy":{
"programmers": [
{
"firstName": "Brett",
"lastName": "McLaughlin",
"email": "aaaa"
},
{
"firstName": "Jason",
"lastName": "Hunter",
"email": "bbbb"
},
{
"firstName": "Elliotte",
"lastName": "Harold",
"email": "cccc"
}
]
}
}`
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
var token Result var token Result
mtok := Get(basicJSON, "loggy.programmers.#.firstName")
if mtok.Type != Multi {
t.Fatal("expected %v, got %v", Multi, mtok.Type)
}
if len(mtok.Multi) != 3 {
t.Fatalf("expected 3, got %v", len(mtok.Multi))
}
for i, ex := range []string{"Brett", "Jason", "Elliotte"} {
if mtok.Multi[i].String() != ex {
t.Fatalf("expected '%v', got '%v'", ex, mtok.Multi[i].String())
}
}
mtok = Get(basicJSON, "loggy.programmers.#.asd")
if mtok.Type != Multi {
t.Fatal("expected %v, got %v", Multi, mtok.Type)
}
if len(mtok.Multi) != 0 {
t.Fatalf("expected 0, got %v", len(mtok.Multi))
}
if Get(basicJSON, "items.3.tags.#").Num != 3 { if Get(basicJSON, "items.3.tags.#").Num != 3 {
t.Fatalf("expected 3, got %v", Get(basicJSON, "items.3.tags.#").Num) t.Fatalf("expected 3, got %v", Get(basicJSON, "items.3.tags.#").Num)
} }