diff --git a/README.md b/README.md index 7710fe4..85ab090 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Examples ### Getting a value -JJ uses a special [path syntax](https://github.com/tidwall/gjson#path-syntax) for finding values. +JJ uses a [path syntax](https://github.com/tidwall/gjson#path-syntax) for finding values. Get a string: ```sh @@ -95,6 +95,26 @@ $ echo '{"friends":["Tom","Jane","Carol"]}' | jj friends.1 Jane ``` +## JSON Lines + +There's support for [JSON Lines](http://jsonlines.org/) using the `..` path prefix. +Which when specified, treats the multi-lined document as an array. + +For example: + +``` +{"name": "Gilbert", "age": 61} +{"name": "Alexa", "age": 34} +{"name": "May", "age": 57} +``` + +``` +..# >> 4 +..1 >> {"name": "Alexa", "age": 34} +..#.name >> ["Gilbert","Alexa","May"] +..#[name="May"].age >> 57 +``` + ### Setting a value The [path syntax](https://github.com/tidwall/sjson#path-syntax) for setting values has a couple of tiny differences than for getting values. diff --git a/build.sh b/build.sh index a31d709..3b7d0ec 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -VERSION="1.1.2" +VERSION="1.2.0" PROTECTED_MODE="no" export GO15VENDOREXPERIMENT=1 diff --git a/vendor/github.com/tidwall/gjson/README.md b/vendor/github.com/tidwall/gjson/README.md index 5dbfed3..d689949 100644 --- a/vendor/github.com/tidwall/gjson/README.md +++ b/vendor/github.com/tidwall/gjson/README.md @@ -10,10 +10,11 @@ -

get a json value quickly

+

get json values quickly

