commit f4afb106da47e3872a69a2b01c4636177056f557 Author: Josh Baker Date: Wed Aug 10 20:07:45 2016 -0700 first commit diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4f2ee4d --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..58f5819 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..c260fee --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +

+GJSON +
+Build Status +GoDoc +

+ +

get a json value quickly

+ +GJSON is a Go package the provides a [very fast](#performance) and simple way to get a value from a json document. The reason for this library it to give efficent json indexing for the [BuntDB](https://github.com/tidwall/buntdb) project. + +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". 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. 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 +``` + +A path is a series of keys seperated 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 use the '#' character. + +``` +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"] +} +"name.last" >> "Anderson" +"age" >> 37 +"children.#" >> 3 +"children.1" >> "Alex" +"child*.2" >> "Jack" +"c?ildren.0" >> "Sara" +``` + + +## 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 types: + +``` +bool, for JSON booleans +float64, for JSON numbers +Number, for JSON numbers +string, for JSON string literals +nil, for JSON null +``` + +To get the value call the `Value()` method: + + +```go +result.Value() // interface{} which may be nil, string, float64, or bool + +// Or just get the value in one step. +gjson.Get(json, "name.last").Value() +``` + +To directly access the value from its original type: + +```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 +``` + +## Performance + +Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), [ffjson](https://github.com/pquerna/ffjson), and [EasyJSON](https://github.com/mailru/easyjson). + +``` +BenchmarkGJSONGet-8 3000000 477 ns/op 0 B/op 0 allocs/op +BenchmarkJSONUnmarshalMap-8 600000 10738 ns/op 3176 B/op 69 allocs/op +BenchmarkJSONUnmarshalStruct-8 600000 11635 ns/op 1960 B/op 69 allocs/op +BenchmarkJSONDecoder-8 300000 17193 ns/op 4864 B/op 184 allocs/op +BenchmarkFFJSONLexer-8 1500000 3773 ns/op 1024 B/op 8 allocs/op +BenchmarkEasyJSONLexer-8 3000000 1134 ns/op 741 B/op 6 allocs/op +``` + +The JSON document used was: + +```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 are the results from running the benchmarks on a MacBook Pro 15" 2.8 GHz Intel Core i7:* + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +GJSON source code is available under the MIT [License](/LICENSE). diff --git a/gjson.go b/gjson.go new file mode 100644 index 0000000..1d9045c --- /dev/null +++ b/gjson.go @@ -0,0 +1,591 @@ +// Package gjson provides searching for json strings. +package gjson + +import "strconv" + +// Type is Result type +type Type byte + +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 +) + +// 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 +} + +// String returns a string representation of the value. +func (t Result) String() string { + switch t.Type { + default: + return "null" + case False: + return "false" + case Number: + return strconv.FormatFloat(t.Num, 'f', -1, 64) + case String: + return t.Str + case JSON: + return t.Raw + case True: + return "true" + } +} + +// 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 +// +func (t Result) Value() interface{} { + switch t.Type { + default: + return nil + case False: + return false + case Number: + return t.Num + case String: + return t.Str + case JSON: + return t.Raw + case True: + return true + } +} + +// Get searches json for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// 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. +// When the value is found it's returned immediately. +// +// A path is a series of keys seperated 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 use the '#' character. +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children.#" >> 3 +// "children.1" >> "Alex" +// "child*.2" >> "Jack" +// "c?ildren.0" >> "Sara" +// +func Get(json string, path string) Result { + var i, s, depth int + var squashed string + var key string + var stype byte + var count int + var wild bool + var matched bool + var parts = make([]string, 0, 4) + var wilds = make([]bool, 0, 4) + var keys = make([]string, 0, 4) + var stypes = make([]byte, 0, 4) + var counts = make([]int, 0, 4) + + // do nothing when no path specified + if len(path) == 0 { + return Result{} // nothing + } + + depth = 1 + + // look for first delimiter + for ; i < len(json); i++ { + if json[i] > ' ' { + if json[i] == '{' { + stype = '{' + } else if json[i] == '[' { + stype = '[' + } else { + // not a valid type + return Result{} + } + i++ + break + } + } + + stypes = append(stypes, stype) + counts = append(counts, count) + + // parse the path. just split on the dot + for i := 0; i < len(path); i++ { + if path[i] == '.' { + parts = append(parts, path[s:i]) + wilds = append(wilds, wild) + if wild { + wild = false + } + s = i + 1 + } else if path[i] == '*' || path[i] == '?' { + wild = true + } + } + parts = append(parts, path[s:]) + wilds = append(wilds, wild) + + // search for key +read_key: + if stype == '[' { + key = strconv.FormatInt(int64(count), 10) + count++ + } else { + for ; i < len(json); i++ { + if json[i] == '"' { + //read to end of key + i++ + // readstr + // the first double-quote has already been read + s = i + for ; i < len(json); i++ { + if json[i] == '"' { + key = json[s:i] + i++ + break + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + key = unescape(json[s:i]) + i++ + break + } + } + break + } + } + } + // end readstr + + // we have a brand new key. + // is it the key that we are looking for? + if wilds[depth-1] { + // it's a wildcard path element + matched = wildcardMatch(key, parts[depth-1]) + } else { + matched = parts[depth-1] == key + } + + // read to the value token + // there's likely a colon here, but who cares. just burn past it. + var val string + var vc byte + for ; i < len(json); i++ { + switch json[i] { + case 't', 'f', 'n': // true, false, null + vc = json[i] + s = i + i++ + for ; i < len(json); i++ { + // let's pick up any character. it doesn't matter. + if json[i] < 'a' || json[i] > 'z' { + break + } + } + val = json[s:i] + goto proc_val + case '{': // open object + i++ + vc = '{' + goto proc_delim + case '[': // open array + i++ + vc = '[' + goto proc_delim + case '"': // string + i++ + // we read the val below + vc = '"' + goto proc_val + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': // number + vc = '0' + s = i + i++ + // look for characters that cannot be in a number + for ; i < len(json); i++ { + switch json[i] { + default: + continue + case ' ', '\t', '\r', '\n', ',', ']', '}': + } + break + } + val = json[s:i] + goto proc_val + } + } + + // sanity check before we move on + if i >= len(json) { + return Result{} + } + +proc_delim: + if (matched && depth == len(parts)) || !matched { + // -- BEGIN SQUASH -- // + // squash the value, ignoring all nested arrays and objects. + s = i - 1 + // the first '[' or '{' has already been read + depth := 1 + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + depth++ + } else if json[i] == '}' || json[i] == ']' { + depth-- + if depth == 0 { + i++ + break + } + } else if json[i] == '"' { + i++ + s2 := i + for ; i < len(json); i++ { + 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 i == len(json) { + break + } + } + } + squashed = json[s:i] + // -- END SQUASH -- // + } + + // process the value +proc_val: + if matched { + // hit, that's good! + if depth == len(parts) { + var value Result + value.Raw = val + switch vc { + case '{', '[': + value.Raw = squashed + value.Type = JSON + case 'n': + value.Type = Null + case 't': + value.Type = True + case 'f': + value.Type = False + case '"': + value.Type = String + // readstr + // the val has not been read yet + // the first double-quote has already been read + s = i + for ; i < len(json); i++ { + if json[i] == '"' { + value.Str = json[s:i] + i++ + break + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + value.Str = unescape(json[s:i]) + i++ + break + } + } + // end readstr + case '0': + value.Type = Number + value.Num, _ = strconv.ParseFloat(val, 64) + } + return value + //} else if vc != '{' { + // can only deep search objects + // return Result{} + } else { + stype = vc + keys = append(keys, key) + stypes = append(stypes, stype) + counts = append(counts, count) + depth++ + goto read_key + } + } + if vc == '"' { + // readstr + // the val has not been read yet. we can read and throw away. + // the first double-quote has already been read + s = i + for ; i < len(json); i++ { + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + i++ + // end readstr + } + + // read to the comma or end of object + for ; i < len(json); i++ { + switch json[i] { + case '}', ']': + if parts[depth-1] == "#" { + return Result{Type: Number, Num: float64(count)} + } + // step the stack back + depth-- + if depth == 0 { + return Result{} + } + keys = keys[:len(keys)-1] + stypes = stypes[:len(stypes)-1] + counts = counts[:len(counts)-1] + stype = stypes[len(stypes)-1] + count = counts[len(counts)-1] + case ',': + i++ + goto read_key + } + } + return Result{} +} + +// unescape unescapes a string +func unescape(json string) string { //, error) { + 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 "" //, errors.New("invalid character in string") + case json[i] == '\\': + i++ + if i >= len(json) { + return "" //, errors.New("invalid escape sequence") + } + switch json[i] { + default: + return "" //, errors.New("invalid escape sequence") + 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 "" //, errors.New("invalid escape sequence") + } + i++ + // extract the codepoint + var code int + for j := i; j < i+4; j++ { + switch { + default: + return "" //, errors.New("invalid escape sequence") + case json[j] >= '0' && json[j] <= '9': + code += (int(json[j]) - '0') << uint(12-(j-i)*4) + case json[j] >= 'a' && json[j] <= 'f': + code += (int(json[j]) - 'a' + 10) << uint(12-(j-i)*4) + case json[j] >= 'a' && json[j] <= 'f': + code += (int(json[j]) - 'a' + 10) << uint(12-(j-i)*4) + } + } + str = append(str, []byte(string(code))...) + i += 3 // only 3 because we will increment on the for-loop + } + } + } + return string(str) //, nil +} + +// 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 + } + switch t.Type { + default: + return t.Raw < token.Raw + case String: + if caseSensitive { + return t.Str < token.Str + } + return stringLessInsensitive(t.Str, token.Str) + case Number: + return t.Num < token.Num + } +} + +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) +} + +// wilcardMatch returns true if str matches pattern. This is a very +// simple wildcard match where '*' matches on any number characters +// and '?' matches on any one character. +func wildcardMatch(str, pattern string) bool { + if pattern == "*" { + return true + } + return deepMatch(str, pattern) +} +func deepMatch(str, pattern string) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '?': + if len(str) == 0 { + return false + } + case '*': + return wildcardMatch(str, pattern[1:]) || + (len(str) > 0 && wildcardMatch(str[1:], pattern)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} diff --git a/gjson_test.go b/gjson_test.go new file mode 100644 index 0000000..e652e5a --- /dev/null +++ b/gjson_test.go @@ -0,0 +1,493 @@ +package gjson + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "io" + "math/rand" + "strings" + "testing" + "time" + + "github.com/mailru/easyjson/jlexer" + fflib "github.com/pquerna/ffjson/fflib/v1" +) + +// TestRandomData is a fuzzing test that throughs 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]) + Get(lstr, "zzzz") + } +} + +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") + } + } +} + +// 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, + "escaped\\\"":true, + "arr":["1",2,"3",{"hello":"world"},"4",5], + "vals":[1,2,3,{"sadf":sdf"asdf"}],"name":{"first":"tom","last":null}}` + +func TestBasic(t *testing.T) { + var token Result + + 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" { + 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() != "null" { + t.Fatal("expecting 'null'", "got", token.String()) + } + if token.Value() != nil { + t.Fatal("should be nil") + } + 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().(string) + + if Get(basicJSON, "").Value() != nil { + t.Fatal("should be nil") + } + + if !Get(basicJSON, "escaped\\\"").Value().(bool) { + t.Fatal("could not escape") + } + + Get(basicJSON, "vals.hello") +} + +func TestUnescape(t *testing.T) { + unescape(string([]byte{'\\', '\\', 0})) + unescape(string([]byte{'\\', '/', '\\', 'b', '\\', 'f'})) +} +func assert(t testing.TB, cond bool) { + if !cond { + t.Fatal("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 TestTwitter(t *testing.T) { + data, err := ioutil.ReadFile("twitter.json") + if err != nil { + return + } + token := Get(string(data), "search_metadata.max_id") + if token.Num != 505874924095815700 { + t.Fatalf("expecting %d\n", 505874924095815700) + } + +} +func BenchmarkTwitter(t *testing.B) { + // the twitter.json file must be present + data, err := ioutil.ReadFile("twitter.json") + if err != nil { + return + } + json := string(data) + t.ResetTimer() + for i := 0; i < t.N; i++ { + token := Get(json, "search_metadata.max_id") + if token.Type != Number || token.Raw != "505874924095815700" || token.Num != 505874924095815700 { + t.Fatal("invalid response") + } + } +} +*/ + +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;" + } +}} +` + +type BenchStruct struct { + Widget struct { + Window struct { + Name string `json:"name"` + } `json:"window"` + Image struct { + HOffset int `json:"hOffset"` + } `json:"image"` + Text struct { + OnMouseUp string `json:"onMouseUp"` + } `json:"text"` + } `json:"widget"` +} + +var benchPaths = []string{ + "widget.window.name", + "widget.image.hOffset", + "widget.text.onMouseUp", +} + +func BenchmarkGJSONGet(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + for j := 0; j < len(benchPaths); j++ { + if Get(exampleJSON, benchPaths[j]).Type == Null { + t.Fatal("did not find the value") + } + } + } + t.N *= len(benchPaths) // because we are running against 3 paths +} + +func BenchmarkJSONUnmarshalMap(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + for j := 0; j < len(benchPaths); j++ { + parts := strings.Split(benchPaths[j], ".") + var m map[string]interface{} + if err := json.Unmarshal([]byte(exampleJSON), &m); err != nil { + t.Fatal(err) + } + var v interface{} + for len(parts) > 0 { + part := parts[0] + if len(parts) > 1 { + m = m[part].(map[string]interface{}) + if m == nil { + t.Fatal("did not find the value") + } + } else { + v = m[part] + if v == nil { + t.Fatal("did not find the value") + } + } + parts = parts[1:] + } + } + } + t.N *= len(benchPaths) // because we are running against 3 paths +} + +func BenchmarkJSONUnmarshalStruct(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + for j := 0; j < len(benchPaths); j++ { + var s BenchStruct + if err := json.Unmarshal([]byte(exampleJSON), &s); err != nil { + t.Fatal(err) + } + switch benchPaths[j] { + case "widget.window.name": + if s.Widget.Window.Name == "" { + t.Fatal("did not find the value") + } + case "widget.image.hOffset": + if s.Widget.Image.HOffset == 0 { + t.Fatal("did not find the value") + } + case "widget.text.onMouseUp": + if s.Widget.Text.OnMouseUp == "" { + t.Fatal("did not find the value") + } + } + } + } + t.N *= len(benchPaths) // because we are running against 3 paths +} + +func BenchmarkJSONDecoder(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + for j := 0; j < len(benchPaths); j++ { + dec := json.NewDecoder(bytes.NewBuffer([]byte(exampleJSON))) + var found bool + outer: + for { + tok, err := dec.Token() + if err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + switch v := tok.(type) { + case string: + if found { + // break out once we find the value. + break outer + } + switch benchPaths[j] { + case "widget.window.name": + if v == "name" { + found = true + } + case "widget.image.hOffset": + if v == "hOffset" { + found = true + } + case "widget.text.onMouseUp": + if v == "onMouseUp" { + found = true + } + } + } + } + if !found { + t.Fatal("field not found") + } + } + } + t.N *= len(benchPaths) // because we are running against 3 paths +} + +func BenchmarkFFJSONLexer(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + for j := 0; j < len(benchPaths); j++ { + l := fflib.NewFFLexer([]byte(exampleJSON)) + var found bool + outer: + for { + t := l.Scan() + if t == fflib.FFTok_eof { + break + } + if t == fflib.FFTok_string { + b, _ := l.CaptureField(t) + v := string(b) + if found { + // break out once we find the value. + break outer + } + switch benchPaths[j] { + case "widget.window.name": + if v == "\"name\"" { + found = true + } + case "widget.image.hOffset": + if v == "\"hOffset\"" { + found = true + } + case "widget.text.onMouseUp": + if v == "\"onMouseUp\"" { + found = true + } + } + } + } + if !found { + t.Fatal("field not found") + } + } + } + t.N *= len(benchPaths) // because we are running against 3 paths +} + +func BenchmarkEasyJSONLexer(t *testing.B) { + t.ReportAllocs() + skipCC := func(l *jlexer.Lexer, n int) { + for i := 0; i < n; i++ { + l.Skip() + l.WantColon() + l.Skip() + l.WantComma() + } + } + skipGroup := func(l *jlexer.Lexer, n int) { + l.WantColon() + l.Delim('{') + skipCC(l, n) + l.Delim('}') + l.WantComma() + } + for i := 0; i < t.N; i++ { + for j := 0; j < len(benchPaths); j++ { + l := &jlexer.Lexer{Data: []byte(exampleJSON)} + l.Delim('{') + if l.String() == "widget" { + l.WantColon() + l.Delim('{') + switch benchPaths[j] { + case "widget.window.name": + skipCC(l, 1) + if l.String() == "window" { + l.WantColon() + l.Delim('{') + skipCC(l, 1) + if l.String() == "name" { + l.WantColon() + if l.String() == "" { + t.Fatal("did not find the value") + } + } + } + case "widget.image.hOffset": + skipCC(l, 1) + if l.String() == "window" { + skipGroup(l, 4) + } + if l.String() == "image" { + l.WantColon() + l.Delim('{') + skipCC(l, 1) + if l.String() == "hOffset" { + l.WantColon() + if l.Int() == 0 { + t.Fatal("did not find the value") + } + } + } + case "widget.text.onMouseUp": + skipCC(l, 1) + if l.String() == "window" { + skipGroup(l, 4) + } + if l.String() == "image" { + skipGroup(l, 4) + } + if l.String() == "text" { + l.WantColon() + l.Delim('{') + skipCC(l, 5) + if l.String() == "onMouseUp" { + l.WantColon() + if l.String() == "" { + t.Fatal("did not find the value") + } + } + } + } + } + } + } + t.N *= len(benchPaths) // because we are running against 3 paths +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..c7f17d0 Binary files /dev/null and b/logo.png differ