diff --git a/gjson/LICENSE b/gjson/LICENSE new file mode 100644 index 000000000..58f5819a4 --- /dev/null +++ b/gjson/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/gjson/README.md b/gjson/README.md new file mode 100644 index 000000000..4108deb37 --- /dev/null +++ b/gjson/README.md @@ -0,0 +1,495 @@ +

+GJSON +
+Build Status +GoDoc +GJSON Playground +

+ + + +

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), and [parsing json lines](#json-lines). + +Also check out [SJSON](https://github.com/tidwall/sjson) for modifying json, and the [JJ](https://github.com/tidwall/jj) command line tool. + +Getting Started +=============== + +## Installing + +To start using GJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/gjson +``` + +This will retrieve the library. + +## Get a value +Get searches json for the specified path. A path is in dot syntax, such as "name.last" or "age". When the value is found it's returned immediately. + +```go +package main + +import "github.com/tidwall/gjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value := gjson.Get(json, "name.last") + println(value.String()) +} +``` + +This will print: + +``` +Prichard +``` +*There's also the [GetMany](#get-multiple-values-at-once) function to get multiple values at once, and [GetBytes](#working-with-bytes) for working with JSON byte slices.* + +## Path Syntax + +Below is a quick overview of the path syntax, for more complete information please +check out [GJSON Syntax](SYNTAX.md). + +A path is a series of keys separated by a dot. +A key may contain special wildcard characters '\*' and '?'. +To access an array value use the index as the key. +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 '\\'. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}, + {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]}, + {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children" >> ["Sara","Alex","Jack"] +"children.#" >> 3 +"children.1" >> "Alex" +"child*.2" >> "Jack" +"c?ildren.0" >> "Sara" +"fav\.movie" >> "Deer Hunter" +"friends.#.first" >> ["Dale","Roger","Jane"] +"friends.1.last" >> "Craig" +``` + +You can also query an array for the first match by using `#(...)`, or find all +matches with `#(...)#`. Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` +comparison operators and the simple pattern matching `%` (like) and `!%` +(not like) operators. + +``` +friends.#(last=="Murphy").first >> "Dale" +friends.#(last=="Murphy")#.first >> ["Dale","Jane"] +friends.#(age>45)#.last >> ["Craig","Murphy"] +friends.#(first%"D*").last >> "Murphy" +friends.#(first!%"D*").last >> "Craig" +friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"] +``` + +*Please note that prior to v1.3.0, queries used the `#[...]` brackets. This was +changed in v1.3.0 as to avoid confusion with the new +[multipath](SYNTAX.md#multipaths) syntax. For backwards compatibility, +`#[...]` will continue to work until the next major release.* + +## Result Type + +GJSON supports the json types `string`, `number`, `bool`, and `null`. +Arrays and Objects are returned as their raw json types. + +The `Result` type holds one of these: + +``` +bool, for JSON booleans +float64, for JSON numbers +string, for JSON string literals +nil, for JSON null +``` + +To directly access the value: + +```go +result.Type // can be String, Number, True, False, Null, or JSON +result.Str // holds the string +result.Num // holds the float64 number +result.Raw // holds the raw json +result.Index // index of raw value in original json, zero means index unknown +``` + +There are a variety of handy functions that work on a result: + +```go +result.Exists() bool +result.Value() interface{} +result.Int() int64 +result.Uint() uint64 +result.Float() float64 +result.String() string +result.Bool() bool +result.Time() time.Time +result.Array() []gjson.Result +result.Map() map[string]gjson.Result +result.Get(path string) Result +result.ForEach(iterator func(key, value Result) bool) +result.Less(token Result, caseSensitive bool) bool +``` + +The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types: + +The `result.Array()` function returns back an array of values. +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. + +```go +boolean >> bool +number >> float64 +string >> string +null >> nil +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 +``` + +## Modifiers and path chaining + +New in version 1.2 is support for modifier functions and path chaining. + +A modifier is a path component that performs custom processing on the +json. + +Multiple paths can be "chained" together using the pipe character. +This is useful for getting results from a modified query. + +For example, using the built-in `@reverse` modifier on the above json document, +we'll get `children` array and reverse the order: + +``` +"children|@reverse" >> ["Jack","Alex","Sara"] +"children|@reverse|0" >> "Jack" +``` + +There are currently the following built-in modifiers: + +- `@reverse`: Reverse an array or the members of an object. +- `@ugly`: Remove all whitespace from a json document. +- `@pretty`: Make the json document more human readable. +- `@this`: Returns the current element. It can be used to retrieve the root element. +- `@valid`: Ensure the json document is valid. +- `@flatten`: Flattens an array. +- `@join`: Joins multiple objects into a single object. + +### Modifier arguments + +A modifier may accept an optional argument. The argument can be a valid JSON +document or just characters. + +For example, the `@pretty` modifier takes a json object as its argument. + +``` +@pretty:{"sortKeys":true} +``` + +Which makes the json pretty and orders all of its keys. + +```json +{ + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"age": 44, "first": "Dale", "last": "Murphy"}, + {"age": 68, "first": "Roger", "last": "Craig"}, + {"age": 47, "first": "Jane", "last": "Murphy"} + ], + "name": {"first": "Tom", "last": "Anderson"} +} +``` + +*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`. +Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.* + +### Custom modifiers + +You can also add custom modifiers. + +For example, here we create a modifier that makes the entire json document upper +or lower case. + +```go +gjson.AddModifier("case", func(json, arg string) string { + if arg == "upper" { + return strings.ToUpper(json) + } + if arg == "lower" { + return strings.ToLower(json) + } + return json +}) +``` + +``` +"children|@case:upper" >> ["SARA","ALEX","JACK"] +"children|@case:lower|@reverse" >> ["jack","alex","sara"] +``` + +## JSON Lines + +There's support for [JSON Lines](http://jsonlines.org/) using the `..` prefix, which treats a multilined 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 JSON lines. + +```go +gjson.ForEachLine(json, func(line gjson.Result) bool{ + println(line.String()) + return true +}) +``` + +## Get nested array values + +Suppose you want all the last names from the following json: + +```json +{ + "programmers": [ + { + "firstName": "Janet", + "lastName": "McLaughlin", + }, { + "firstName": "Elliotte", + "lastName": "Hunter", + }, { + "firstName": "Jason", + "lastName": "Harold", + } + ] +} +``` + +You would use the path "programmers.#.lastName" like such: + +```go +result := gjson.Get(json, "programmers.#.lastName") +for _, name := range result.Array() { + println(name.String()) +} +``` + +You can also query an object inside an array: + +```go +name := gjson.Get(json, `programmers.#(lastName="Hunter").firstName`) +println(name.String()) // prints "Elliotte" +``` + +## Iterate through an object or array + +The `ForEach` function allows for quickly iterating through an object or array. +The key and value are passed to the iterator function for objects. +Only the value is passed for arrays. +Returning `false` from an iterator will stop iteration. + +```go +result := gjson.Get(json, "programmers") +result.ForEach(func(key, value gjson.Result) bool { + println(value.String()) + return true // keep iterating +}) +``` + +## Simple Parse and Get + +There's a `Parse(json)` function that will do a simple parse, and `result.Get(path)` that will search a result. + +For example, all of these will return the same result: + +```go +gjson.Parse(json).Get("name").Get("last") +gjson.Get(json, "name").Get("last") +gjson.Get(json, "name.last") +``` + +## Check for the existence of a value + +Sometimes you just want to know if a value exists. + +```go +value := gjson.Get(json, "name.last") +if !value.Exists() { + println("no last name") +} else { + println(value.String()) +} + +// Or as one step +if gjson.Get(json, "name.last").Exists() { + println("has a last name") +} +``` + +## Validate JSON + +The `Get*` and `Parse*` functions expects that the json is well-formed. Bad json will not panic, but it may return back unexpected results. + +If you are consuming JSON from an unpredictable source then you may want to validate prior to using GJSON. + +```go +if !gjson.Valid(json) { + return errors.New("invalid json") +} +value := gjson.Get(json, "name.last") +``` + +## Unmarshal to a map + +To unmarshal to a `map[string]interface{}`: + +```go +m, ok := gjson.Parse(json).Value().(map[string]interface{}) +if !ok { + // not a map +} +``` + +## Working with Bytes + +If your JSON is contained in a `[]byte` slice, there's the [GetBytes](https://godoc.org/github.com/tidwall/gjson#GetBytes) function. This is preferred over `Get(string(data), path)`. + +```go +var json []byte = ... +result := gjson.GetBytes(json, path) +``` + +If you are using the `gjson.GetBytes(json, path)` function and you want to avoid converting `result.Raw` to a `[]byte`, then you can use this pattern: + +```go +var json []byte = ... +result := gjson.GetBytes(json, path) +var raw []byte +if result.Index > 0 { + raw = json[result.Index:result.Index+len(result.Raw)] +} else { + raw = []byte(result.Raw) +} +``` + +This is a best-effort no allocation sub slice of the original json. This method utilizes the `result.Index` field, which is the position of the raw data in the original json. It's possible that the value of `result.Index` equals zero, in which case the `result.Raw` is converted to a `[]byte`. + +## Get multiple values at 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") +``` + +The return value is a `[]Result`, which will always contain exactly the same number of items as the input paths. + +## Performance + +Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +[jsonparser](https://github.com/buger/jsonparser), +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 +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 +BenchmarkJSONParserGet-8 3000000 499 ns/op 21 B/op 0 allocs/op +BenchmarkJSONIterator-8 3000000 812 ns/op 544 B/op 9 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated though one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +*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).* + + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +GJSON source code is available under the MIT [License](/LICENSE). diff --git a/gjson/SYNTAX.md b/gjson/SYNTAX.md new file mode 100644 index 000000000..9558019e7 --- /dev/null +++ b/gjson/SYNTAX.md @@ -0,0 +1,269 @@ +# GJSON Path Syntax + +A GJSON Path is a text string syntax that describes a search pattern for quickly retreiving values from a JSON payload. + +This document is designed to explain the structure of a GJSON Path through examples. + +- [Path structure](#path-structure) +- [Basic](#basic) +- [Wildcards](#wildcards) +- [Escape Character](#escape-character) +- [Arrays](#arrays) +- [Queries](#queries) +- [Dot vs Pipe](#dot-vs-pipe) +- [Modifiers](#modifiers) +- [Multipaths](#multipaths) + +The definitive implemenation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson). +Use the [GJSON Playground](https://gjson.dev) to experiment with the syntax online. + + +## Path structure + +A GJSON Path is intended to be easily expressed as a series of components seperated by a `.` character. + +Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, and `?`. + +## Example + +Given this JSON + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}, + {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]}, + {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]} + ] +} +``` + +The following GJSON Paths evaluate to the accompanying values. + +### Basic + +In many cases you'll just want to retreive values by object name or array index. + +```go +name.last "Anderson" +name.first "Tom" +age 37 +children ["Sara","Alex","Jack"] +children.0 "Sara" +children.1 "Alex" +friends.1 {"first": "Roger", "last": "Craig", "age": 68} +friends.1.first "Roger" +``` + +### Wildcards + +A key may contain the special wildcard characters `*` and `?`. +The `*` will match on any zero+ characters, and `?` matches on any one character. + +```go +child*.2 "Jack" +c?ildren.0 "Sara" +``` + +### Escape character + +Special purpose characters, such as `.`, `*`, and `?` can be escaped with `\`. + +```go +fav\.movie "Deer Hunter" +``` + +### Arrays + +The `#` character allows for digging into JSON Arrays. + +To get the length of an array you'll just use the `#` all by itself. + +```go +friends.# 3 +friends.#.age [44,68,47] +``` + +### Queries + +You can also query an array for the first match by using `#(...)`, or find all matches with `#(...)#`. +Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` comparison operators, +and the simple pattern matching `%` (like) and `!%` (not like) operators. + +```go +friends.#(last=="Murphy").first "Dale" +friends.#(last=="Murphy")#.first ["Dale","Jane"] +friends.#(age>45)#.last ["Craig","Murphy"] +friends.#(first%"D*").last "Murphy" +friends.#(first!%"D*").last "Craig" +``` + +To query for a non-object value in an array, you can forgo the string to the right of the operator. + +```go +children.#(!%"*a*") "Alex" +children.#(%"*a*")# ["Sara","Jack"] +``` + +Nested queries are allowed. + +```go +friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"] +``` + +*Please note that prior to v1.3.0, queries used the `#[...]` brackets. This was +changed in v1.3.0 as to avoid confusion with the new [multipath](#multipaths) +syntax. For backwards compatibility, `#[...]` will continue to work until the +next major release.* + +### Dot vs Pipe + +The `.` is standard separator, but it's also possible to use a `|`. +In most cases they both end up returning the same results. +The cases where`|` differs from `.` is when it's used after the `#` for [Arrays](#arrays) and [Queries](#queries). + +Here are some examples + +```go +friends.0.first "Dale" +friends|0.first "Dale" +friends.0|first "Dale" +friends|0|first "Dale" +friends|# 3 +friends.# 3 +friends.#(last="Murphy")# [{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}] +friends.#(last="Murphy")#.first ["Dale","Jane"] +friends.#(last="Murphy")#|first +friends.#(last="Murphy")#.0 [] +friends.#(last="Murphy")#|0 {"first": "Dale", "last": "Murphy", "age": 44} +friends.#(last="Murphy")#.# [] +friends.#(last="Murphy")#|# 2 +``` + +Let's break down a few of these. + +The path `friends.#(last="Murphy")#` all by itself results in + +```json +[{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}] +``` + +The `.first` suffix will process the `first` path on each array element *before* returning the results. Which becomes + +```json +["Dale","Jane"] +``` + +But the `|first` suffix actually processes the `first` path *after* the previous result. +Since the previous result is an array, not an object, it's not possible to process +because `first` does not exist. + +Yet, `|0` suffix returns + +```json +{"first": "Dale", "last": "Murphy", "age": 44} +``` + +Because `0` is the first index of the previous result. + +### Modifiers + +A modifier is a path component that performs custom processing on the JSON. + +For example, using the built-in `@reverse` modifier on the above JSON payload will reverse the `children` array: + +```go +children.@reverse ["Jack","Alex","Sara"] +children.@reverse.0 "Jack" +``` + +There are currently the following built-in modifiers: + +- `@reverse`: Reverse an array or the members of an object. +- `@ugly`: Remove all whitespace from JSON. +- `@pretty`: Make the JSON more human readable. +- `@this`: Returns the current element. It can be used to retrieve the root element. +- `@valid`: Ensure the json document is valid. +- `@flatten`: Flattens an array. +- `@join`: Joins multiple objects into a single object. + +#### Modifier arguments + +A modifier may accept an optional argument. The argument can be a valid JSON payload or just characters. + +For example, the `@pretty` modifier takes a json object as its argument. + +``` +@pretty:{"sortKeys":true} +``` + +Which makes the json pretty and orders all of its keys. + +```json +{ + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"age": 44, "first": "Dale", "last": "Murphy"}, + {"age": 68, "first": "Roger", "last": "Craig"}, + {"age": 47, "first": "Jane", "last": "Murphy"} + ], + "name": {"first": "Tom", "last": "Anderson"} +} +``` + +*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`. +Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.* + +#### Custom modifiers + +You can also add custom modifiers. + +For example, here we create a modifier which makes the entire JSON payload upper or lower case. + +```go +gjson.AddModifier("case", func(json, arg string) string { + if arg == "upper" { + return strings.ToUpper(json) + } + if arg == "lower" { + return strings.ToLower(json) + } + return json +}) +"children.@case:upper" ["SARA","ALEX","JACK"] +"children.@case:lower.@reverse" ["jack","alex","sara"] +``` + +### Multipaths + +Starting with v1.3.0, GJSON added the ability to join multiple paths together +to form new documents. Wrapping comma-separated paths between `{...}` or +`[...]` will result in a new array or object, respectively. + +For example, using the given multipath + +``` +{name.first,age,"the_murphys":friends.#(last="Murphy")#.first} +``` + +Here we selected the first name, age, and the first name for friends with the +last name "Murphy". + +You'll notice that an optional key can be provided, in this case +"the_murphys", to force assign a key to a value. Otherwise, the name of the +actual field will be used, in this case "first". If a name cannot be +determined, then "_" is used. + +This results in + +``` +{"first":"Tom","age":37,"the_murphys":["Dale","Jane"]} +``` + + diff --git a/gjson/gjson.go b/gjson/gjson.go new file mode 100644 index 000000000..647ed735e --- /dev/null +++ b/gjson/gjson.go @@ -0,0 +1,2824 @@ +// Package gjson provides searching for json strings. +package gjson + +import ( + "encoding/json" + "strconv" + "strings" + "time" + "unicode/utf16" + "unicode/utf8" + + "github.com/tidwall/match" + "github.com/tidwall/pretty" +) + +// Type is Result type +type Type int + +const ( + // Null is a null json value + Null Type = iota + // False is a json false boolean + False + // Number is json number + Number + // String is a json string + String + // True is a json true boolean + True + // JSON is a raw block of JSON + JSON +) + +// String returns a string representation of the type. +func (t Type) String() string { + switch t { + default: + return "" + case Null: + return "Null" + case False: + return "False" + case Number: + return "Number" + case String: + return "String" + case True: + return "True" + case JSON: + return "JSON" + } +} + +// Result represents a json value that is returned from Get(). +type Result struct { + // Type is the json type + Type Type + // Raw is the raw json + Raw string + // Str is the json string + Str string + // Num is the json number + Num float64 + // Index of raw value in original json, zero means index unknown + Index int +} + +// String returns a string representation of the value. +func (t Result) String() string { + switch t.Type { + default: + return "" + case False: + return "false" + case Number: + if len(t.Raw) == 0 { + // calculated result + return strconv.FormatFloat(t.Num, 'f', -1, 64) + } + var i int + if t.Raw[0] == '-' { + i++ + } + for ; i < len(t.Raw); i++ { + if t.Raw[i] < '0' || t.Raw[i] > '9' { + return strconv.FormatFloat(t.Num, 'f', -1, 64) + } + } + return t.Raw + case String: + return t.Str + case JSON: + return t.Raw + case True: + return "true" + } +} + +// Bool returns an boolean representation. +func (t Result) Bool() bool { + switch t.Type { + default: + return false + case True: + return true + case String: + return t.Str != "" && t.Str != "0" && t.Str != "false" + case Number: + return t.Num != 0 + } +} + +// Int returns an integer representation. +func (t Result) Int() int64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := parseInt(t.Str) + return n + case Number: + // try to directly convert the float64 to int64 + n, ok := floatToInt(t.Num) + if !ok { + // now try to parse the raw string + n, ok = parseInt(t.Raw) + if !ok { + // fallback to a standard conversion + return int64(t.Num) + } + } + return n + } +} + +// Uint returns an unsigned integer representation. +func (t Result) Uint() uint64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := parseUint(t.Str) + return n + case Number: + // try to directly convert the float64 to uint64 + n, ok := floatToUint(t.Num) + if !ok { + // now try to parse the raw string + n, ok = parseUint(t.Raw) + if !ok { + // fallback to a standard conversion + return uint64(t.Num) + } + } + return n + } +} + +// Float returns an float64 representation. +func (t Result) Float() float64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := strconv.ParseFloat(t.Str, 64) + return n + case Number: + return t.Num + } +} + +// Time returns a time.Time representation. +func (t Result) Time() time.Time { + res, _ := time.Parse(time.RFC3339, t.String()) + return res +} + +// Array returns back an array of values. +// 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.Type == Null { + return []Result{} + } + if t.Type != JSON { + return []Result{t} + } + r := t.arrayOrMap('[', false) + return r.a +} + +// IsObject returns true if the result value is a JSON object. +func (t Result) IsObject() bool { + return t.Type == JSON && len(t.Raw) > 0 && t.Raw[0] == '{' +} + +// 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] == '[' +} + +// ForEach iterates through values. +// If the result represents a non-existent value, then no values will be +// iterated. If the result is an Object, the iterator will pass the key and +// value of each item. If the result is an Array, the iterator will only pass +// the value of each item. If the result is not a JSON array or object, the +// iterator will pass back one value equal to the result. +func (t Result) ForEach(iterator func(key, value Result) bool) { + if !t.Exists() { + return + } + if t.Type != JSON { + iterator(Result{}, t) + return + } + json := t.Raw + var keys bool + var i int + var key, value Result + for ; i < len(json); i++ { + if json[i] == '{' { + i++ + key.Type = String + keys = true + break + } else if json[i] == '[' { + i++ + break + } + if json[i] > ' ' { + return + } + } + var str string + var vesc bool + var ok bool + for ; i < len(json); i++ { + if keys { + if json[i] != '"' { + continue + } + s := i + i, str, vesc, ok = parseString(json, i+1) + if !ok { + return + } + if vesc { + key.Str = unescape(str[1 : len(str)-1]) + } else { + key.Str = str[1 : len(str)-1] + } + key.Raw = str + key.Index = s + } + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ':' { + continue + } + break + } + s := i + i, value, ok = parseAny(json, i, true) + if !ok { + return + } + value.Index = s + if !iterator(key, value) { + return + } + } +} + +// Map returns back an map of values. The result should be a JSON array. +func (t Result) Map() map[string]Result { + if t.Type != JSON { + return map[string]Result{} + } + r := t.arrayOrMap('{', false) + return r.o +} + +// Get searches result for the specified path. +// The result should be a JSON array or object. +func (t Result) Get(path string) Result { + return Get(t.Raw, path) +} + +type arrayOrMapResult struct { + a []Result + ai []interface{} + o map[string]Result + oi map[string]interface{} + vc byte +} + +func (t Result) arrayOrMap(vc byte, valueize bool) (r arrayOrMapResult) { + var json = t.Raw + var i int + var value Result + var count int + var key Result + if vc == 0 { + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + r.vc = json[i] + i++ + break + } + if json[i] > ' ' { + goto end + } + } + } else { + for ; i < len(json); i++ { + if json[i] == vc { + i++ + break + } + if json[i] > ' ' { + goto end + } + } + r.vc = vc + } + if r.vc == '{' { + if valueize { + r.oi = make(map[string]interface{}) + } else { + r.o = make(map[string]Result) + } + } else { + if valueize { + r.ai = make([]interface{}, 0) + } else { + r.a = make([]Result, 0) + } + } + for ; i < len(json); i++ { + if json[i] <= ' ' { + continue + } + // get next value + if json[i] == ']' || json[i] == '}' { + break + } + switch json[i] { + default: + if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' { + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + value.Str = "" + } else { + continue + } + case '{', '[': + value.Type = JSON + value.Raw = squash(json[i:]) + value.Str, value.Num = "", 0 + case 'n': + value.Type = Null + value.Raw = tolit(json[i:]) + value.Str, value.Num = "", 0 + case 't': + value.Type = True + value.Raw = tolit(json[i:]) + value.Str, value.Num = "", 0 + case 'f': + value.Type = False + value.Raw = tolit(json[i:]) + value.Str, value.Num = "", 0 + case '"': + value.Type = String + value.Raw, value.Str = tostr(json[i:]) + value.Num = 0 + } + i += len(value.Raw) - 1 + + if r.vc == '{' { + if count%2 == 0 { + key = value + } else { + if valueize { + if _, ok := r.oi[key.Str]; !ok { + r.oi[key.Str] = value.Value() + } + } else { + if _, ok := r.o[key.Str]; !ok { + r.o[key.Str] = value + } + } + } + count++ + } else { + if valueize { + r.ai = append(r.ai, value.Value()) + } else { + r.a = append(r.a, value) + } + } + } +end: + return +} + +// Parse parses the json and returns a result. +// +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// If you are consuming JSON from an unpredictable source then you may want to +// use the Valid function first. +func Parse(json string) Result { + var value Result + for i := 0; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + value.Type = JSON + value.Raw = json[i:] // just take the entire raw + break + } + if json[i] <= ' ' { + continue + } + switch json[i] { + default: + if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' { + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + } else { + return Result{} + } + case 'n': + value.Type = Null + value.Raw = tolit(json[i:]) + case 't': + value.Type = True + value.Raw = tolit(json[i:]) + case 'f': + value.Type = False + value.Raw = tolit(json[i:]) + case '"': + value.Type = String + value.Raw, value.Str = tostr(json[i:]) + } + break + } + return value +} + +// ParseBytes parses the json and returns a result. +// If working with bytes, this method preferred over Parse(string(data)) +func ParseBytes(json []byte) Result { + return Parse(string(json)) +} + +func squash(json string) string { + // expects that the lead character is a '[' or '{' or '(' or '"' + // squash the value, ignoring all nested arrays and objects. + var i, depth int + if json[0] != '"' { + i, depth = 1, 1 + } + for ; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + if depth == 0 { + return json[:i+1] + } + case '{', '[', '(': + depth++ + case '}', ']', ')': + depth-- + if depth == 0 { + return json[:i+1] + } + } + } + } + return json +} + +func tonum(json string) (raw string, num float64) { + for i := 1; i < len(json); i++ { + // less than dash might have valid characters + if json[i] <= '-' { + if json[i] <= ' ' || json[i] == ',' { + // break on whitespace and comma + raw = json[:i] + num, _ = strconv.ParseFloat(raw, 64) + return + } + // could be a '+' or '-'. let's assume so. + continue + } + if json[i] < ']' { + // probably a valid number + continue + } + if json[i] == 'e' || json[i] == 'E' { + // allow for exponential numbers + continue + } + // likely a ']' or '}' + raw = json[:i] + num, _ = strconv.ParseFloat(raw, 64) + return + } + raw = json + num, _ = strconv.ParseFloat(raw, 64) + return +} + +func tolit(json string) (raw string) { + for i := 1; i < len(json); i++ { + if json[i] < 'a' || json[i] > 'z' { + return json[:i] + } + } + return json +} + +func tostr(json string) (raw string, str string) { + // expects that the lead character is a '"' + for i := 1; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + return json[:i+1], json[1:i] + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + var ret string + if i+1 < len(json) { + ret = json[:i+1] + } else { + ret = json[:i] + } + return ret, unescape(json[1:i]) + } + } + return json, json[1:] +} + +// Exists returns true if value exists. +// +// if gjson.Get(json, "name.last").Exists(){ +// println("value exists") +// } +func (t Result) Exists() bool { + return t.Type != Null || len(t.Raw) != 0 +} + +// Value returns one of these types: +// +// bool, for JSON booleans +// float64, for JSON numbers +// Number, for JSON numbers +// string, for JSON string literals +// nil, for JSON null +// map[string]interface{}, for JSON objects +// []interface{}, for JSON arrays +// +func (t Result) Value() interface{} { + if t.Type == String { + return t.Str + } + switch t.Type { + default: + return nil + case False: + return false + case Number: + return t.Num + case JSON: + r := t.arrayOrMap(0, true) + if r.vc == '{' { + return r.oi + } else if r.vc == '[' { + return r.ai + } + return nil + case True: + return true + } +} + +func parseString(json string, i int) (int, string, bool, bool) { + var s = i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + return i + 1, json[s-1 : i+1], false, true + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + return i + 1, json[s-1 : i+1], true, true + } + } + break + } + } + return i, json[s-1:], false, false +} + +func parseNumber(json string, i int) (int, string) { + var s = i + i++ + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ']' || + json[i] == '}' { + return i, json[s:i] + } + } + return i, json[s:] +} + +func parseLiteral(json string, i int) (int, string) { + var s = i + i++ + for ; i < len(json); i++ { + if json[i] < 'a' || json[i] > 'z' { + return i, json[s:i] + } + } + return i, json[s:] +} + +type arrayPathResult struct { + part string + path string + pipe string + piped bool + more bool + alogok bool + arrch bool + alogkey string + query struct { + on bool + path string + op string + value string + all bool + } +} + +func parseArrayPath(path string) (r arrayPathResult) { + for i := 0; i < len(path); i++ { + if path[i] == '|' { + r.part = path[:i] + r.pipe = path[i+1:] + r.piped = true + return + } + if path[i] == '.' { + r.part = path[:i] + r.path = path[i+1:] + r.more = true + return + } + if path[i] == '#' { + r.arrch = true + if i == 0 && len(path) > 1 { + if path[1] == '.' { + r.alogok = true + 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 = ')' + } + i += 2 + // whitespace + for ; i < len(path); i++ { + if path[i] > ' ' { + break + } + } + s := i + for ; i < len(path); i++ { + if path[i] <= ' ' || + path[i] == '!' || + path[i] == '=' || + path[i] == '<' || + path[i] == '>' || + path[i] == '%' || + path[i] == end { + break + } + } + r.query.path = path[s:i] + // whitespace + for ; i < len(path); i++ { + if path[i] > ' ' { + break + } + } + 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 + 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 + } + } + } + } + continue + } + } + r.part = path + r.path = "" + 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 + pipe string + piped bool + wild bool + more bool +} + +func parseObjectPath(path string) (r objectPathResult) { + for i := 0; i < len(path); i++ { + if path[i] == '|' { + r.part = path[:i] + r.pipe = path[i+1:] + r.piped = true + return + } + if path[i] == '.' { + // peek at the next byte and see if it's a '@', '[', or '{'. + r.part = path[:i] + if !DisableModifiers && + i < len(path)-1 && + (path[i+1] == '@' || + path[i+1] == '[' || path[i+1] == '{') { + r.pipe = path[i+1:] + r.piped = true + } else { + r.path = path[i+1:] + r.more = true + } + return + } + if path[i] == '*' || path[i] == '?' { + r.wild = true + continue + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + if i < len(path) { + epart = append(epart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + // peek at the next byte and see if it's a '@' modifier + if !DisableModifiers && + i < len(path)-1 && path[i+1] == '@' { + r.pipe = path[i+1:] + r.piped = true + } else { + r.path = path[i+1:] + r.more = true + } + r.more = true + return + } else if path[i] == '|' { + r.part = string(epart) + r.pipe = path[i+1:] + r.piped = true + return + } else if path[i] == '*' || path[i] == '?' { + r.wild = true + } + epart = append(epart, path[i]) + } + } + // append the last part + r.part = string(epart) + return + } + } + r.part = path + return +} + +func parseSquash(json string, i int) (int, string) { + // expects that the lead character is a '[' or '{' or '(' + // squash the value, ignoring all nested arrays and objects. + // the first '[' or '{' or '(' has already been read + s := i + i++ + depth := 1 + for ; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + case '{', '[', '(': + depth++ + case '}', ']', ')': + depth-- + if depth == 0 { + i++ + return i, json[s:i] + } + } + } + } + return i, json[s:] +} + +func parseObject(c *parseContext, i int, path string) (int, bool) { + var pmatch, kesc, vesc, ok, hit bool + var key, val string + rp := parseObjectPath(path) + if !rp.more && rp.piped { + c.pipe = rp.pipe + c.piped = true + } + for i < len(c.json) { + for ; i < len(c.json); i++ { + if c.json[i] == '"' { + // parse_key_string + // this is slightly different from getting s string value + // because we don't need the outer quotes. + i++ + var s = i + for ; i < len(c.json); i++ { + if c.json[i] > '\\' { + continue + } + if c.json[i] == '"' { + i, key, kesc, ok = i+1, c.json[s:i], false, true + goto parse_key_string_done + } + if c.json[i] == '\\' { + i++ + for ; i < len(c.json); i++ { + if c.json[i] > '\\' { + continue + } + if c.json[i] == '"' { + // look for an escaped slash + if c.json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if c.json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + i, key, kesc, ok = i+1, c.json[s:i], true, true + goto parse_key_string_done + } + } + break + } + } + key, kesc, ok = c.json[s:], false, false + parse_key_string_done: + break + } + if c.json[i] == '}' { + return i + 1, false + } + } + if !ok { + return i, false + } + if rp.wild { + if kesc { + pmatch = match.Match(unescape(key), rp.part) + } else { + pmatch = match.Match(key, rp.part) + } + } else { + if kesc { + pmatch = rp.part == unescape(key) + } else { + pmatch = rp.part == key + } + } + hit = pmatch && !rp.more + for ; i < len(c.json); i++ { + switch c.json[i] { + default: + continue + case '"': + i++ + i, val, vesc, ok = parseString(c.json, i) + if !ok { + return i, false + } + if hit { + if vesc { + c.value.Str = unescape(val[1 : len(val)-1]) + } else { + c.value.Str = val[1 : len(val)-1] + } + c.value.Raw = val + c.value.Type = String + return i, true + } + case '{': + if pmatch && !hit { + i, hit = parseObject(c, i+1, rp.path) + if hit { + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '[': + if pmatch && !hit { + i, hit = parseArray(c, i+1, rp.path) + if hit { + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + i, val = parseNumber(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = Number + c.value.Num, _ = strconv.ParseFloat(val, 64) + return i, true + } + case 't', 'f', 'n': + vc := c.json[i] + i, val = parseLiteral(c.json, i) + if hit { + c.value.Raw = val + switch vc { + case 't': + c.value.Type = True + case 'f': + c.value.Type = False + } + return i, true + } + } + break + } + } + return i, false +} +func queryMatches(rp *arrayPathResult, value Result) bool { + rpv := rp.query.value + 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 { + case "=": + return value.Str == rpv + case "!=": + return value.Str != rpv + case "<": + return value.Str < rpv + case "<=": + return value.Str <= rpv + case ">": + return value.Str > rpv + case ">=": + return value.Str >= rpv + case "%": + return match.Match(value.Str, rpv) + case "!%": + return !match.Match(value.Str, rpv) + } + case Number: + rpvn, _ := strconv.ParseFloat(rpv, 64) + switch rp.query.op { + case "=": + return value.Num == rpvn + case "!=": + return value.Num != rpvn + case "<": + return value.Num < rpvn + case "<=": + return value.Num <= rpvn + case ">": + return value.Num > rpvn + case ">=": + return value.Num >= rpvn + } + case True: + switch rp.query.op { + case "=": + return rpv == "true" + case "!=": + return rpv != "true" + case ">": + return rpv == "false" + case ">=": + return true + } + case False: + switch rp.query.op { + case "=": + return rpv == "false" + case "!=": + return rpv != "false" + case "<": + return rpv == "true" + case "<=": + return true + } + } + return false +} +func parseArray(c *parseContext, i int, path string) (int, bool) { + var pmatch, vesc, ok, hit bool + var val string + var h int + var alog []int + var partidx int + var multires []byte + rp := parseArrayPath(path) + if !rp.arrch { + n, ok := parseUint(rp.part) + if !ok { + partidx = -1 + } else { + partidx = int(n) + } + } + if !rp.more && rp.piped { + c.pipe = rp.pipe + c.piped = true + } + + procQuery := func(qval Result) bool { + if rp.query.all { + if len(multires) == 0 { + multires = append(multires, '[') + } + } + var res Result + if qval.Type == JSON { + res = qval.Get(rp.query.path) + } else { + if rp.query.path != "" { + return false + } + res = qval + } + if queryMatches(&rp, res) { + if rp.more { + left, right, ok := splitPossiblePipe(rp.path) + if ok { + rp.path = left + c.pipe = right + c.piped = true + } + res = qval.Get(rp.path) + } else { + res = qval + } + if rp.query.all { + raw := res.Raw + if len(raw) == 0 { + raw = res.String() + } + if raw != "" { + if len(multires) > 1 { + multires = append(multires, ',') + } + multires = append(multires, raw...) + } + } else { + c.value = res + return true + } + } + return false + } + + for i < len(c.json)+1 { + if !rp.arrch { + pmatch = partidx == h + hit = pmatch && !rp.more + } + h++ + if rp.alogok { + alog = append(alog, 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 '"': + i++ + i, val, vesc, ok = parseString(c.json, i) + if !ok { + return i, false + } + if rp.query.on { + var qval Result + if vesc { + qval.Str = unescape(val[1 : len(val)-1]) + } else { + qval.Str = val[1 : len(val)-1] + } + qval.Raw = val + qval.Type = String + if procQuery(qval) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + if vesc { + c.value.Str = unescape(val[1 : len(val)-1]) + } else { + c.value.Str = val[1 : len(val)-1] + } + c.value.Raw = val + c.value.Type = String + return i, true + } + case '{': + if pmatch && !hit { + i, hit = parseObject(c, i+1, rp.path) + if hit { + if rp.alogok { + break + } + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if rp.query.on { + if procQuery(Result{Raw: val, Type: JSON}) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '[': + if pmatch && !hit { + i, hit = parseArray(c, i+1, rp.path) + if hit { + if rp.alogok { + break + } + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if rp.query.on { + if procQuery(Result{Raw: val, Type: JSON}) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + i, val = parseNumber(c.json, i) + if rp.query.on { + var qval Result + qval.Raw = val + qval.Type = Number + qval.Num, _ = strconv.ParseFloat(val, 64) + if procQuery(qval) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = Number + c.value.Num, _ = strconv.ParseFloat(val, 64) + return i, true + } + case 't', 'f', 'n': + vc := c.json[i] + i, val = parseLiteral(c.json, i) + if rp.query.on { + var qval Result + qval.Raw = val + switch vc { + case 't': + qval.Type = True + case 'f': + qval.Type = False + } + if procQuery(qval) { + return i, true + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + switch vc { + case 't': + c.value.Type = True + case 'f': + c.value.Type = False + } + return i, true + } + case ']': + if rp.arrch && rp.part == "#" { + if rp.alogok { + left, right, ok := splitPossiblePipe(rp.alogkey) + if ok { + rp.alogkey = left + c.pipe = right + c.piped = true + } + var jsons = make([]byte, 0, 64) + jsons = append(jsons, '[') + for j, k := 0, 0; j < len(alog); j++ { + idx := alog[j] + for idx < len(c.json) { + switch c.json[idx] { + case ' ', '\t', '\r', '\n': + idx++ + continue + } + break + } + if idx < len(c.json) && c.json[idx] != ']' { + _, res, ok := parseAny(c.json, idx, true) + if ok { + res := res.Get(rp.alogkey) + if res.Exists() { + if k > 0 { + jsons = append(jsons, ',') + } + raw := res.Raw + if len(raw) == 0 { + raw = res.String() + } + jsons = append(jsons, []byte(raw)...) + k++ + } + } + } + } + jsons = append(jsons, ']') + c.value.Type = JSON + c.value.Raw = string(jsons) + return i + 1, true + } + if rp.alogok { + break + } + + c.value.Type = Number + c.value.Num = float64(h - 1) + c.value.Raw = strconv.Itoa(h - 1) + c.calcd = true + return i + 1, true + } + if len(multires) > 0 && !c.value.Exists() { + c.value = Result{ + Raw: string(append(multires, ']')), + Type: JSON, + } + } + return i + 1, false + } + break + } + } + return i, false +} + +func splitPossiblePipe(path string) (left, right string, ok bool) { + // take a quick peek for the pipe character. If found we'll split the piped + // part of the path into the c.pipe field and shorten the rp. + var possible bool + for i := 0; i < len(path); i++ { + if path[i] == '|' { + possible = true + break + } + } + if !possible { + return + } + + if len(path) > 0 && path[0] == '{' { + squashed := squash(path[1:]) + if len(squashed) < len(path)-1 { + squashed = path[:len(squashed)+1] + remain := path[len(squashed):] + if remain[0] == '|' { + return squashed, remain[1:], true + } + } + return + } + + // split the left and right side of the path with the pipe character as + // the delimiter. This is a little tricky because we'll need to basically + // parse the entire path. + for i := 0; i < len(path); i++ { + if path[i] == '\\' { + i++ + } else if path[i] == '.' { + if i == len(path)-1 { + return + } + if path[i+1] == '#' { + i += 2 + if i == len(path) { + return + } + if path[i] == '[' || path[i] == '(' { + var start, end byte + if path[i] == '[' { + start, end = '[', ']' + } else { + start, end = '(', ')' + } + // inside selector, balance brackets + i++ + depth := 1 + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + } else if path[i] == start { + depth++ + } else if path[i] == end { + depth-- + if depth == 0 { + break + } + } else if path[i] == '"' { + // inside selector string, balance quotes + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + } else if path[i] == '"' { + break + } + } + } + } + } + } + } else if path[i] == '|' { + return path[:i], path[i+1:], true + } + } + return +} + +// 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 subSelector struct { + name string + path string +} + +// parseSubSelectors returns the subselectors belonging to a '[path1,path2]' or +// '{"field1":path1,"field2":path2}' type subSelection. It's expected that the +// first character in path is either '[' or '{', and has already been checked +// prior to calling this function. +func parseSubSelectors(path string) (sels []subSelector, out string, ok bool) { + modifer := 0 + depth := 1 + colon := 0 + start := 1 + i := 1 + pushSel := func() { + var sel subSelector + if colon == 0 { + sel.path = path[start:i] + } else { + sel.name = path[start:colon] + sel.path = path[colon+1 : i] + } + sels = append(sels, sel) + colon = 0 + start = i + 1 + } + for ; i < len(path); i++ { + switch path[i] { + case '\\': + i++ + case '@': + if modifer == 0 && i > 0 && (path[i-1] == '.' || path[i-1] == '|') { + modifer = i + } + case ':': + if modifer == 0 && colon == 0 && depth == 1 { + colon = i + } + case ',': + if depth == 1 { + pushSel() + } + case '"': + i++ + loop: + for ; i < len(path); i++ { + switch path[i] { + case '\\': + i++ + case '"': + break loop + } + } + case '[', '(', '{': + depth++ + case ']', ')', '}': + depth-- + if depth == 0 { + pushSel() + path = path[i+1:] + return sels, path, true + } + } + } + return +} + +// nameOfLast returns the name of the last component +func nameOfLast(path string) string { + for i := len(path) - 1; i >= 0; i-- { + if path[i] == '|' || path[i] == '.' { + if i > 0 { + if path[i-1] == '\\' { + continue + } + } + return path[i+1:] + } + } + return path +} + +func isSimpleName(component string) bool { + for i := 0; i < len(component); i++ { + if component[i] < ' ' { + return false + } + switch component[i] { + case '[', ']', '{', '}', '(', ')', '#', '|': + return false + } + } + return true +} + +func appendJSONString(dst []byte, s string) []byte { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 { + d, _ := json.Marshal(s) + return append(dst, string(d)...) + } + } + dst = append(dst, '"') + dst = append(dst, s...) + dst = append(dst, '"') + return dst +} + +type parseContext struct { + json string + value Result + pipe string + piped bool + calcd bool + lines bool +} + +// Get searches json for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// When the value is found it's returned immediately. +// +// A path is a series of keys searated by a dot. +// A key may contain special wildcard characters '*' and '?'. +// To access an array value use the index as the key. +// 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 '\'. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children" >> ["Sara","Alex","Jack"] +// "children.#" >> 3 +// "children.1" >> "Alex" +// "child*.2" >> "Jack" +// "c?ildren.0" >> "Sara" +// "friends.#.first" >> ["James","Roger"] +// +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// If you are consuming JSON from an unpredictable source then you may want to +// use the Valid function first. +func Get(json, path string) Result { + if len(path) > 1 { + if !DisableModifiers { + if path[0] == '@' { + // possible modifier + var ok bool + var npath string + var rjson string + npath, rjson, ok = execModifier(json, path) + if ok { + path = npath + if len(path) > 0 && (path[0] == '|' || path[0] == '.') { + res := Get(rjson, path[1:]) + res.Index = 0 + return res + } + return Parse(rjson) + } + } + } + if path[0] == '[' || path[0] == '{' { + // using a subselector path + kind := path[0] + var ok bool + var subs []subSelector + subs, path, ok = parseSubSelectors(path) + if ok { + if len(path) == 0 || (path[0] == '|' || path[0] == '.') { + var b []byte + b = append(b, kind) + var i int + for _, sub := range subs { + res := Get(json, sub.path) + if res.Exists() { + if i > 0 { + b = append(b, ',') + } + if kind == '{' { + if len(sub.name) > 0 { + if sub.name[0] == '"' && Valid(sub.name) { + b = append(b, sub.name...) + } else { + b = appendJSONString(b, sub.name) + } + } else { + last := nameOfLast(sub.path) + if isSimpleName(last) { + b = appendJSONString(b, last) + } else { + b = appendJSONString(b, "_") + } + } + b = append(b, ':') + } + var raw string + if len(res.Raw) == 0 { + raw = res.String() + if len(raw) == 0 { + raw = "null" + } + } else { + raw = res.Raw + } + b = append(b, raw...) + i++ + } + } + b = append(b, kind+2) + var res Result + res.Raw = string(b) + res.Type = JSON + if len(path) > 0 { + res = res.Get(path[1:]) + } + res.Index = 0 + return res + } + } + } + } + + var i int + var c = &parseContext{json: json} + 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 c.piped { + res := c.value.Get(c.pipe) + res.Index = 0 + return res + } + fillIndex(json, c) + return c.value +} + +// GetBytes searches json for the specified path. +// If working with bytes, this method preferred over Get(string(data), path) +func GetBytes(json []byte, path string) Result { + return getBytes(json, path) +} + +// runeit returns the rune from the the \uXXXX +func runeit(json string) rune { + n, _ := strconv.ParseUint(json[:4], 16, 64) + return rune(n) +} + +// unescape unescapes a string +func unescape(json string) string { + var str = make([]byte, 0, len(json)) + for i := 0; i < len(json); i++ { + switch { + default: + str = append(str, json[i]) + case json[i] < ' ': + return string(str) + case json[i] == '\\': + i++ + if i >= len(json) { + return string(str) + } + switch json[i] { + default: + return string(str) + case '\\': + str = append(str, '\\') + case '/': + str = append(str, '/') + case 'b': + str = append(str, '\b') + case 'f': + str = append(str, '\f') + case 'n': + str = append(str, '\n') + case 'r': + str = append(str, '\r') + case 't': + str = append(str, '\t') + case '"': + str = append(str, '"') + case 'u': + if i+5 > len(json) { + return string(str) + } + r := runeit(json[i+1:]) + i += 5 + if utf16.IsSurrogate(r) { + // need another code + if len(json[i:]) >= 6 && json[i] == '\\' && + json[i+1] == 'u' { + // we expect it to be correct so just consume it + r = utf16.DecodeRune(r, runeit(json[i+2:])) + i += 6 + } + } + // provide enough space to encode the largest utf8 possible + str = append(str, 0, 0, 0, 0, 0, 0, 0, 0) + n := utf8.EncodeRune(str[len(str)-8:], r) + str = str[:len(str)-8+n] + i-- // backtrack index by one + } + } + } + return string(str) +} + +// Less return true if a token is less than another token. +// The caseSensitive paramater is used when the tokens are Strings. +// The order when comparing two different type is: +// +// Null < False < Number < String < True < JSON +// +func (t Result) Less(token Result, caseSensitive bool) bool { + if t.Type < token.Type { + return true + } + if t.Type > token.Type { + return false + } + if t.Type == String { + if caseSensitive { + return t.Str < token.Str + } + return stringLessInsensitive(t.Str, token.Str) + } + if t.Type == Number { + return t.Num < token.Num + } + return t.Raw < token.Raw +} + +func stringLessInsensitive(a, b string) bool { + for i := 0; i < len(a) && i < len(b); i++ { + if a[i] >= 'A' && a[i] <= 'Z' { + if b[i] >= 'A' && b[i] <= 'Z' { + // both are uppercase, do nothing + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } else { + // a is uppercase, convert a to lowercase + if a[i]+32 < b[i] { + return true + } else if a[i]+32 > b[i] { + return false + } + } + } else if b[i] >= 'A' && b[i] <= 'Z' { + // b is uppercase, convert b to lowercase + if a[i] < b[i]+32 { + return true + } else if a[i] > b[i]+32 { + return false + } + } else { + // neither are uppercase + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } + } + return len(a) < len(b) +} + +// parseAny parses the next value from a json string. +// A Result is returned when the hit param is set. +// The return values are (i int, res Result, ok bool) +func parseAny(json string, i int, hit bool) (int, Result, bool) { + var res Result + var val string + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + i, val = parseSquash(json, i) + if hit { + res.Raw = val + res.Type = JSON + } + return i, res, true + } + if json[i] <= ' ' { + continue + } + switch json[i] { + case '"': + i++ + var vesc bool + var ok bool + i, val, vesc, ok = parseString(json, i) + if !ok { + return i, res, false + } + if hit { + res.Type = String + res.Raw = val + if vesc { + res.Str = unescape(val[1 : len(val)-1]) + } else { + res.Str = val[1 : len(val)-1] + } + } + return i, res, true + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + i, val = parseNumber(json, i) + if hit { + res.Raw = val + res.Type = Number + res.Num, _ = strconv.ParseFloat(val, 64) + } + return i, res, true + case 't', 'f', 'n': + vc := json[i] + i, val = parseLiteral(json, i) + if hit { + res.Raw = val + switch vc { + case 't': + res.Type = True + case 'f': + res.Type = False + } + return i, res, true + } + } + } + return i, res, false +} + +var ( // used for testing + testWatchForFallback bool + testLastWasFallback bool +) + +// 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, path ...string) []Result { + res := make([]Result, len(path)) + for i, path := range path { + res[i] = Get(json, path) + } + return res +} + +// 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 { + res := make([]Result, len(path)) + for i, path := range path { + res[i] = GetBytes(json, path) + } + return res +} + +func validpayload(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + i, ok = validany(data, i) + if !ok { + return i, false + } + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + } + } + return i, true + case ' ', '\t', '\n', '\r': + continue + } + } + return i, false +} +func validany(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case '{': + return validobject(data, i+1) + case '[': + return validarray(data, i+1) + case '"': + return validstring(data, i+1) + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return validnumber(data, i+1) + case 't': + return validtrue(data, i+1) + case 'f': + return validfalse(data, i+1) + case 'n': + return validnull(data, i+1) + } + } + return i, false +} +func validobject(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case '}': + return i + 1, true + case '"': + key: + if i, ok = validstring(data, i+1); !ok { + return i, false + } + if i, ok = validcolon(data, i); !ok { + return i, false + } + if i, ok = validany(data, i); !ok { + return i, false + } + if i, ok = validcomma(data, i, '}'); !ok { + return i, false + } + if data[i] == '}' { + return i + 1, true + } + i++ + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case '"': + goto key + } + } + return i, false + } + } + return i, false +} +func validcolon(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case ':': + return i + 1, true + } + } + return i, false +} +func validcomma(data []byte, i int, end byte) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + return i, false + case ' ', '\t', '\n', '\r': + continue + case ',': + return i, true + case end: + return i, true + } + } + return i, false +} +func validarray(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + switch data[i] { + default: + for ; i < len(data); i++ { + if i, ok = validany(data, i); !ok { + return i, false + } + if i, ok = validcomma(data, i, ']'); !ok { + return i, false + } + if data[i] == ']' { + return i + 1, true + } + } + case ' ', '\t', '\n', '\r': + continue + case ']': + return i + 1, true + } + } + return i, false +} +func validstring(data []byte, i int) (outi int, ok bool) { + for ; i < len(data); i++ { + if data[i] < ' ' { + return i, false + } else if data[i] == '\\' { + i++ + if i == len(data) { + return i, false + } + switch data[i] { + default: + return i, false + case '"', '\\', '/', 'b', 'f', 'n', 'r', 't': + case 'u': + for j := 0; j < 4; j++ { + i++ + if i >= len(data) { + return i, false + } + if !((data[i] >= '0' && data[i] <= '9') || + (data[i] >= 'a' && data[i] <= 'f') || + (data[i] >= 'A' && data[i] <= 'F')) { + return i, false + } + } + } + } else if data[i] == '"' { + return i + 1, true + } + } + return i, false +} +func validnumber(data []byte, i int) (outi int, ok bool) { + i-- + // sign + if data[i] == '-' { + i++ + } + // int + if i == len(data) { + return i, false + } + if data[i] == '0' { + i++ + } else { + for ; i < len(data); i++ { + if data[i] >= '0' && data[i] <= '9' { + continue + } + break + } + } + // frac + if i == len(data) { + return i, true + } + if data[i] == '.' { + i++ + if i == len(data) { + return i, false + } + if data[i] < '0' || data[i] > '9' { + return i, false + } + i++ + for ; i < len(data); i++ { + if data[i] >= '0' && data[i] <= '9' { + continue + } + break + } + } + // exp + if i == len(data) { + return i, true + } + if data[i] == 'e' || data[i] == 'E' { + i++ + if i == len(data) { + return i, false + } + if data[i] == '+' || data[i] == '-' { + i++ + } + if i == len(data) { + return i, false + } + if data[i] < '0' || data[i] > '9' { + return i, false + } + i++ + for ; i < len(data); i++ { + if data[i] >= '0' && data[i] <= '9' { + continue + } + break + } + } + return i, true +} + +func validtrue(data []byte, i int) (outi int, ok bool) { + if i+3 <= len(data) && data[i] == 'r' && data[i+1] == 'u' && + data[i+2] == 'e' { + return i + 3, true + } + return i, false +} +func validfalse(data []byte, i int) (outi int, ok bool) { + if i+4 <= len(data) && data[i] == 'a' && data[i+1] == 'l' && + data[i+2] == 's' && data[i+3] == 'e' { + return i + 4, true + } + return i, false +} +func validnull(data []byte, i int) (outi int, ok bool) { + if i+3 <= len(data) && data[i] == 'u' && data[i+1] == 'l' && + data[i+2] == 'l' { + return i + 3, true + } + return i, false +} + +// Valid returns true if the input is valid json. +// +// if !gjson.Valid(json) { +// return errors.New("invalid json") +// } +// value := gjson.Get(json, "name.last") +// +func Valid(json string) bool { + _, ok := validpayload(stringBytes(json), 0) + return ok +} + +// ValidBytes returns true if the input is valid json. +// +// if !gjson.Valid(json) { +// return errors.New("invalid json") +// } +// value := gjson.Get(json, "name.last") +// +// If working with bytes, this method preferred over ValidBytes(string(data)) +// +func ValidBytes(json []byte) bool { + _, ok := validpayload(json, 0) + return ok +} + +func parseUint(s string) (n uint64, ok bool) { + var i int + if i == len(s) { + return 0, false + } + for ; i < len(s); i++ { + if s[i] >= '0' && s[i] <= '9' { + n = n*10 + uint64(s[i]-'0') + } else { + return 0, false + } + } + return n, true +} + +func parseInt(s string) (n int64, ok bool) { + var i int + var sign bool + if len(s) > 0 && s[0] == '-' { + sign = true + i++ + } + if i == len(s) { + return 0, false + } + for ; i < len(s); i++ { + if s[i] >= '0' && s[i] <= '9' { + n = n*10 + int64(s[i]-'0') + } else { + return 0, false + } + } + if sign { + return n * -1, true + } + return n, true +} + +const minUint53 = 0 +const maxUint53 = 4503599627370495 +const minInt53 = -2251799813685248 +const maxInt53 = 2251799813685247 + +func floatToUint(f float64) (n uint64, ok bool) { + n = uint64(f) + if float64(n) == f && n >= minUint53 && n <= maxUint53 { + return n, true + } + return 0, false +} + +func floatToInt(f float64) (n int64, ok bool) { + n = int64(f) + if float64(n) == f && n >= minInt53 && n <= maxInt53 { + return n, true + } + return 0, false +} + +// execModifier parses the path to find a matching modifier function. +// then input expects that the path already starts with a '@' +func execModifier(json, path string) (pathOut, res string, ok bool) { + name := path[1:] + var hasArgs bool + for i := 1; i < len(path); i++ { + if path[i] == ':' { + pathOut = path[i+1:] + name = path[1:i] + hasArgs = len(pathOut) > 0 + break + } + if path[i] == '|' { + pathOut = path[i:] + name = path[1:i] + break + } + if path[i] == '.' { + pathOut = path[i:] + name = path[1:i] + break + } + } + if fn, ok := modifiers[name]; ok { + var args string + if hasArgs { + var parsedArgs bool + switch pathOut[0] { + case '{', '[', '"': + res := Parse(pathOut) + if res.Exists() { + args = squash(pathOut) + pathOut = pathOut[len(args):] + parsedArgs = true + } + } + if !parsedArgs { + idx := strings.IndexByte(pathOut, '|') + if idx == -1 { + args = pathOut + pathOut = "" + } else { + args = pathOut[:idx] + pathOut = pathOut[idx:] + } + } + } + return pathOut, fn(json, args), true + } + return pathOut, res, false +} + +// unwrap removes the '[]' or '{}' characters around json +func unwrap(json string) string { + json = trim(json) + if len(json) >= 2 && json[0] == '[' || json[0] == '{' { + json = json[1 : len(json)-1] + } + return json +} + +// DisableModifiers will disable the modifier syntax +var DisableModifiers = false + +var modifiers = map[string]func(json, arg string) string{ + "pretty": modPretty, + "ugly": modUgly, + "reverse": modReverse, + "this": modThis, + "flatten": modFlatten, + "join": modJoin, + "valid": modValid, +} + +// AddModifier binds a custom modifier command to the GJSON syntax. +// This operation is not thread safe and should be executed prior to +// using all other gjson function. +func AddModifier(name string, fn func(json, arg string) string) { + modifiers[name] = fn +} + +// ModifierExists returns true when the specified modifier exists. +func ModifierExists(name string, fn func(json, arg string) string) bool { + _, ok := modifiers[name] + return ok +} + +// @pretty modifier makes the json look nice. +func modPretty(json, arg string) string { + if len(arg) > 0 { + opts := *pretty.DefaultOptions + Parse(arg).ForEach(func(key, value Result) bool { + switch key.String() { + case "sortKeys": + opts.SortKeys = value.Bool() + case "indent": + opts.Indent = value.String() + case "prefix": + opts.Prefix = value.String() + case "width": + opts.Width = int(value.Int()) + } + return true + }) + return bytesString(pretty.PrettyOptions(stringBytes(json), &opts)) + } + return bytesString(pretty.Pretty(stringBytes(json))) +} + +// @this returns the current element. Can be used to retrieve the root element. +func modThis(json, arg string) string { + return json +} + +// @ugly modifier removes all whitespace. +func modUgly(json, arg string) string { + return bytesString(pretty.Ugly(stringBytes(json))) +} + +// @reverse reverses array elements or root object members. +func modReverse(json, arg string) string { + res := Parse(json) + if res.IsArray() { + var values []Result + res.ForEach(func(_, value Result) bool { + values = append(values, value) + return true + }) + out := make([]byte, 0, len(json)) + out = append(out, '[') + for i, j := len(values)-1, 0; i >= 0; i, j = i-1, j+1 { + if j > 0 { + out = append(out, ',') + } + out = append(out, values[i].Raw...) + } + out = append(out, ']') + return bytesString(out) + } + if res.IsObject() { + var keyValues []Result + res.ForEach(func(key, value Result) bool { + keyValues = append(keyValues, key, value) + return true + }) + out := make([]byte, 0, len(json)) + out = append(out, '{') + for i, j := len(keyValues)-2, 0; i >= 0; i, j = i-2, j+1 { + if j > 0 { + out = append(out, ',') + } + out = append(out, keyValues[i+0].Raw...) + out = append(out, ':') + out = append(out, keyValues[i+1].Raw...) + } + out = append(out, '}') + return bytesString(out) + } + return json +} + +// @flatten an array with child arrays. +// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]] +// The {"deep":true} arg can be provide for deep flattening. +// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7] +// The original json is returned when the json is not an array. +func modFlatten(json, arg string) string { + res := Parse(json) + if !res.IsArray() { + return json + } + var deep bool + if arg != "" { + Parse(arg).ForEach(func(key, value Result) bool { + if key.String() == "deep" { + deep = value.Bool() + } + return true + }) + } + var out []byte + out = append(out, '[') + var idx int + res.ForEach(func(_, value Result) bool { + if idx > 0 { + out = append(out, ',') + } + if value.IsArray() { + if deep { + out = append(out, unwrap(modFlatten(value.Raw, arg))...) + } else { + out = append(out, unwrap(value.Raw)...) + } + } else { + out = append(out, value.Raw...) + } + idx++ + return true + }) + out = append(out, ']') + return bytesString(out) +} + +// @join multiple objects into a single object. +// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"} +// The arg can be "true" to specify that duplicate keys should be preserved. +// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41} +// Without preserved keys: +// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41} +// The original json is returned when the json is not an object. +func modJoin(json, arg string) string { + res := Parse(json) + if !res.IsArray() { + return json + } + var preserve bool + if arg != "" { + Parse(arg).ForEach(func(key, value Result) bool { + if key.String() == "preserve" { + preserve = value.Bool() + } + return true + }) + } + var out []byte + out = append(out, '{') + if preserve { + // Preserve duplicate keys. + var idx int + res.ForEach(func(_, value Result) bool { + if !value.IsObject() { + return true + } + if idx > 0 { + out = append(out, ',') + } + out = append(out, unwrap(value.Raw)...) + idx++ + return true + }) + } else { + // Deduplicate keys and generate an object with stable ordering. + var keys []Result + kvals := make(map[string]Result) + res.ForEach(func(_, value Result) bool { + if !value.IsObject() { + return true + } + value.ForEach(func(key, value Result) bool { + k := key.String() + if _, ok := kvals[k]; !ok { + keys = append(keys, key) + } + kvals[k] = value + return true + }) + return true + }) + for i := 0; i < len(keys); i++ { + if i > 0 { + out = append(out, ',') + } + out = append(out, keys[i].Raw...) + out = append(out, ':') + out = append(out, kvals[keys[i].String()].Raw...) + } + } + out = append(out, '}') + return bytesString(out) +} + +// @valid ensures that the json is valid before moving on. An empty string is +// returned when the json is not valid, otherwise it returns the original json. +func modValid(json, arg string) string { + if !Valid(json) { + return "" + } + return json +} diff --git a/gjson/gjson_gae.go b/gjson/gjson_gae.go new file mode 100644 index 000000000..bacc11eea --- /dev/null +++ b/gjson/gjson_gae.go @@ -0,0 +1,34 @@ +//+build appengine js + +package gjson + +import ( + "reflect" + "unsafe" +) + +func getBytes(json []byte, path string) Result { + return Get(string(json), path) +} + +// fillIndex finds the position of Raw data and assigns it to the Index field +// of the resulting value. If the position cannot be found then Index zero is +// used instead. +func fillIndex(json string, c *parseContext) { + if len(c.value.Raw) > 0 && !c.calcd { + jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json)) + rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw))) + c.value.Index = int(rhdr.Data - jhdr.Data) + if c.value.Index < 0 || c.value.Index >= len(json) { + c.value.Index = 0 + } + } +} + +func stringBytes(s string) []byte { + return []byte(s) +} + +func bytesString(b []byte) string { + return string(b) +} diff --git a/gjson/gjson_ngae.go b/gjson/gjson_ngae.go new file mode 100644 index 000000000..bc608b53b --- /dev/null +++ b/gjson/gjson_ngae.go @@ -0,0 +1,81 @@ +//+build !appengine +//+build !js + +package gjson + +import ( + "reflect" + "unsafe" +) + +// getBytes casts the input json bytes to a string and safely returns the +// results as uniquely allocated data. This operation is intended to minimize +// copies and allocations for the large json string->[]byte. +func getBytes(json []byte, path string) Result { + var result Result + if json != nil { + // unsafe cast to string + result = Get(*(*string)(unsafe.Pointer(&json)), path) + // safely get the string headers + rawhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Raw)) + strhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Str)) + // create byte slice headers + rawh := reflect.SliceHeader{Data: rawhi.Data, Len: rawhi.Len} + strh := reflect.SliceHeader{Data: strhi.Data, Len: strhi.Len} + if strh.Data == 0 { + // str is nil + if rawh.Data == 0 { + // raw is nil + result.Raw = "" + } else { + // raw has data, safely copy the slice header to a string + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + } + result.Str = "" + } else if rawh.Data == 0 { + // raw is nil + result.Raw = "" + // str has data, safely copy the slice header to a string + result.Str = string(*(*[]byte)(unsafe.Pointer(&strh))) + } else if strh.Data >= rawh.Data && + int(strh.Data)+strh.Len <= int(rawh.Data)+rawh.Len { + // Str is a substring of Raw. + start := int(strh.Data - rawh.Data) + // safely copy the raw slice header + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + // substring the raw + result.Str = result.Raw[start : start+strh.Len] + } else { + // safely copy both the raw and str slice headers to strings + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + result.Str = string(*(*[]byte)(unsafe.Pointer(&strh))) + } + } + return result +} + +// fillIndex finds the position of Raw data and assigns it to the Index field +// of the resulting value. If the position cannot be found then Index zero is +// used instead. +func fillIndex(json string, c *parseContext) { + if len(c.value.Raw) > 0 && !c.calcd { + jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json)) + rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw))) + c.value.Index = int(rhdr.Data - jhdr.Data) + if c.value.Index < 0 || c.value.Index >= len(json) { + c.value.Index = 0 + } + } +} + +func stringBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: (*reflect.StringHeader)(unsafe.Pointer(&s)).Data, + Len: len(s), + Cap: len(s), + })) +} + +func bytesString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/gjson/gjson_test.go b/gjson/gjson_test.go new file mode 100644 index 000000000..36a427332 --- /dev/null +++ b/gjson/gjson_test.go @@ -0,0 +1,2164 @@ +package gjson + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math/rand" + "strconv" + "strings" + "testing" + "time" + + "github.com/tidwall/pretty" +) + +// TestRandomData is a fuzzing test that throws random data at the Parse +// function looking for panics. +func TestRandomData(t *testing.T) { + var lstr string + defer func() { + if v := recover(); v != nil { + println("'" + hex.EncodeToString([]byte(lstr)) + "'") + println("'" + lstr + "'") + panic(v) + } + }() + rand.Seed(time.Now().UnixNano()) + b := make([]byte, 200) + for i := 0; i < 2000000; i++ { + n, err := rand.Read(b[:rand.Int()%len(b)]) + if err != nil { + t.Fatal(err) + } + lstr = string(b[:n]) + GetBytes([]byte(lstr), "zzzz") + Parse(lstr) + } +} + +func TestRandomValidStrings(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + b := make([]byte, 200) + for i := 0; i < 100000; i++ { + n, err := rand.Read(b[:rand.Int()%len(b)]) + if err != nil { + t.Fatal(err) + } + sm, err := json.Marshal(string(b[:n])) + if err != nil { + t.Fatal(err) + } + var su string + if err := json.Unmarshal([]byte(sm), &su); err != nil { + t.Fatal(err) + } + token := Get(`{"str":`+string(sm)+`}`, "str") + if token.Type != String || token.Str != su { + println("["+token.Raw+"]", "["+token.Str+"]", "["+su+"]", + "["+string(sm)+"]") + t.Fatal("string mismatch") + } + } +} + +func TestEmoji(t *testing.T) { + const input = `{"utf8":"Example emoji, KO: \ud83d\udd13, \ud83c\udfc3 ` + + `OK: \u2764\ufe0f "}` + value := Get(input, "utf8") + var s string + json.Unmarshal([]byte(value.Raw), &s) + if value.String() != s { + t.Fatalf("expected '%v', got '%v'", s, value.String()) + } +} + +func testEscapePath(t *testing.T, json, path, expect string) { + if Get(json, path).String() != expect { + t.Fatalf("expected '%v', got '%v'", expect, Get(json, path).String()) + } +} + +func TestEscapePath(t *testing.T) { + json := `{ + "test":{ + "*":"valZ", + "*v":"val0", + "keyv*":"val1", + "key*v":"val2", + "keyv?":"val3", + "key?v":"val4", + "keyv.":"val5", + "key.v":"val6", + "keyk*":{"key?":"val7"} + } + }` + + testEscapePath(t, json, "test.\\*", "valZ") + testEscapePath(t, json, "test.\\*v", "val0") + testEscapePath(t, json, "test.keyv\\*", "val1") + testEscapePath(t, json, "test.key\\*v", "val2") + testEscapePath(t, json, "test.keyv\\?", "val3") + testEscapePath(t, json, "test.key\\?v", "val4") + testEscapePath(t, json, "test.keyv\\.", "val5") + testEscapePath(t, json, "test.key\\.v", "val6") + testEscapePath(t, json, "test.keyk\\*.key\\?", "val7") +} + +// this json block is poorly formed on purpose. +var basicJSON = `{"age":100, "name":{"here":"B\\\"R"}, + "noop":{"what is a wren?":"a bird"}, + "happy":true,"immortal":false, + "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], + "vals":[1,2,3,{"sadf":sdf"asdf"}],"name":{"first":"tom","last":null}, + "created":"2014-05-16T08:28:06.989Z", + "loggy":{ + "programmers": [ + { + "firstName": "Brett", + "lastName": "McLaughlin", + "email": "aaaa", + "tag": "good" + }, + { + "firstName": "Jason", + "lastName": "Hunter", + "email": "bbbb", + "tag": "bad" + }, + { + "firstName": "Elliotte", + "lastName": "Harold", + "email": "cccc", + "tag":, "good" + }, + { + "firstName": 1002.3, + "age": 101 + } + ] + }, + "lastly":{"yay":"final"} +}` +var basicJSONB = []byte(basicJSON) + +func TestTimeResult(t *testing.T) { + assert(t, Get(basicJSON, "created").String() == + Get(basicJSON, "created").Time().Format(time.RFC3339Nano)) +} + +func TestParseAny(t *testing.T) { + assert(t, Parse("100").Float() == 100) + assert(t, Parse("true").Bool()) + assert(t, Parse("false").Bool() == false) + assert(t, Parse("yikes").Exists() == false) +} + +func TestManyVariousPathCounts(t *testing.T) { + json := `{"a":"a","b":"b","c":"c"}` + counts := []int{3, 4, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, + 128, 129, 255, 256, 257, 511, 512, 513} + paths := []string{"a", "b", "c"} + expects := []string{"a", "b", "c"} + for _, count := range counts { + var gpaths []string + var gexpects []string + for i := 0; i < count; i++ { + if i < len(paths) { + gpaths = append(gpaths, paths[i]) + gexpects = append(gexpects, expects[i]) + } else { + gpaths = append(gpaths, fmt.Sprintf("not%d", i)) + gexpects = append(gexpects, "null") + } + } + results := GetMany(json, gpaths...) + for i := 0; i < len(paths); i++ { + if results[i].String() != expects[i] { + t.Fatalf("expected '%v', got '%v'", expects[i], + results[i].String()) + } + } + } +} +func TestManyRecursion(t *testing.T) { + var json string + var path string + for i := 0; i < 100; i++ { + json += `{"a":` + path += ".a" + } + json += `"b"` + for i := 0; i < 100; i++ { + json += `}` + } + path = path[1:] + assert(t, GetMany(json, path)[0].String() == "b") +} +func TestByteSafety(t *testing.T) { + jsonb := []byte(`{"name":"Janet","age":38}`) + mtok := GetBytes(jsonb, "name") + if mtok.String() != "Janet" { + t.Fatalf("expected %v, got %v", "Jason", mtok.String()) + } + mtok2 := GetBytes(jsonb, "age") + if mtok2.Raw != "38" { + t.Fatalf("expected %v, got %v", "Jason", mtok2.Raw) + } + jsonb[9] = 'T' + jsonb[12] = 'd' + jsonb[13] = 'y' + if mtok.String() != "Janet" { + t.Fatalf("expected %v, got %v", "Jason", mtok.String()) + } +} + +func get(json, path string) Result { + return GetBytes([]byte(json), path) +} + +func TestBasic(t *testing.T) { + var mtok Result + mtok = get(basicJSON, `loggy.programmers.#[tag="good"].firstName`) + if mtok.String() != "Brett" { + t.Fatalf("expected %v, got %v", "Brett", mtok.String()) + } + mtok = get(basicJSON, `loggy.programmers.#[tag="good"]#.firstName`) + if mtok.String() != `["Brett","Elliotte"]` { + t.Fatalf("expected %v, got %v", `["Brett","Elliotte"]`, mtok.String()) + } +} + +func TestIsArrayIsObject(t *testing.T) { + mtok := get(basicJSON, "loggy") + assert(t, mtok.IsObject()) + assert(t, !mtok.IsArray()) + + mtok = get(basicJSON, "loggy.programmers") + assert(t, !mtok.IsObject()) + assert(t, mtok.IsArray()) + + mtok = get(basicJSON, `loggy.programmers.#[tag="good"]#.firstName`) + assert(t, mtok.IsArray()) + + mtok = get(basicJSON, `loggy.programmers.0.firstName`) + assert(t, !mtok.IsObject()) + assert(t, !mtok.IsArray()) +} + +func TestPlus53BitInts(t *testing.T) { + json := `{"IdentityData":{"GameInstanceId":634866135153775564}}` + value := Get(json, "IdentityData.GameInstanceId") + assert(t, value.Uint() == 634866135153775564) + assert(t, value.Int() == 634866135153775564) + assert(t, value.Float() == 634866135153775616) + + json = `{"IdentityData":{"GameInstanceId":634866135153775564.88172}}` + value = Get(json, "IdentityData.GameInstanceId") + assert(t, value.Uint() == 634866135153775616) + assert(t, value.Int() == 634866135153775616) + assert(t, value.Float() == 634866135153775616.88172) + + json = `{ + "min_uint64": 0, + "max_uint64": 18446744073709551615, + "overflow_uint64": 18446744073709551616, + "min_int64": -9223372036854775808, + "max_int64": 9223372036854775807, + "overflow_int64": 9223372036854775808, + "min_uint53": 0, + "max_uint53": 4503599627370495, + "overflow_uint53": 4503599627370496, + "min_int53": -2251799813685248, + "max_int53": 2251799813685247, + "overflow_int53": 2251799813685248 + }` + + assert(t, Get(json, "min_uint53").Uint() == 0) + assert(t, Get(json, "max_uint53").Uint() == 4503599627370495) + assert(t, Get(json, "overflow_uint53").Int() == 4503599627370496) + assert(t, Get(json, "min_int53").Int() == -2251799813685248) + assert(t, Get(json, "max_int53").Int() == 2251799813685247) + assert(t, Get(json, "overflow_int53").Int() == 2251799813685248) + assert(t, Get(json, "min_uint64").Uint() == 0) + assert(t, Get(json, "max_uint64").Uint() == 18446744073709551615) + // this next value overflows the max uint64 by one which will just + // flip the number to zero + assert(t, Get(json, "overflow_uint64").Int() == 0) + assert(t, Get(json, "min_int64").Int() == -9223372036854775808) + assert(t, Get(json, "max_int64").Int() == 9223372036854775807) + // this next value overflows the max int64 by one which will just + // flip the number to the negative sign. + assert(t, Get(json, "overflow_int64").Int() == -9223372036854775808) +} +func TestIssue38(t *testing.T) { + // These should not fail, even though the unicode is invalid. + Get(`["S3O PEDRO DO BUTI\udf93"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93asdf"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93\u"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93\u1"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93\u13"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93\u134"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93\u1345"]`, "0") + Get(`["S3O PEDRO DO BUTI\udf93\u1345asd"]`, "0") +} +func TestTypes(t *testing.T) { + assert(t, (Result{Type: String}).Type.String() == "String") + assert(t, (Result{Type: Number}).Type.String() == "Number") + assert(t, (Result{Type: Null}).Type.String() == "Null") + assert(t, (Result{Type: False}).Type.String() == "False") + assert(t, (Result{Type: True}).Type.String() == "True") + assert(t, (Result{Type: JSON}).Type.String() == "JSON") + assert(t, (Result{Type: 100}).Type.String() == "") + // bool + assert(t, (Result{Type: String, Str: "true"}).Bool()) + assert(t, (Result{Type: True}).Bool()) + assert(t, (Result{Type: False}).Bool() == false) + assert(t, (Result{Type: Number, Num: 1}).Bool()) + // int + assert(t, (Result{Type: String, Str: "1"}).Int() == 1) + assert(t, (Result{Type: True}).Int() == 1) + assert(t, (Result{Type: False}).Int() == 0) + assert(t, (Result{Type: Number, Num: 1}).Int() == 1) + // uint + assert(t, (Result{Type: String, Str: "1"}).Uint() == 1) + assert(t, (Result{Type: True}).Uint() == 1) + assert(t, (Result{Type: False}).Uint() == 0) + assert(t, (Result{Type: Number, Num: 1}).Uint() == 1) + // float + assert(t, (Result{Type: String, Str: "1"}).Float() == 1) + assert(t, (Result{Type: True}).Float() == 1) + assert(t, (Result{Type: False}).Float() == 0) + assert(t, (Result{Type: Number, Num: 1}).Float() == 1) +} +func TestForEach(t *testing.T) { + Result{}.ForEach(nil) + Result{Type: String, Str: "Hello"}.ForEach(func(_, value Result) bool { + assert(t, value.String() == "Hello") + return false + }) + Result{Type: JSON, Raw: "*invalid*"}.ForEach(nil) + + json := ` {"name": {"first": "Janet","last": "Prichard"}, + "asd\nf":"\ud83d\udd13","age": 47}` + var count int + ParseBytes([]byte(json)).ForEach(func(key, value Result) bool { + count++ + return true + }) + assert(t, count == 3) + ParseBytes([]byte(`{"bad`)).ForEach(nil) + ParseBytes([]byte(`{"ok":"bad`)).ForEach(nil) +} +func TestMap(t *testing.T) { + assert(t, len(ParseBytes([]byte(`"asdf"`)).Map()) == 0) + assert(t, ParseBytes([]byte(`{"asdf":"ghjk"`)).Map()["asdf"].String() == + "ghjk") + assert(t, len(Result{Type: JSON, Raw: "**invalid**"}.Map()) == 0) + assert(t, Result{Type: JSON, Raw: "**invalid**"}.Value() == nil) + assert(t, Result{Type: JSON, Raw: "{"}.Map() != nil) +} +func TestBasic1(t *testing.T) { + mtok := get(basicJSON, `loggy.programmers`) + var count int + mtok.ForEach(func(key, value Result) bool { + if key.Exists() { + t.Fatalf("expected %v, got %v", false, key.Exists()) + } + count++ + if count == 3 { + return false + } + if count == 1 { + i := 0 + value.ForEach(func(key, value Result) bool { + switch i { + case 0: + if key.String() != "firstName" || + value.String() != "Brett" { + t.Fatalf("expected %v/%v got %v/%v", "firstName", + "Brett", key.String(), value.String()) + } + case 1: + if key.String() != "lastName" || + value.String() != "McLaughlin" { + t.Fatalf("expected %v/%v got %v/%v", "lastName", + "McLaughlin", key.String(), value.String()) + } + case 2: + if key.String() != "email" || value.String() != "aaaa" { + t.Fatalf("expected %v/%v got %v/%v", "email", "aaaa", + key.String(), value.String()) + } + } + i++ + return true + }) + } + return true + }) + if count != 3 { + t.Fatalf("expected %v, got %v", 3, count) + } +} +func TestBasic2(t *testing.T) { + mtok := get(basicJSON, `loggy.programmers.#[age=101].firstName`) + if mtok.String() != "1002.3" { + t.Fatalf("expected %v, got %v", "1002.3", mtok.String()) + } + mtok = get(basicJSON, + `loggy.programmers.#[firstName != "Brett"].firstName`) + if mtok.String() != "Jason" { + t.Fatalf("expected %v, got %v", "Jason", mtok.String()) + } + mtok = get(basicJSON, `loggy.programmers.#[firstName % "Bre*"].email`) + if mtok.String() != "aaaa" { + t.Fatalf("expected %v, got %v", "aaaa", mtok.String()) + } + mtok = get(basicJSON, `loggy.programmers.#[firstName !% "Bre*"].email`) + if mtok.String() != "bbbb" { + t.Fatalf("expected %v, got %v", "bbbb", mtok.String()) + } + mtok = get(basicJSON, `loggy.programmers.#[firstName == "Brett"].email`) + if mtok.String() != "aaaa" { + t.Fatalf("expected %v, got %v", "aaaa", mtok.String()) + } + mtok = get(basicJSON, "loggy") + if mtok.Type != JSON { + t.Fatalf("expected %v, got %v", JSON, mtok.Type) + } + if len(mtok.Map()) != 1 { + t.Fatalf("expected %v, got %v", 1, len(mtok.Map())) + } + programmers := mtok.Map()["programmers"] + if programmers.Array()[1].Map()["firstName"].Str != "Jason" { + t.Fatalf("expected %v, got %v", "Jason", + mtok.Map()["programmers"].Array()[1].Map()["firstName"].Str) + } +} +func TestBasic3(t *testing.T) { + var mtok Result + if Parse(basicJSON).Get("loggy.programmers").Get("1"). + Get("firstName").Str != "Jason" { + t.Fatalf("expected %v, got %v", "Jason", Parse(basicJSON). + Get("loggy.programmers").Get("1").Get("firstName").Str) + } + var token Result + if token = Parse("-102"); token.Num != -102 { + t.Fatalf("expected %v, got %v", -102, token.Num) + } + if token = Parse("102"); token.Num != 102 { + t.Fatalf("expected %v, got %v", 102, token.Num) + } + if token = Parse("102.2"); token.Num != 102.2 { + t.Fatalf("expected %v, got %v", 102.2, token.Num) + } + if token = Parse(`"hello"`); token.Str != "hello" { + t.Fatalf("expected %v, got %v", "hello", token.Str) + } + if token = Parse(`"\"he\nllo\""`); token.Str != "\"he\nllo\"" { + t.Fatalf("expected %v, got %v", "\"he\nllo\"", token.Str) + } + mtok = get(basicJSON, "loggy.programmers.#.firstName") + if len(mtok.Array()) != 4 { + t.Fatalf("expected 4, got %v", len(mtok.Array())) + } + for i, ex := range []string{"Brett", "Jason", "Elliotte", "1002.3"} { + if mtok.Array()[i].String() != ex { + t.Fatalf("expected '%v', got '%v'", ex, mtok.Array()[i].String()) + } + } + mtok = get(basicJSON, "loggy.programmers.#.asd") + if mtok.Type != JSON { + t.Fatalf("expected %v, got %v", JSON, mtok.Type) + } + if len(mtok.Array()) != 0 { + t.Fatalf("expected 0, got %v", len(mtok.Array())) + } +} +func TestBasic4(t *testing.T) { + if get(basicJSON, "items.3.tags.#").Num != 3 { + t.Fatalf("expected 3, got %v", get(basicJSON, "items.3.tags.#").Num) + } + if get(basicJSON, "items.3.points.1.#").Num != 2 { + t.Fatalf("expected 2, got %v", + get(basicJSON, "items.3.points.1.#").Num) + } + if get(basicJSON, "items.#").Num != 8 { + t.Fatalf("expected 6, got %v", get(basicJSON, "items.#").Num) + } + if get(basicJSON, "vals.#").Num != 4 { + t.Fatalf("expected 4, got %v", get(basicJSON, "vals.#").Num) + } + if !get(basicJSON, "name.last").Exists() { + t.Fatal("expected true, got false") + } + token := get(basicJSON, "name.here") + if token.String() != "B\\\"R" { + t.Fatal("expecting 'B\\\"R'", "got", token.String()) + } + token = get(basicJSON, "arr.#") + if token.String() != "6" { + fmt.Printf("%#v\n", token) + t.Fatal("expecting 6", "got", token.String()) + } + token = get(basicJSON, "arr.3.hello") + if token.String() != "world" { + t.Fatal("expecting 'world'", "got", token.String()) + } + _ = token.Value().(string) + token = get(basicJSON, "name.first") + if token.String() != "tom" { + t.Fatal("expecting 'tom'", "got", token.String()) + } + _ = token.Value().(string) + token = get(basicJSON, "name.last") + if token.String() != "" { + t.Fatal("expecting ''", "got", token.String()) + } + if token.Value() != nil { + t.Fatal("should be nil") + } +} +func TestBasic5(t *testing.T) { + token := get(basicJSON, "age") + if token.String() != "100" { + t.Fatal("expecting '100'", "got", token.String()) + } + _ = token.Value().(float64) + token = get(basicJSON, "happy") + if token.String() != "true" { + t.Fatal("expecting 'true'", "got", token.String()) + } + _ = token.Value().(bool) + token = get(basicJSON, "immortal") + if token.String() != "false" { + t.Fatal("expecting 'false'", "got", token.String()) + } + _ = token.Value().(bool) + token = get(basicJSON, "noop") + if token.String() != `{"what is a wren?":"a bird"}` { + t.Fatal("expecting '"+`{"what is a wren?":"a bird"}`+"'", "got", + token.String()) + } + _ = token.Value().(map[string]interface{}) + + if get(basicJSON, "").Value() != nil { + t.Fatal("should be nil") + } + + get(basicJSON, "vals.hello") + + type msi = map[string]interface{} + type fi = []interface{} + mm := Parse(basicJSON).Value().(msi) + fn := mm["loggy"].(msi)["programmers"].(fi)[1].(msi)["firstName"].(string) + if fn != "Jason" { + t.Fatalf("expecting %v, got %v", "Jason", fn) + } +} +func TestUnicode(t *testing.T) { + var json = `{"key":0,"的情况下解":{"key":1,"的情况":2}}` + if Get(json, "的情况下解.key").Num != 1 { + t.Fatal("fail") + } + if Get(json, "的情况下解.的情况").Num != 2 { + t.Fatal("fail") + } + if Get(json, "的情况下解.的?况").Num != 2 { + t.Fatal("fail") + } + if Get(json, "的情况下解.的?*").Num != 2 { + t.Fatal("fail") + } + if Get(json, "的情况下解.*?况").Num != 2 { + t.Fatal("fail") + } + if Get(json, "的情?下解.*?况").Num != 2 { + t.Fatal("fail") + } + if Get(json, "的情下解.*?况").Num != 0 { + t.Fatal("fail") + } +} + +func TestUnescape(t *testing.T) { + unescape(string([]byte{'\\', '\\', 0})) + unescape(string([]byte{'\\', '/', '\\', 'b', '\\', 'f'})) +} +func assert(t testing.TB, cond bool) { + if !cond { + panic("assert failed") + } +} +func TestLess(t *testing.T) { + assert(t, !Result{Type: Null}.Less(Result{Type: Null}, true)) + assert(t, Result{Type: Null}.Less(Result{Type: False}, true)) + assert(t, Result{Type: Null}.Less(Result{Type: True}, true)) + assert(t, Result{Type: Null}.Less(Result{Type: JSON}, true)) + assert(t, Result{Type: Null}.Less(Result{Type: Number}, true)) + assert(t, Result{Type: Null}.Less(Result{Type: String}, true)) + assert(t, !Result{Type: False}.Less(Result{Type: Null}, true)) + assert(t, Result{Type: False}.Less(Result{Type: True}, true)) + assert(t, Result{Type: String, Str: "abc"}.Less(Result{Type: String, + Str: "bcd"}, true)) + assert(t, Result{Type: String, Str: "ABC"}.Less(Result{Type: String, + Str: "abc"}, true)) + assert(t, !Result{Type: String, Str: "ABC"}.Less(Result{Type: String, + Str: "abc"}, false)) + assert(t, Result{Type: Number, Num: 123}.Less(Result{Type: Number, + Num: 456}, true)) + assert(t, !Result{Type: Number, Num: 456}.Less(Result{Type: Number, + Num: 123}, true)) + assert(t, !Result{Type: Number, Num: 456}.Less(Result{Type: Number, + Num: 456}, true)) + assert(t, stringLessInsensitive("abcde", "BBCDE")) + assert(t, stringLessInsensitive("abcde", "bBCDE")) + assert(t, stringLessInsensitive("Abcde", "BBCDE")) + assert(t, stringLessInsensitive("Abcde", "bBCDE")) + assert(t, !stringLessInsensitive("bbcde", "aBCDE")) + assert(t, !stringLessInsensitive("bbcde", "ABCDE")) + assert(t, !stringLessInsensitive("Bbcde", "aBCDE")) + assert(t, !stringLessInsensitive("Bbcde", "ABCDE")) + assert(t, !stringLessInsensitive("abcde", "ABCDE")) + assert(t, !stringLessInsensitive("Abcde", "ABCDE")) + assert(t, !stringLessInsensitive("abcde", "ABCDE")) + assert(t, !stringLessInsensitive("ABCDE", "ABCDE")) + assert(t, !stringLessInsensitive("abcde", "abcde")) + assert(t, !stringLessInsensitive("123abcde", "123Abcde")) + assert(t, !stringLessInsensitive("123Abcde", "123Abcde")) + assert(t, !stringLessInsensitive("123Abcde", "123abcde")) + assert(t, !stringLessInsensitive("123abcde", "123abcde")) + assert(t, !stringLessInsensitive("124abcde", "123abcde")) + assert(t, !stringLessInsensitive("124Abcde", "123Abcde")) + assert(t, !stringLessInsensitive("124Abcde", "123abcde")) + assert(t, !stringLessInsensitive("124abcde", "123abcde")) + assert(t, stringLessInsensitive("124abcde", "125abcde")) + assert(t, stringLessInsensitive("124Abcde", "125Abcde")) + assert(t, stringLessInsensitive("124Abcde", "125abcde")) + assert(t, stringLessInsensitive("124abcde", "125abcde")) +} + +func TestIssue6(t *testing.T) { + data := `{ + "code": 0, + "msg": "", + "data": { + "sz002024": { + "qfqday": [ + [ + "2014-01-02", + "8.93", + "9.03", + "9.17", + "8.88", + "621143.00" + ], + [ + "2014-01-03", + "9.03", + "9.30", + "9.47", + "8.98", + "1624438.00" + ] + ] + } + } + }` + + var num []string + for _, v := range Get(data, "data.sz002024.qfqday.0").Array() { + num = append(num, v.String()) + } + if fmt.Sprintf("%v", num) != "[2014-01-02 8.93 9.03 9.17 8.88 621143.00]" { + t.Fatalf("invalid result") + } +} + +var exampleJSON = `{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +}` + +func TestNewParse(t *testing.T) { + //fmt.Printf("%v\n", parse2(exampleJSON, "widget").String()) +} + +func TestUnmarshalMap(t *testing.T) { + var m1 = Parse(exampleJSON).Value().(map[string]interface{}) + var m2 map[string]interface{} + if err := json.Unmarshal([]byte(exampleJSON), &m2); err != nil { + t.Fatal(err) + } + b1, err := json.Marshal(m1) + if err != nil { + t.Fatal(err) + } + b2, err := json.Marshal(m2) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(b1, b2) != 0 { + t.Fatal("b1 != b2") + } +} + +func TestSingleArrayValue(t *testing.T) { + var json = `{"key": "value","key2":[1,2,3,4,"A"]}` + var result = Get(json, "key") + var array = result.Array() + if len(array) != 1 { + t.Fatal("array is empty") + } + if array[0].String() != "value" { + t.Fatalf("got %s, should be %s", array[0].String(), "value") + } + + array = Get(json, "key2.#").Array() + if len(array) != 1 { + t.Fatalf("got '%v', expected '%v'", len(array), 1) + } + + array = Get(json, "key3").Array() + if len(array) != 0 { + t.Fatalf("got '%v', expected '%v'", len(array), 0) + } + +} + +var manyJSON = ` { + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{ + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{ + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{ + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{ + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{ + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{ + "a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"hello":"world" + }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} + "position":{"type":"Point","coordinates":[-115.24,33.09]}, + "loves":["world peace"], + "name":{"last":"Anderson","first":"Nancy"}, + "age":31 + "":{"a":"emptya","b":"emptyb"}, + "name.last":"Yellow", + "name.first":"Cat", +}` + +func combine(results []Result) string { + return fmt.Sprintf("%v", results) +} +func TestManyBasic(t *testing.T) { + testWatchForFallback = true + defer func() { + testWatchForFallback = false + }() + testMany := func(shouldFallback bool, expect string, paths ...string) { + results := GetManyBytes( + []byte(manyJSON), + paths..., + ) + if len(results) != len(paths) { + t.Fatalf("expected %v, got %v", len(paths), len(results)) + } + if fmt.Sprintf("%v", results) != expect { + fmt.Printf("%v\n", paths) + t.Fatalf("expected %v, got %v", expect, results) + } + //if testLastWasFallback != shouldFallback { + // t.Fatalf("expected %v, got %v", shouldFallback, testLastWasFallback) + //} + } + testMany(false, "[Point]", "position.type") + testMany(false, `[emptya ["world peace"] 31]`, ".a", "loves", "age") + testMany(false, `[["world peace"]]`, "loves") + testMany(false, `[{"last":"Anderson","first":"Nancy"} Nancy]`, "name", + "name.first") + testMany(true, `[]`, strings.Repeat("a.", 40)+"hello") + res := Get(manyJSON, strings.Repeat("a.", 48)+"a") + testMany(true, `[`+res.String()+`]`, strings.Repeat("a.", 48)+"a") + // these should fallback + testMany(true, `[Cat Nancy]`, "name\\.first", "name.first") + testMany(true, `[world]`, strings.Repeat("a.", 70)+"hello") +} +func testMany(t *testing.T, json string, paths, expected []string) { + testManyAny(t, json, paths, expected, true) + testManyAny(t, json, paths, expected, false) +} +func testManyAny(t *testing.T, json string, paths, expected []string, + bytes bool) { + var result []Result + for i := 0; i < 2; i++ { + var which string + if i == 0 { + which = "Get" + result = nil + for j := 0; j < len(expected); j++ { + if bytes { + result = append(result, GetBytes([]byte(json), paths[j])) + } else { + result = append(result, Get(json, paths[j])) + } + } + } else if i == 1 { + which = "GetMany" + if bytes { + result = GetManyBytes([]byte(json), paths...) + } else { + result = GetMany(json, paths...) + } + } + for j := 0; j < len(expected); j++ { + if result[j].String() != expected[j] { + t.Fatalf("Using key '%s' for '%s'\nexpected '%v', got '%v'", + paths[j], which, expected[j], result[j].String()) + } + } + } +} +func TestIssue20(t *testing.T) { + json := `{ "name": "FirstName", "name1": "FirstName1", ` + + `"address": "address1", "addressDetails": "address2", }` + paths := []string{"name", "name1", "address", "addressDetails"} + expected := []string{"FirstName", "FirstName1", "address1", "address2"} + t.Run("SingleMany", func(t *testing.T) { + testMany(t, json, paths, + expected) + }) +} + +func TestIssue21(t *testing.T) { + json := `{ "Level1Field1":3, + "Level1Field4":4, + "Level1Field2":{ "Level2Field1":[ "value1", "value2" ], + "Level2Field2":{ "Level3Field1":[ { "key1":"value1" } ] } } }` + paths := []string{"Level1Field1", "Level1Field2.Level2Field1", + "Level1Field2.Level2Field2.Level3Field1", "Level1Field4"} + expected := []string{"3", `[ "value1", "value2" ]`, + `[ { "key1":"value1" } ]`, "4"} + t.Run("SingleMany", func(t *testing.T) { + testMany(t, json, paths, + expected) + }) +} + +func TestRandomMany(t *testing.T) { + var lstr string + defer func() { + if v := recover(); v != nil { + println("'" + hex.EncodeToString([]byte(lstr)) + "'") + println("'" + lstr + "'") + panic(v) + } + }() + rand.Seed(time.Now().UnixNano()) + b := make([]byte, 512) + for i := 0; i < 50000; i++ { + n, err := rand.Read(b[:rand.Int()%len(b)]) + if err != nil { + t.Fatal(err) + } + lstr = string(b[:n]) + paths := make([]string, rand.Int()%64) + for i := range paths { + var b []byte + n := rand.Int() % 5 + for j := 0; j < n; j++ { + if j > 0 { + b = append(b, '.') + } + nn := rand.Int() % 10 + for k := 0; k < nn; k++ { + b = append(b, 'a'+byte(rand.Int()%26)) + } + } + paths[i] = string(b) + } + GetMany(lstr, paths...) + } +} + +type ComplicatedType struct { + unsettable int + Tagged string `json:"tagged"` + NotTagged bool + Nested struct { + Yellow string `json:"yellow"` + } + NestedTagged struct { + Green string + Map map[string]interface{} + Ints struct { + Int int `json:"int"` + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 `json:"int64"` + } + Uints struct { + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + } + Floats struct { + Float64 float64 + Float32 float32 + } + Byte byte + Bool bool + } `json:"nestedTagged"` + LeftOut string `json:"-"` + SelfPtr *ComplicatedType + SelfSlice []ComplicatedType + SelfSlicePtr []*ComplicatedType + SelfPtrSlice *[]ComplicatedType + Interface interface{} `json:"interface"` + Array [3]int + Time time.Time `json:"time"` + Binary []byte + NonBinary []byte +} + +var complicatedJSON = ` +{ + "tagged": "OK", + "Tagged": "KO", + "NotTagged": true, + "unsettable": 101, + "Nested": { + "Yellow": "Green", + "yellow": "yellow" + }, + "nestedTagged": { + "Green": "Green", + "Map": { + "this": "that", + "and": "the other thing" + }, + "Ints": { + "Uint": 99, + "Uint16": 16, + "Uint32": 32, + "Uint64": 65 + }, + "Uints": { + "int": -99, + "Int": -98, + "Int16": -16, + "Int32": -32, + "int64": -64, + "Int64": -65 + }, + "Uints": { + "Float32": 32.32, + "Float64": 64.64 + }, + "Byte": 254, + "Bool": true + }, + "LeftOut": "you shouldn't be here", + "SelfPtr": {"tagged":"OK","nestedTagged":{"Ints":{"Uint32":32}}}, + "SelfSlice": [{"tagged":"OK","nestedTagged":{"Ints":{"Uint32":32}}}], + "SelfSlicePtr": [{"tagged":"OK","nestedTagged":{"Ints":{"Uint32":32}}}], + "SelfPtrSlice": [{"tagged":"OK","nestedTagged":{"Ints":{"Uint32":32}}}], + "interface": "Tile38 Rocks!", + "Interface": "Please Download", + "Array": [0,2,3,4,5], + "time": "2017-05-07T13:24:43-07:00", + "Binary": "R0lGODlhPQBEAPeo", + "NonBinary": [9,3,100,115] +} +` + +func testvalid(t *testing.T, json string, expect bool) { + t.Helper() + _, ok := validpayload([]byte(json), 0) + if ok != expect { + t.Fatal("mismatch") + } +} + +func TestValidBasic(t *testing.T) { + testvalid(t, "0", true) + testvalid(t, "00", false) + testvalid(t, "-00", false) + testvalid(t, "-.", false) + testvalid(t, "0.0", true) + testvalid(t, "10.0", true) + testvalid(t, "10e1", true) + testvalid(t, "10EE", false) + testvalid(t, "10E-", false) + testvalid(t, "10E+", false) + testvalid(t, "10E123", true) + testvalid(t, "10E-123", true) + testvalid(t, "10E-0123", true) + testvalid(t, "", false) + testvalid(t, " ", false) + testvalid(t, "{}", true) + testvalid(t, "{", false) + testvalid(t, "-", false) + testvalid(t, "-1", true) + testvalid(t, "-1.", false) + testvalid(t, "-1.0", true) + testvalid(t, " -1.0", true) + testvalid(t, " -1.0 ", true) + testvalid(t, "-1.0 ", true) + testvalid(t, "-1.0 i", false) + testvalid(t, "-1.0 i", false) + testvalid(t, "true", true) + testvalid(t, " true", true) + testvalid(t, " true ", true) + testvalid(t, " True ", false) + testvalid(t, " tru", false) + testvalid(t, "false", true) + testvalid(t, " false", true) + testvalid(t, " false ", true) + testvalid(t, " False ", false) + testvalid(t, " fals", false) + testvalid(t, "null", true) + testvalid(t, " null", true) + testvalid(t, " null ", true) + testvalid(t, " Null ", false) + testvalid(t, " nul", false) + testvalid(t, " []", true) + testvalid(t, " [true]", true) + testvalid(t, " [ true, null ]", true) + testvalid(t, " [ true,]", false) + testvalid(t, `{"hello":"world"}`, true) + testvalid(t, `{ "hello": "world" }`, true) + testvalid(t, `{ "hello": "world", }`, false) + testvalid(t, `{"a":"b",}`, false) + testvalid(t, `{"a":"b","a"}`, false) + testvalid(t, `{"a":"b","a":}`, false) + testvalid(t, `{"a":"b","a":1}`, true) + testvalid(t, `{"a":"b",2"1":2}`, false) + testvalid(t, `{"a":"b","a": 1, "c":{"hi":"there"} }`, true) + testvalid(t, `{"a":"b","a": 1, "c":{"hi":"there", "easy":["going",`+ + `{"mixed":"bag"}]} }`, true) + testvalid(t, `""`, true) + testvalid(t, `"`, false) + testvalid(t, `"\n"`, true) + testvalid(t, `"\"`, false) + testvalid(t, `"\\"`, true) + testvalid(t, `"a\\b"`, true) + testvalid(t, `"a\\b\\\"a"`, true) + testvalid(t, `"a\\b\\\uFFAAa"`, true) + testvalid(t, `"a\\b\\\uFFAZa"`, false) + testvalid(t, `"a\\b\\\uFFA"`, false) + testvalid(t, string(complicatedJSON), true) + testvalid(t, string(exampleJSON), true) +} + +var jsonchars = []string{"{", "[", ",", ":", "}", "]", "1", "0", "true", + "false", "null", `""`, `"\""`, `"a"`} + +func makeRandomJSONChars(b []byte) { + var bb []byte + for len(bb) < len(b) { + bb = append(bb, jsonchars[rand.Int()%len(jsonchars)]...) + } + copy(b, bb[:len(b)]) +} + +func TestValidRandom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + b := make([]byte, 100000) + start := time.Now() + for time.Since(start) < time.Second*3 { + n := rand.Int() % len(b) + rand.Read(b[:n]) + validpayload(b[:n], 0) + } + + start = time.Now() + for time.Since(start) < time.Second*3 { + n := rand.Int() % len(b) + makeRandomJSONChars(b[:n]) + 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) + } + +} + +func TestNumUint64String(t *testing.T) { + var i int64 = 9007199254740993 //2^53 + 1 + j := fmt.Sprintf(`{"data": [ %d, "hello" ] }`, i) + res := Get(j, "data.0") + if res.String() != "9007199254740993" { + t.Fatalf("expected '%v', got '%v'", "9007199254740993", res.String()) + } +} + +func TestNumInt64String(t *testing.T) { + var i int64 = -9007199254740993 + j := fmt.Sprintf(`{"data":[ "hello", %d ]}`, i) + res := Get(j, "data.1") + if res.String() != "-9007199254740993" { + t.Fatalf("expected '%v', got '%v'", "-9007199254740993", res.String()) + } +} + +func TestNumBigString(t *testing.T) { + i := "900719925474099301239109123101" // very big + j := fmt.Sprintf(`{"data":[ "hello", "%s" ]}`, i) + res := Get(j, "data.1") + if res.String() != "900719925474099301239109123101" { + t.Fatalf("expected '%v', got '%v'", "900719925474099301239109123101", + res.String()) + } +} + +func TestNumFloatString(t *testing.T) { + var i int64 = -9007199254740993 + j := fmt.Sprintf(`{"data":[ "hello", %d ]}`, i) //No quotes around value!! + res := Get(j, "data.1") + if res.String() != "-9007199254740993" { + t.Fatalf("expected '%v', got '%v'", "-9007199254740993", res.String()) + } +} + +func TestDuplicateKeys(t *testing.T) { + // this is vaild json according to the JSON spec + var json = `{"name": "Alex","name": "Peter"}` + if Parse(json).Get("name").String() != + Parse(json).Map()["name"].String() { + t.Fatalf("expected '%v', got '%v'", + Parse(json).Get("name").String(), + Parse(json).Map()["name"].String(), + ) + } + if !Valid(json) { + t.Fatal("should be valid") + } +} + +func TestArrayValues(t *testing.T) { + var json = `{"array": ["PERSON1","PERSON2",0],}` + values := Get(json, "array").Array() + var output string + for i, val := range values { + if i > 0 { + output += "\n" + } + output += fmt.Sprintf("%#v", val) + } + expect := strings.Join([]string{ + `gjson.Result{Type:3, Raw:"\"PERSON1\"", Str:"PERSON1", Num:0, ` + + `Index:0}`, + `gjson.Result{Type:3, Raw:"\"PERSON2\"", Str:"PERSON2", Num:0, ` + + `Index:0}`, + `gjson.Result{Type:2, Raw:"0", Str:"", Num:0, Index:0}`, + }, "\n") + if output != expect { + t.Fatalf("expected '%v', got '%v'", expect, output) + } + +} + +func BenchmarkValid(b *testing.B) { + for i := 0; i < b.N; i++ { + Valid(complicatedJSON) + } +} + +func BenchmarkValidBytes(b *testing.B) { + complicatedJSON := []byte(complicatedJSON) + for i := 0; i < b.N; i++ { + ValidBytes(complicatedJSON) + } +} + +func BenchmarkGoStdlibValidBytes(b *testing.B) { + complicatedJSON := []byte(complicatedJSON) + for i := 0; i < b.N; i++ { + json.Valid(complicatedJSON) + } +} + +func TestModifier(t *testing.T) { + json := `{"other":{"hello":"world"},"arr":[1,2,3,4,5,6]}` + opts := *pretty.DefaultOptions + opts.SortKeys = true + exp := string(pretty.PrettyOptions([]byte(json), &opts)) + res := Get(json, `@pretty:{"sortKeys":true}`).String() + if res != exp { + t.Fatalf("expected '%v', got '%v'", exp, res) + } + res = Get(res, "@pretty|@reverse|@ugly").String() + if res != json { + t.Fatalf("expected '%v', got '%v'", json, res) + } + if res := Get(res, "@this").String(); res != json { + t.Fatalf("expected '%v', got '%v'", json, res) + } + if res := Get(res, "other.@this").String(); res != `{"hello":"world"}` { + t.Fatalf("expected '%v', got '%v'", json, res) + } + res = Get(res, "@pretty|@reverse|arr|@reverse|2").String() + if res != "4" { + t.Fatalf("expected '%v', got '%v'", "4", res) + } + AddModifier("case", func(json, arg string) string { + if arg == "upper" { + return strings.ToUpper(json) + } + if arg == "lower" { + return strings.ToLower(json) + } + return json + }) + res = Get(json, "other|@case:upper").String() + if res != `{"HELLO":"WORLD"}` { + t.Fatalf("expected '%v', got '%v'", `{"HELLO":"WORLD"}`, res) + } +} + +func TestChaining(t *testing.T) { + json := `{ + "info": { + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44}, + {"first": "Roger", "last": "Craig", "age": 68}, + {"first": "Jane", "last": "Murphy", "age": 47} + ] + } + }` + res := Get(json, "info.friends|0|first").String() + if res != "Dale" { + t.Fatalf("expected '%v', got '%v'", "Dale", res) + } + res = Get(json, "info.friends|@reverse|0|age").String() + if res != "47" { + t.Fatalf("expected '%v', got '%v'", "47", res) + } + res = Get(json, "@ugly|i\\nfo|friends.0.first").String() + if res != "Dale" { + t.Fatalf("expected '%v', got '%v'", "Dale", res) + } +} + +func TestSplitPipe(t *testing.T) { + split := func(t *testing.T, path, el, er string, eo bool) { + t.Helper() + left, right, ok := splitPossiblePipe(path) + // fmt.Printf("%-40s [%v] [%v] [%v]\n", path, left, right, ok) + if left != el || right != er || ok != eo { + t.Fatalf("expected '%v/%v/%v', got '%v/%v/%v", + el, er, eo, left, right, ok) + } + } + + split(t, "hello", "", "", false) + split(t, "hello.world", "", "", false) + split(t, "hello|world", "hello", "world", true) + split(t, "hello\\|world", "", "", false) + split(t, "hello.#", "", "", false) + split(t, `hello.#[a|1="asdf\"|1324"]#\|that`, "", "", false) + split(t, `hello.#[a|1="asdf\"|1324"]#|that.more|yikes`, + `hello.#[a|1="asdf\"|1324"]#`, "that.more|yikes", true) + split(t, `a.#[]#\|b`, "", "", false) + +} + +func TestArrayEx(t *testing.T) { + json := ` + [ + { + "c":[ + {"a":10.11} + ] + }, { + "c":[ + {"a":11.11} + ] + } + ]` + res := Get(json, "@ugly|#.c.#[a=10.11]").String() + if res != `[{"a":10.11}]` { + t.Fatalf("expected '%v', got '%v'", `[{"a":10.11}]`, res) + } + res = Get(json, "@ugly|#.c.#").String() + if res != `[1,1]` { + t.Fatalf("expected '%v', got '%v'", `[1,1]`, res) + } + res = Get(json, "@reverse|0|c|0|a").String() + if res != "11.11" { + t.Fatalf("expected '%v', got '%v'", "11.11", res) + } + res = Get(json, "#.c|#").String() + if res != "2" { + t.Fatalf("expected '%v', got '%v'", "2", res) + } +} + +func TestPipeDotMixing(t *testing.T) { + json := `{ + "info": { + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44}, + {"first": "Roger", "last": "Craig", "age": 68}, + {"first": "Jane", "last": "Murphy", "age": 47} + ] + } + }` + var res string + res = Get(json, `info.friends.#[first="Dale"].last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `info|friends.#[first="Dale"].last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `info|friends.#[first="Dale"]|last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `info|friends|#[first="Dale"]|last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `@ugly|info|friends|#[first="Dale"]|last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `@ugly|info.@ugly|friends|#[first="Dale"]|last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `@ugly.info|@ugly.friends|#[first="Dale"]|last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } +} + +func TestDeepSelectors(t *testing.T) { + json := `{ + "info": { + "friends": [ + { + "first": "Dale", "last": "Murphy", + "extra": [10,20,30], + "details": { + "city": "Tempe", + "state": "Arizona" + } + }, + { + "first": "Roger", "last": "Craig", + "extra": [40,50,60], + "details": { + "city": "Phoenix", + "state": "Arizona" + } + } + ] + } + }` + var res string + res = Get(json, `info.friends.#[first="Dale"].extra.0`).String() + if res != "10" { + t.Fatalf("expected '%v', got '%v'", "10", res) + } + res = Get(json, `info.friends.#[first="Dale"].extra|0`).String() + if res != "10" { + t.Fatalf("expected '%v', got '%v'", "10", res) + } + res = Get(json, `info.friends.#[first="Dale"]|extra|0`).String() + if res != "10" { + t.Fatalf("expected '%v', got '%v'", "10", res) + } + res = Get(json, `info.friends.#[details.city="Tempe"].last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } + res = Get(json, `info.friends.#[details.city="Phoenix"].last`).String() + if res != "Craig" { + t.Fatalf("expected '%v', got '%v'", "Craig", res) + } + res = Get(json, `info.friends.#[details.state="Arizona"].last`).String() + if res != "Murphy" { + t.Fatalf("expected '%v', got '%v'", "Murphy", res) + } +} + +func TestMultiArrayEx(t *testing.T) { + json := `{ + "info": { + "friends": [ + { + "first": "Dale", "last": "Murphy", "kind": "Person", + "cust1": true, + "extra": [10,20,30], + "details": { + "city": "Tempe", + "state": "Arizona" + } + }, + { + "first": "Roger", "last": "Craig", "kind": "Person", + "cust2": false, + "extra": [40,50,60], + "details": { + "city": "Phoenix", + "state": "Arizona" + } + } + ] + } + }` + + var res string + + res = Get(json, `info.friends.#[kind="Person"]#.kind|0`).String() + if res != "Person" { + t.Fatalf("expected '%v', got '%v'", "Person", res) + } + res = Get(json, `info.friends.#.kind|0`).String() + if res != "Person" { + t.Fatalf("expected '%v', got '%v'", "Person", res) + } + + res = Get(json, `info.friends.#[kind="Person"]#.kind`).String() + if res != `["Person","Person"]` { + t.Fatalf("expected '%v', got '%v'", `["Person","Person"]`, res) + } + res = Get(json, `info.friends.#.kind`).String() + if res != `["Person","Person"]` { + t.Fatalf("expected '%v', got '%v'", `["Person","Person"]`, res) + } + + res = Get(json, `info.friends.#[kind="Person"]#|kind`).String() + if res != `` { + t.Fatalf("expected '%v', got '%v'", ``, res) + } + res = Get(json, `info.friends.#|kind`).String() + if res != `` { + t.Fatalf("expected '%v', got '%v'", ``, res) + } + + res = Get(json, `i*.f*.#[kind="Other"]#`).String() + if res != `[]` { + t.Fatalf("expected '%v', got '%v'", `[]`, res) + } +} + +func TestQueries(t *testing.T) { + json := `{ + "info": { + "friends": [ + { + "first": "Dale", "last": "Murphy", "kind": "Person", + "cust1": true, + "extra": [10,20,30], + "details": { + "city": "Tempe", + "state": "Arizona" + } + }, + { + "first": "Roger", "last": "Craig", "kind": "Person", + "cust2": false, + "extra": [40,50,60], + "details": { + "city": "Phoenix", + "state": "Arizona" + } + } + ] + } + }` + + // numbers + assert(t, Get(json, "i*.f*.#[extra.0<11].first").Exists()) + assert(t, Get(json, "i*.f*.#[extra.0<=11].first").Exists()) + assert(t, !Get(json, "i*.f*.#[extra.0<10].first").Exists()) + assert(t, Get(json, "i*.f*.#[extra.0<=10].first").Exists()) + assert(t, Get(json, "i*.f*.#[extra.0=10].first").Exists()) + assert(t, !Get(json, "i*.f*.#[extra.0=11].first").Exists()) + assert(t, Get(json, "i*.f*.#[extra.0!=10].first").String() == "Roger") + assert(t, Get(json, "i*.f*.#[extra.0>10].first").String() == "Roger") + assert(t, Get(json, "i*.f*.#[extra.0>=10].first").String() == "Dale") + + // strings + assert(t, Get(json, `i*.f*.#[extra.0<"11"].first`).Exists()) + assert(t, Get(json, `i*.f*.#[first>"Dale"].last`).String() == "Craig") + assert(t, Get(json, `i*.f*.#[first>="Dale"].last`).String() == "Murphy") + assert(t, Get(json, `i*.f*.#[first="Dale"].last`).String() == "Murphy") + assert(t, Get(json, `i*.f*.#[first!="Dale"].last`).String() == "Craig") + assert(t, !Get(json, `i*.f*.#[first<"Dale"].last`).Exists()) + assert(t, Get(json, `i*.f*.#[first<="Dale"].last`).Exists()) + assert(t, Get(json, `i*.f*.#[first%"Da*"].last`).Exists()) + assert(t, Get(json, `i*.f*.#[first%"Dale"].last`).Exists()) + assert(t, Get(json, `i*.f*.#[first%"*a*"]#|#`).String() == "1") + assert(t, Get(json, `i*.f*.#[first%"*e*"]#|#`).String() == "2") + assert(t, Get(json, `i*.f*.#[first!%"*e*"]#|#`).String() == "0") + + // trues + assert(t, Get(json, `i*.f*.#[cust1=true].first`).String() == "Dale") + assert(t, Get(json, `i*.f*.#[cust2=false].first`).String() == "Roger") + assert(t, Get(json, `i*.f*.#[cust1!=false].first`).String() == "Dale") + assert(t, Get(json, `i*.f*.#[cust2!=true].first`).String() == "Roger") + assert(t, !Get(json, `i*.f*.#[cust1>true].first`).Exists()) + assert(t, Get(json, `i*.f*.#[cust1>=true].first`).Exists()) + assert(t, !Get(json, `i*.f*.#[cust29)#|#").Int() == 4) + assert(t, Get(json, "friends.#(a>10)#|#").Int() == 3) + assert(t, Get(json, "friends.#(a>40)#|#").Int() == 0) +} + +func TestSubSelectors(t *testing.T) { + json := `{ + "info": { + "friends": [ + { + "first": "Dale", "last": "Murphy", "kind": "Person", + "cust1": true, + "extra": [10,20,30], + "details": { + "city": "Tempe", + "state": "Arizona" + } + }, + { + "first": "Roger", "last": "Craig", "kind": "Person", + "cust2": false, + "extra": [40,50,60], + "details": { + "city": "Phoenix", + "state": "Arizona" + } + } + ] + } + }` + assert(t, Get(json, "[]").String() == "[]") + assert(t, Get(json, "{}").String() == "{}") + res := Get(json, `{`+ + `abc:info.friends.0.first,`+ + `info.friends.1.last,`+ + `"a`+"\r"+`a":info.friends.0.kind,`+ + `"abc":info.friends.1.kind,`+ + `{123:info.friends.1.cust2},`+ + `[info.friends.#[details.city="Phoenix"]#|#]`+ + `}.@pretty.@ugly`).String() + // println(res) + // {"abc":"Dale","last":"Craig","\"a\ra\"":"Person","_":{"123":false},"_":[1]} + assert(t, Get(res, "abc").String() == "Dale") + assert(t, Get(res, "last").String() == "Craig") + assert(t, Get(res, "\"a\ra\"").String() == "Person") + assert(t, Get(res, "@reverse.abc").String() == "Person") + assert(t, Get(res, "_.123").String() == "false") + assert(t, Get(res, "@reverse._.0").String() == "1") + assert(t, Get(json, "info.friends.[0.first,1.extra.0]").String() == + `["Dale",40]`) + assert(t, Get(json, "info.friends.#.[first,extra.0]").String() == + `[["Dale",10],["Roger",40]]`) +} + +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"]`) +} + +func TestSingleModifier(t *testing.T) { + var data = `{"@key": "value"}` + assert(t, Get(data, "@key").String() == "value") + assert(t, Get(data, "\\@key").String() == "value") +} + +func TestModifiersInMultipaths(t *testing.T) { + AddModifier("case", func(json, arg string) string { + if arg == "upper" { + return strings.ToUpper(json) + } + if arg == "lower" { + return strings.ToLower(json) + } + return json + }) + json := `{"friends": [ + {"age": 44, "first": "Dale", "last": "Murphy"}, + {"age": 68, "first": "Roger", "last": "Craig"}, + {"age": 47, "first": "Jane", "last": "Murphy"} + ]}` + + res := Get(json, `friends.#.{age,first|@case:upper}|@ugly`) + exp := `[{"age":44,"@case:upper":"DALE"},{"age":68,"@case:upper":"ROGER"},{"age":47,"@case:upper":"JANE"}]` + assert(t, res.Raw == exp) + + res = Get(json, `{friends.#.{age,first:first|@case:upper}|0.first}`) + exp = `{"first":"DALE"}` + assert(t, res.Raw == exp) + +} + +func TestIssue141(t *testing.T) { + json := `{"data": [{"q": 11, "w": 12}, {"q": 21, "w": 22}, {"q": 31, "w": 32} ], "sql": "some stuff here"}` + assert(t, Get(json, "data.#").Int() == 3) + assert(t, Get(json, "data.#.{q}|@ugly").Raw == `[{"q":11},{"q":21},{"q":31}]`) + assert(t, Get(json, "data.#.q|@ugly").Raw == `[11,21,31]`) +} + +func TestChainedModifierStringArgs(t *testing.T) { + // issue #143 + AddModifier("push", func(json, arg string) string { + json = strings.TrimSpace(json) + if len(json) < 2 || !Parse(json).IsArray() { + return json + } + json = strings.TrimSpace(json[1 : len(json)-1]) + if len(json) == 0 { + return "[" + arg + "]" + } + return "[" + json + "," + arg + "]" + }) + res := Get("[]", `@push:"2"|@push:"3"|@push:{"a":"b","c":["e","f"]}|@push:true|@push:10.23`) + assert(t, res.String() == `["2","3",{"a":"b","c":["e","f"]},true,10.23]`) +} + +func TestFlatten(t *testing.T) { + json := `[1,[2],[3,4],[5,[6,[7]]],{"hi":"there"},8,[9]]` + assert(t, Get(json, "@flatten").String() == `[1,2,3,4,5,[6,[7]],{"hi":"there"},8,9]`) + assert(t, Get(json, `@flatten:{"deep":true}`).String() == `[1,2,3,4,5,6,7,{"hi":"there"},8,9]`) + assert(t, Get(`{"9999":1234}`, "@flatten").String() == `{"9999":1234}`) +} + +func TestJoin(t *testing.T) { + assert(t, Get(`[{},{}]`, "@join").String() == `{}`) + assert(t, Get(`[{"a":1},{"b":2}]`, "@join").String() == `{"a":1,"b":2}`) + assert(t, Get(`[{"a":1,"b":1},{"b":2}]`, "@join").String() == `{"a":1,"b":2}`) + assert(t, Get(`[{"a":1,"b":1},{"b":2},5,{"c":3}]`, "@join").String() == `{"a":1,"b":2,"c":3}`) + assert(t, Get(`[{"a":1,"b":1},{"b":2},5,{"c":3}]`, `@join:{"preserve":true}`).String() == `{"a":1,"b":1,"b":2,"c":3}`) + assert(t, Get(`[{"a":1,"b":1},{"b":2},5,{"c":3}]`, `@join:{"preserve":true}.b`).String() == `1`) + assert(t, Get(`{"9999":1234}`, "@join").String() == `{"9999":1234}`) +} + +func TestValid(t *testing.T) { + assert(t, Get("[{}", "@valid").Exists() == false) + assert(t, Get("[{}]", "@valid").Exists() == true) +} + +// https://github.com/tidwall/gjson/issues/152 +func TestJoin152(t *testing.T) { + var json = `{ + "distance": 1374.0, + "validFrom": "2005-11-14", + "historical": { + "type": "Day", + "name": "last25Hours", + "summary": { + "units": { + "temperature": "C", + "wind": "m/s", + "snow": "cm", + "precipitation": "mm" + }, + "days": [ + { + "time": "2020-02-08", + "hours": [ + { + "temperature": { + "min": -2.0, + "max": -1.6, + "value": -1.6 + }, + "wind": {}, + "precipitation": {}, + "humidity": { + "value": 92.0 + }, + "snow": { + "depth": 49.0 + }, + "time": "2020-02-08T16:00:00+01:00" + }, + { + "temperature": { + "min": -1.7, + "max": -1.3, + "value": -1.3 + }, + "wind": {}, + "precipitation": {}, + "humidity": { + "value": 92.0 + }, + "snow": { + "depth": 49.0 + }, + "time": "2020-02-08T17:00:00+01:00" + }, + { + "temperature": { + "min": -1.3, + "max": -0.9, + "value": -1.2 + }, + "wind": {}, + "precipitation": {}, + "humidity": { + "value": 91.0 + }, + "snow": { + "depth": 49.0 + }, + "time": "2020-02-08T18:00:00+01:00" + } + ] + }, + { + "time": "2020-02-09", + "hours": [ + { + "temperature": { + "min": -1.7, + "max": -0.9, + "value": -1.5 + }, + "wind": {}, + "precipitation": {}, + "humidity": { + "value": 91.0 + }, + "snow": { + "depth": 49.0 + }, + "time": "2020-02-09T00:00:00+01:00" + }, + { + "temperature": { + "min": -1.5, + "max": 0.9, + "value": 0.2 + }, + "wind": {}, + "precipitation": {}, + "humidity": { + "value": 67.0 + }, + "snow": { + "depth": 49.0 + }, + "time": "2020-02-09T01:00:00+01:00" + } + ] + } + ] + } + } + }` + + res := Get(json, "historical.summary.days.#.hours|@flatten|#.humidity.value") + assert(t, res.Raw == `[92.0,92.0,91.0,91.0,67.0]`) +} diff --git a/gjson/go.mod b/gjson/go.mod new file mode 100644 index 000000000..d851688cc --- /dev/null +++ b/gjson/go.mod @@ -0,0 +1,8 @@ +module github.com/tidwall/gjson + +go 1.12 + +require ( + github.com/tidwall/match v1.0.1 + github.com/tidwall/pretty v1.0.0 +) diff --git a/gjson/go.sum b/gjson/go.sum new file mode 100644 index 000000000..a4a2d872c --- /dev/null +++ b/gjson/go.sum @@ -0,0 +1,4 @@ +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= diff --git a/gjson/logo.png b/gjson/logo.png new file mode 100644 index 000000000..17a8bbe9d Binary files /dev/null and b/gjson/logo.png differ diff --git a/go.mod b/go.mod index 4c5f4d056..b17db0cbb 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ replace github.com/lib/pq => github.com/matrix-org/pq v1.3.2 replace github.com/prometheus/client_golang => ./prometheus +replace github.com/tidwall/gjson => ./gjson + require ( github.com/gorilla/mux v1.7.3 github.com/lib/pq v1.2.0 @@ -20,6 +22,7 @@ require ( github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.4.1 github.com/sirupsen/logrus v1.4.2 + github.com/tidwall/pretty v1.0.1 // indirect github.com/uber/jaeger-client-go v2.22.1+incompatible github.com/uber/jaeger-lib v2.2.0+incompatible golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 diff --git a/go.sum b/go.sum index b66734378..ee7554a1b 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,9 @@ github.com/tidwall/gjson v1.1.5 h1:QysILxBeUEY3GTLA0fQVgkQG1zme8NxGvhh2SSqWNwI= github.com/tidwall/gjson v1.1.5/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= +github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/sjson v1.0.3 h1:DeF+0LZqvIt4fKYw41aPB29ZGlvwVkHKktoXJ1YW9Y8= github.com/tidwall/sjson v1.0.3/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= github.com/uber-go/atomic v1.3.0 h1:ylWoWcs+jXihgo3Us1Sdsatf2R6+OlBGm8fexR3oFG4=