GJSON is a Go package that provides a [fast](#performance) and [simple](#get-a-value) way to get values from a json document. It has features such as [one line retrieval](#get-a-value), [dot notation paths](#path-syntax), [iteration](#iterate-through-an-object-or-array). +For a command-line tool that uses the GJSON syntax check out [JJ](https://github.com/tidwall/jj). Getting Started =============== @@ -95,6 +96,43 @@ friends.#[age>45]#.last >> ["Craig","Murphy"] friends.#[first%"D*"].last >> "Murphy" ``` +## JSON Lines + +There also support for [JSON Lines](http://jsonlines.org/) using the `..` prefix. +Which when specified, treats the multi-lined document as an array. + +For example: + +``` +{"name": "Gilbert", "age": 61} +{"name": "Alexa", "age": 34} +{"name": "May", "age": 57} +{"name": "Deloise", "age": 44} +``` + +``` +..# >> 4 +..1 >> {"name": "Alexa", "age": 34} +..3 >> {"name": "Deloise", "age": 44} +..#.name >> ["Gilbert","Alexa","May","Deloise"] +..#[name="May"].age >> 57 +``` + +The `ForEachLines` function will iterate through lines. + +```go +gjson.ForEachLine(json, func(line gjson.Result) bool{ + println(line.String()) + return true +}) + +// Outputs: +// {"name": "Gilbert", "age": 61} +// {"name": "Alexa", "age": 34} +// {"name": "May", "age": 57} +// {"name": "Deloise", "age": 44} +``` + ## Result Type GJSON supports the json types `string`, `number`, `bool`, and `null`. @@ -152,6 +190,15 @@ array >> []interface{} object >> map[string]interface{} ``` +### 64-bit integers + +The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits, allowing for large JSON integers. + +```go +result.Int() int64 // -9223372036854775808 to 9223372036854775807 +result.Uint() int64 // 0 to 18446744073709551615 +``` + ## Get nested array values Suppose you want all the last names from the following json: @@ -234,53 +281,6 @@ if gjson.Get(json, "name.last").Exists() { } ``` -## Unmarshalling - -There's a `gjson.Unmarshal` function which loads json data into a value. -It's a general replacement for `json.Unmarshal` and you can typically -see a 2-3x boost in performance without the need for external generators. - -This function works almost identically to `json.Unmarshal` except that -`gjson.Unmarshal` will automatically attempt to convert JSON values to any -Go type. For example, the JSON string "100" or the JSON number 100 can be -equally assigned to Go string, int, byte, uint64, etc. This rule applies to -all types. - - -```go -package main - -import ( - "fmt" - - "github.com/tidwall/gjson" -) - -type Animal struct { - Type string `json:"type"` - Sound string `json:"sound"` - Age int `json:"age"` -} - -var json = `{ - "type": "Dog", - "Sound": "Bark", - "Age": "11" -}` - -func main() { - var dog Animal - gjson.Unmarshal([]byte(json), &dog) - fmt.Printf("type: %s, sound: %s, age: %d\n", dog.Type, dog.Sound, dog.Age) -} -``` - -This will print: - -``` -type: Dog, sound: Bark, age: 11 -``` - ## Unmarshal to a map To unmarshal to a `map[string]interface{}`: @@ -318,7 +318,7 @@ This is a best-effort no allocation sub slice of the original json. This method ## Get multiple values at once -The `GetMany` function can be used to get multiple values at the same time, and is optimized to scan over a JSON payload once. +The `GetMany` function can be used to get multiple values at the same time. ```go results := gjson.GetMany(json, "name.first", "name.last", "age") @@ -338,7 +338,6 @@ and [json-iterator](https://github.com/json-iterator/go) BenchmarkGJSONGet-8 3000000 372 ns/op 0 B/op 0 allocs/op BenchmarkGJSONUnmarshalMap-8 900000 4154 ns/op 1920 B/op 26 allocs/op BenchmarkJSONUnmarshalMap-8 600000 9019 ns/op 3048 B/op 69 allocs/op -BenchmarkJSONUnmarshalStruct-8 600000 9268 ns/op 1832 B/op 69 allocs/op BenchmarkJSONDecoder-8 300000 14120 ns/op 4224 B/op 184 allocs/op BenchmarkFFJSONLexer-8 1500000 3111 ns/op 896 B/op 8 allocs/op BenchmarkEasyJSONLexer-8 3000000 887 ns/op 613 B/op 6 allocs/op @@ -346,17 +345,6 @@ BenchmarkJSONParserGet-8 3000000 499 ns/op 21 B/op BenchmarkJSONIterator-8 3000000 812 ns/op 544 B/op 9 allocs/op ``` -Benchmarks for the `GetMany` function: - -``` -BenchmarkGJSONGetMany4Paths-8 4000000 303 ns/op 112 B/op 0 allocs/op -BenchmarkGJSONGetMany8Paths-8 8000000 208 ns/op 56 B/op 0 allocs/op -BenchmarkGJSONGetMany16Paths-8 16000000 156 ns/op 56 B/op 0 allocs/op -BenchmarkGJSONGetMany32Paths-8 32000000 127 ns/op 64 B/op 0 allocs/op -BenchmarkGJSONGetMany64Paths-8 64000000 117 ns/op 64 B/op 0 allocs/op -BenchmarkGJSONGetMany128Paths-8 128000000 109 ns/op 64 B/op 0 allocs/op -``` - JSON document used: ```json @@ -395,21 +383,6 @@ widget.image.hOffset widget.text.onMouseUp ``` -For the `GetMany` benchmarks these paths are used: - -``` -widget.window.name -widget.image.hOffset -widget.text.onMouseUp -widget.window.title -widget.image.alignment -widget.text.style -widget.window.height -widget.image.src -widget.text.data -widget.text.size -``` - *These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.8 and can be be found [here](https://github.com/tidwall/gjson-benchmarks).* diff --git a/vendor/github.com/tidwall/gjson/gjson.go b/vendor/github.com/tidwall/gjson/gjson.go index f9fe002..9344761 100644 --- a/vendor/github.com/tidwall/gjson/gjson.go +++ b/vendor/github.com/tidwall/gjson/gjson.go @@ -177,8 +177,8 @@ func (t Result) Time() time.Time { // If the result represents a non-existent value, then an empty array will be returned. // If the result is not a JSON array, the return value will be an array containing one result. func (t Result) Array() []Result { - if !t.Exists() { - return nil + if t.Type == Null { + return []Result{} } if t.Type != JSON { return []Result{t} @@ -192,7 +192,7 @@ func (t Result) IsObject() bool { return t.Type == JSON && len(t.Raw) > 0 && t.Raw[0] == '{' } -// IsObject returns true if the result value is a JSON array. +// IsArray returns true if the result value is a JSON array. func (t Result) IsArray() bool { return t.Type == JSON && len(t.Raw) > 0 && t.Raw[0] == '[' } @@ -511,7 +511,7 @@ func tonum(json string) (raw string, num float64) { func tolit(json string) (raw string) { for i := 1; i < len(json); i++ { - if json[i] <= 'a' || json[i] >= 'z' { + if json[i] < 'a' || json[i] > 'z' { return json[:i] } } @@ -1077,7 +1077,7 @@ func queryMatches(rp *arrayPathResult, value Result) bool { case "=": return value.Num == rpvn case "!=": - return value.Num == rpvn + return value.Num != rpvn case "<": return value.Num < rpvn case "<=": @@ -1128,7 +1128,7 @@ func parseArray(c *parseContext, i int, path string) (int, bool) { partidx = int(n) } } - for i < len(c.json) { + for i < len(c.json)+1 { if !rp.arrch { pmatch = partidx == h hit = pmatch && !rp.more @@ -1137,8 +1137,16 @@ func parseArray(c *parseContext, i int, path string) (int, bool) { if rp.alogok { alog = append(alog, i) } - for ; i < len(c.json); i++ { - switch c.json[i] { + for ; ; i++ { + var ch byte + if i > len(c.json) { + break + } else if i == len(c.json) { + ch = ']' + } else { + ch = c.json[i] + } + switch ch { default: continue case '"': @@ -1252,14 +1260,18 @@ func parseArray(c *parseContext, i int, path string) (int, bool) { if rp.alogok { var jsons = make([]byte, 0, 64) jsons = append(jsons, '[') + for j, k := 0, 0; j < len(alog); j++ { - res := Get(c.json[alog[j]:], rp.alogkey) - if res.Exists() { - if k > 0 { - jsons = append(jsons, ',') + _, res, ok := parseAny(c.json, alog[j], true) + if ok { + res := res.Get(rp.alogkey) + if res.Exists() { + if k > 0 { + jsons = append(jsons, ',') + } + jsons = append(jsons, []byte(res.Raw)...) + k++ } - jsons = append(jsons, []byte(res.Raw)...) - k++ } } jsons = append(jsons, ']') @@ -1290,10 +1302,28 @@ func parseArray(c *parseContext, i int, path string) (int, bool) { return i, false } +// ForEachLine iterates through lines of JSON as specified by the JSON Lines +// format (http://jsonlines.org/). +// Each line is returned as a GJSON Result. +func ForEachLine(json string, iterator func(line Result) bool) { + var res Result + var i int + for { + i, res, _ = parseAny(json, i, true) + if !res.Exists() { + break + } + if !iterator(res) { + return + } + } +} + type parseContext struct { json string value Result calcd bool + lines bool } // Get searches json for the specified path. @@ -1329,16 +1359,21 @@ type parseContext struct { func Get(json, path string) Result { var i int var c = &parseContext{json: json} - for ; i < len(c.json); i++ { - if c.json[i] == '{' { - i++ - parseObject(c, i, path) - break - } - if c.json[i] == '[' { - i++ - parseArray(c, i, path) - break + if len(path) >= 2 && path[0] == '.' && path[1] == '.' { + c.lines = true + parseArray(c, 0, path[2:]) + } else { + for ; i < len(c.json); i++ { + if c.json[i] == '{' { + i++ + parseObject(c, i, path) + break + } + if c.json[i] == '[' { + i++ + parseArray(c, i, path) + break + } } } if len(c.value.Raw) > 0 && !c.calcd { @@ -1595,400 +1630,22 @@ var ( // used for testing testLastWasFallback bool ) -// areSimplePaths returns true if all the paths are simple enough -// to parse quickly for GetMany(). Allows alpha-numeric, dots, -// underscores, and the dollar sign. It does not allow non-alnum, -// escape characters, or keys which start with a numbers. -// For example: -// "name.last" == OK -// "user.id0" == OK -// "user.ID" == OK -// "user.first_name" == OK -// "user.firstName" == OK -// "user.0item" == BAD -// "user.#id" == BAD -// "user\.name" == BAD -func areSimplePaths(paths []string) bool { - for _, path := range paths { - var fi int // first key index, for keys with numeric prefix - for i := 0; i < len(path); i++ { - if path[i] >= 'a' && path[i] <= 'z' { - // a-z is likely to be the highest frequency charater. - continue - } - if path[i] == '.' { - fi = i + 1 - continue - } - if path[i] >= 'A' && path[i] <= 'Z' { - continue - } - if path[i] == '_' || path[i] == '$' { - continue - } - if i > fi && path[i] >= '0' && path[i] <= '9' { - continue - } - return false - } - } - return true -} - // GetMany searches json for the multiple paths. // The return value is a Result array where the number of items // will be equal to the number of input paths. -func GetMany(json string, paths ...string) []Result { - if len(paths) < 4 { - if testWatchForFallback { - testLastWasFallback = false - } - switch len(paths) { - case 0: - // return nil when no paths are specified. - return nil - case 1: - return []Result{Get(json, paths[0])} - case 2: - return []Result{Get(json, paths[0]), Get(json, paths[1])} - case 3: - return []Result{Get(json, paths[0]), Get(json, paths[1]), Get(json, paths[2])} - } - } - var results []Result - var ok bool - var i int - if len(paths) > 512 { - // we can only support up to 512 paths. Is that too many? - goto fallback - } - if !areSimplePaths(paths) { - // If there is even one path that is not considered "simple" then - // we need to use the fallback method. - goto fallback +func GetMany(json string, path ...string) []Result { + res := make([]Result, len(path)) + for i, path := range path { + res[i] = Get(json, path) } - // locate the object token. - for ; i < len(json); i++ { - if json[i] == '{' { - i++ - break - } - if json[i] <= ' ' { - continue - } - goto fallback - } - // use the call function table. - if len(paths) <= 8 { - results, ok = getMany8(json, i, paths) - } else if len(paths) <= 16 { - results, ok = getMany16(json, i, paths) - } else if len(paths) <= 32 { - results, ok = getMany32(json, i, paths) - } else if len(paths) <= 64 { - results, ok = getMany64(json, i, paths) - } else if len(paths) <= 128 { - results, ok = getMany128(json, i, paths) - } else if len(paths) <= 256 { - results, ok = getMany256(json, i, paths) - } else if len(paths) <= 512 { - results, ok = getMany512(json, i, paths) - } - if !ok { - // there was some fault while parsing. we should try the - // fallback method. This could result in performance - // degregation in some cases. - goto fallback - } - if testWatchForFallback { - testLastWasFallback = false - } - return results -fallback: - results = results[:0] - for i := 0; i < len(paths); i++ { - results = append(results, Get(json, paths[i])) - } - if testWatchForFallback { - testLastWasFallback = true - } - return results -} - -// GetManyBytes searches json for the specified path. -// If working with bytes, this method preferred over -// GetMany(string(data), paths...) -func GetManyBytes(json []byte, paths ...string) []Result { - if json == nil { - return GetMany("", paths...) - } - results := GetMany(*(*string)(unsafe.Pointer(&json)), paths...) - for i := range results { - results[i] = fromBytesGet(results[i]) - } - return results -} - -// parseGetMany parses a json object for keys that match against the callers -// paths. It's a best-effort attempt and quickly locating and assigning the -// values to the []Result array. If there are failures such as bad json, or -// invalid input paths, or too much recursion, the function will exit with a -// return value of 'false'. -func parseGetMany( - json string, i int, - level uint, kplen int, - paths []string, completed []bool, matches []uint64, results []Result, -) (int, bool) { - if level > 62 { - // The recursion level is limited because the matches []uint64 - // array cannot handle more the 64-bits. - return i, false - } - // At this point the last character read was a '{'. - // Read all object keys and try to match against the paths. - var key string - var val string - var vesc, ok bool -next_key: - for ; i < len(json); i++ { - if json[i] == '"' { - // read the key - i, val, vesc, ok = parseString(json, i+1) - if !ok { - return i, false - } - if vesc { - // the value is escaped - key = unescape(val[1 : len(val)-1]) - } else { - // just a plain old ascii key - key = val[1 : len(val)-1] - } - var hasMatch bool - var parsedVal bool - var valOrgIndex int - var valPathIndex int - for j := 0; j < len(key); j++ { - if key[j] == '.' { - // we need to look for keys with dot and ignore them. - if i, _, ok = parseAny(json, i, false); !ok { - return i, false - } - continue next_key - } - } - var usedPaths int - // loop through paths and look for matches - for j := 0; j < len(paths); j++ { - if completed[j] { - usedPaths++ - // ignore completed paths - continue - } - if level > 0 && (matches[j]>>(level-1))&1 == 0 { - // ignore unmatched paths - usedPaths++ - continue - } - - // try to match the key to the path - // this is spaghetti code but the idea is to minimize - // calls and variable assignments when comparing the - // key to paths - if len(paths[j])-kplen >= len(key) { - i, k := kplen, 0 - for ; k < len(key); k, i = k+1, i+1 { - if key[k] != paths[j][i] { - // no match - goto nomatch - } - } - if i < len(paths[j]) { - if paths[j][i] == '.' { - // matched, but there are still more keys in path - goto match_not_atend - } - } - if len(paths[j]) <= len(key) || kplen != 0 { - // matched and at the end of the path - goto match_atend - } - } - // no match, jump to the nomatch label - goto nomatch - match_atend: - // found a match - // at the end of the path. we must take the value. - usedPaths++ - if !parsedVal { - // the value has not been parsed yet. let's do so. - valOrgIndex = i // keep track of the current position. - i, results[j], ok = parseAny(json, i, true) - if !ok { - return i, false - } - parsedVal = true - valPathIndex = j - } else { - results[j] = results[valPathIndex] - } - // mark as complete - completed[j] = true - // jump over the match_not_atend label - goto nomatch - match_not_atend: - // found a match - // still in the middle of the path. - usedPaths++ - // mark the path as matched - matches[j] |= 1 << level - if !hasMatch { - hasMatch = true - } - nomatch: // noop label - } - - if !parsedVal { - if hasMatch { - // we found a match and the value has not been parsed yet. - // let's find out if the next value type is an object. - for ; i < len(json); i++ { - if json[i] <= ' ' || json[i] == ':' { - continue - } - break - } - if i < len(json) { - if json[i] == '{' { - // it's an object. let's go deeper - i, ok = parseGetMany(json, i+1, level+1, kplen+len(key)+1, paths, completed, matches, results) - if !ok { - return i, false - } - } else { - // not an object. just parse and ignore. - if i, _, ok = parseAny(json, i, false); !ok { - return i, false - } - } - } - } else { - // Since there was no matches we can just parse the value and - // ignore the result. - if i, _, ok = parseAny(json, i, false); !ok { - return i, false - } - } - } else if hasMatch && len(results[valPathIndex].Raw) > 0 && results[valPathIndex].Raw[0] == '{' { - // The value was already parsed and the value type is an object. - // Rewind the json index and let's parse deeper. - i = valOrgIndex - for ; i < len(json); i++ { - if json[i] == '{' { - break - } - } - i, ok = parseGetMany(json, i+1, level+1, kplen+len(key)+1, paths, completed, matches, results) - if !ok { - return i, false - } - } - if usedPaths == len(paths) { - // all paths have been used, either completed or matched. - // we should stop parsing this object to save CPU cycles. - if level > 0 && i < len(json) { - i, _ = parseSquash(json, i) - } - return i, true - } - } else if json[i] == '}' { - // reached the end of the object. end it here. - return i + 1, true - } - } - return i, true + return res } -// Call table for GetMany. Using an isolated function allows for allocating -// arrays with know capacities on the stack, as opposed to dynamically -// allocating on the heap. This can provide a tremendous performance boost -// by avoiding the GC. -func getMany8(json string, i int, paths []string) ([]Result, bool) { - const max = 8 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok -} -func getMany16(json string, i int, paths []string) ([]Result, bool) { - const max = 16 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok -} -func getMany32(json string, i int, paths []string) ([]Result, bool) { - const max = 32 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok -} -func getMany64(json string, i int, paths []string) ([]Result, bool) { - const max = 64 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok -} -func getMany128(json string, i int, paths []string) ([]Result, bool) { - const max = 128 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok -} -func getMany256(json string, i int, paths []string) ([]Result, bool) { - const max = 256 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok -} -func getMany512(json string, i int, paths []string) ([]Result, bool) { - const max = 512 - var completed = make([]bool, 0, max) - var matches = make([]uint64, 0, max) - var results = make([]Result, 0, max) - completed = completed[0:len(paths):max] - matches = matches[0:len(paths):max] - results = results[0:len(paths):max] - _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) - return results, ok +// GetManyBytes searches json for the multiple paths. +// The return value is a Result array where the number of items +// will be equal to the number of input paths. +func GetManyBytes(json []byte, path ...string) []Result { + return GetMany(string(json), path...) } var fieldsmu sync.RWMutex @@ -2094,6 +1751,8 @@ var validate uintptr = 1 // UnmarshalValidationEnabled provides the option to disable JSON validation // during the Unmarshal routine. Validation is enabled by default. +// +// Deprecated: Use encoder/json.Unmarshal instead func UnmarshalValidationEnabled(enabled bool) { if enabled { atomic.StoreUintptr(&validate, 1) @@ -2108,6 +1767,8 @@ func UnmarshalValidationEnabled(enabled bool) { // gjson.Unmarshal will automatically attempt to convert JSON values to any Go // type. For example, the JSON string "100" or the JSON number 100 can be equally // assigned to Go string, int, byte, uint64, etc. This rule applies to all types. +// +// Deprecated: Use encoder/json.Unmarshal instead func Unmarshal(data []byte, v interface{}) error { if atomic.LoadUintptr(&validate) == 1 { _, ok := validpayload(data, 0) diff --git a/vendor/github.com/tidwall/gjson/gjson_test.go b/vendor/github.com/tidwall/gjson/gjson_test.go index 57f0425..c01dc2f 100644 --- a/vendor/github.com/tidwall/gjson/gjson_test.go +++ b/vendor/github.com/tidwall/gjson/gjson_test.go @@ -7,6 +7,7 @@ import ( "fmt" "math/rand" "reflect" + "strconv" "strings" "testing" "time" @@ -1071,3 +1072,283 @@ func TestValidRandom(t *testing.T) { validpayload(b[:n], 0) } } + +func TestGetMany47(t *testing.T) { + json := `{"bar": {"id": 99, "mybar": "my mybar" }, "foo": {"myfoo": [605]}}` + paths := []string{"foo.myfoo", "bar.id", "bar.mybar", "bar.mybarx"} + expected := []string{"[605]", "99", "my mybar", ""} + results := GetMany(json, paths...) + if len(expected) != len(results) { + t.Fatalf("expected %v, got %v", len(expected), len(results)) + } + for i, path := range paths { + if results[i].String() != expected[i] { + t.Fatalf("expected '%v', got '%v' for path '%v'", expected[i], results[i].String(), path) + } + } +} + +func TestGetMany48(t *testing.T) { + json := `{"bar": {"id": 99, "xyz": "my xyz"}, "foo": {"myfoo": [605]}}` + paths := []string{"foo.myfoo", "bar.id", "bar.xyz", "bar.abc"} + expected := []string{"[605]", "99", "my xyz", ""} + results := GetMany(json, paths...) + if len(expected) != len(results) { + t.Fatalf("expected %v, got %v", len(expected), len(results)) + } + for i, path := range paths { + if results[i].String() != expected[i] { + t.Fatalf("expected '%v', got '%v' for path '%v'", expected[i], results[i].String(), path) + } + } +} + +func TestResultRawForLiteral(t *testing.T) { + for _, lit := range []string{"null", "true", "false"} { + result := Parse(lit) + if result.Raw != lit { + t.Fatalf("expected '%v', got '%v'", lit, result.Raw) + } + } +} + +func TestNullArray(t *testing.T) { + n := len(Get(`{"data":null}`, "data").Array()) + if n != 0 { + t.Fatalf("expected '%v', got '%v'", 0, n) + } + n = len(Get(`{}`, "data").Array()) + if n != 0 { + t.Fatalf("expected '%v', got '%v'", 0, n) + } + n = len(Get(`{"data":[]}`, "data").Array()) + if n != 0 { + t.Fatalf("expected '%v', got '%v'", 0, n) + } + n = len(Get(`{"data":[null]}`, "data").Array()) + if n != 1 { + t.Fatalf("expected '%v', got '%v'", 1, n) + } +} + +func TestRandomGetMany(t *testing.T) { + start := time.Now() + for time.Since(start) < time.Second*3 { + testRandomGetMany(t) + } +} +func testRandomGetMany(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + json, keys := randomJSON() + for _, key := range keys { + r := Get(json, key) + if !r.Exists() { + t.Fatal("should exist") + } + } + rkeysi := rand.Perm(len(keys)) + rkeysn := 1 + rand.Int()%32 + if len(rkeysi) > rkeysn { + rkeysi = rkeysi[:rkeysn] + } + var rkeys []string + for i := 0; i < len(rkeysi); i++ { + rkeys = append(rkeys, keys[rkeysi[i]]) + } + mres1 := GetMany(json, rkeys...) + var mres2 []Result + for _, rkey := range rkeys { + mres2 = append(mres2, Get(json, rkey)) + } + if len(mres1) != len(mres2) { + t.Fatalf("expected %d, got %d", len(mres2), len(mres1)) + } + for i := 0; i < len(mres1); i++ { + mres1[i].Index = 0 + mres2[i].Index = 0 + v1 := fmt.Sprintf("%#v", mres1[i]) + v2 := fmt.Sprintf("%#v", mres2[i]) + if v1 != v2 { + t.Fatalf("\nexpected %s\n"+ + " got %s", v2, v1) + } + } +} + +func TestIssue54(t *testing.T) { + var r []Result + json := `{"MarketName":null,"Nounce":6115}` + r = GetMany(json, "Nounce", "Buys", "Sells", "Fills") + if strings.Replace(fmt.Sprintf("%v", r), " ", "", -1) != "[6115]" { + t.Fatalf("expected '%v', got '%v'", "[6115]", strings.Replace(fmt.Sprintf("%v", r), " ", "", -1)) + } + r = GetMany(json, "Nounce", "Buys", "Sells") + if strings.Replace(fmt.Sprintf("%v", r), " ", "", -1) != "[6115]" { + t.Fatalf("expected '%v', got '%v'", "[6115]", strings.Replace(fmt.Sprintf("%v", r), " ", "", -1)) + } + r = GetMany(json, "Nounce") + if strings.Replace(fmt.Sprintf("%v", r), " ", "", -1) != "[6115]" { + t.Fatalf("expected '%v', got '%v'", "[6115]", strings.Replace(fmt.Sprintf("%v", r), " ", "", -1)) + } +} + +func randomString() string { + var key string + N := 1 + rand.Int()%16 + for i := 0; i < N; i++ { + r := rand.Int() % 62 + if r < 10 { + key += string(byte('0' + r)) + } else if r-10 < 26 { + key += string(byte('a' + r - 10)) + } else { + key += string(byte('A' + r - 10 - 26)) + } + } + return `"` + key + `"` +} +func randomBool() string { + switch rand.Int() % 2 { + default: + return "false" + case 1: + return "true" + } +} +func randomNumber() string { + return strconv.FormatInt(int64(rand.Int()%1000000), 10) +} + +func randomObjectOrArray(keys []string, prefix string, array bool, depth int) (string, []string) { + N := 5 + rand.Int()%5 + var json string + if array { + json = "[" + } else { + json = "{" + } + for i := 0; i < N; i++ { + if i > 0 { + json += "," + } + var pkey string + if array { + pkey = prefix + "." + strconv.FormatInt(int64(i), 10) + } else { + key := randomString() + pkey = prefix + "." + key[1:len(key)-1] + json += key + `:` + } + keys = append(keys, pkey[1:]) + var kind int + if depth == 5 { + kind = rand.Int() % 4 + } else { + kind = rand.Int() % 6 + } + switch kind { + case 0: + json += randomString() + case 1: + json += randomBool() + case 2: + json += "null" + case 3: + json += randomNumber() + case 4: + var njson string + njson, keys = randomObjectOrArray(keys, pkey, true, depth+1) + json += njson + case 5: + var njson string + njson, keys = randomObjectOrArray(keys, pkey, false, depth+1) + json += njson + } + + } + if array { + json += "]" + } else { + json += "}" + } + return json, keys +} + +func randomJSON() (json string, keys []string) { + return randomObjectOrArray(nil, "", false, 0) +} + +func TestIssue55(t *testing.T) { + json := `{"one": {"two": 2, "three": 3}, "four": 4, "five": 5}` + results := GetMany(json, "four", "five", "one.two", "one.six") + expected := []string{"4", "5", "2", ""} + for i, r := range results { + if r.String() != expected[i] { + t.Fatalf("expected %v, got %v", expected[i], r.String()) + } + } +} +func TestIssue58(t *testing.T) { + json := `{"data":[{"uid": 1},{"uid": 2}]}` + res := Get(json, `data.#[uid!=1]`).Raw + if res != `{"uid": 2}` { + t.Fatalf("expected '%v', got '%v'", `{"uid": 1}`, res) + } +} + +func TestObjectGrouping(t *testing.T) { + json := ` +[ + true, + {"name":"tom"}, + false, + {"name":"janet"}, + null +] +` + res := Get(json, "#.name") + if res.String() != `["tom","janet"]` { + t.Fatalf("expected '%v', got '%v'", `["tom","janet"]`, res.String()) + } +} + +func TestJSONLines(t *testing.T) { + json := ` +true +false +{"name":"tom"} +[1,2,3,4,5] +{"name":"janet"} +null +12930.1203 + ` + paths := []string{"..#", "..0", "..2.name", "..#.name", "..6", "..7"} + ress := []string{"7", "true", "tom", `["tom","janet"]`, "12930.1203", ""} + for i, path := range paths { + res := Get(json, path) + if res.String() != ress[i] { + t.Fatalf("expected '%v', got '%v'", ress[i], res.String()) + } + } + + json = ` +{"name": "Gilbert", "wins": [["straight", "7♣"], ["one pair", "10♥"]]} +{"name": "Alexa", "wins": [["two pair", "4♠"], ["two pair", "9♠"]]} +{"name": "May", "wins": []} +{"name": "Deloise", "wins": [["three of a kind", "5♣"]]} +` + + var i int + lines := strings.Split(strings.TrimSpace(json), "\n") + ForEachLine(json, func(line Result) bool { + if line.Raw != lines[i] { + t.Fatalf("expected '%v', got '%v'", lines[i], line.Raw) + } + i++ + return true + }) + if i != 4 { + t.Fatalf("expected '%v', got '%v'", 4, i) + } + +}