From f4afb106da47e3872a69a2b01c4636177056f557 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Wed, 10 Aug 2016 20:07:45 -0700 Subject: [PATCH] first commit --- .travis.yml | 1 + LICENSE | 20 ++ README.md | 160 ++++++++++++++ gjson.go | 591 ++++++++++++++++++++++++++++++++++++++++++++++++++ gjson_test.go | 493 +++++++++++++++++++++++++++++++++++++++++ logo.png | Bin 0 -> 21754 bytes 6 files changed, 1265 insertions(+) create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 gjson.go create mode 100644 gjson_test.go create mode 100644 logo.png 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 0000000000000000000000000000000000000000..c7f17d07c69f622aa73e09571a6fd63e083b8808 GIT binary patch literal 21754 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

v&EFGsKo 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 W4v2eemzW{8hB|6uVN5f0j`$C0OQf;@ literal 0 HcmV?d00001