diff --git a/README.md b/README.md index edf9859..1706e47 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ 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, @@ -93,16 +93,6 @@ string, for JSON string literals nil, for JSON null ``` -To get the Go 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: ```go @@ -113,6 +103,30 @@ result.Raw // holds the raw json result.Multi // holds nested array values ``` +There are a variety of handy functions that work on a result: + +```go +result.Value() interface{} +result.Int() int64 +result.Float() float64 +result.String() string +result.Bool() bool +result.Array() []gjson.Result +result.Map() map[string]gjson.Result +result.Get(path string) Result +``` + +The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types: + +```go +boolean >> bool +number >> float64 +string >> string +null >> nil +array >> []interface{} +object >> map[string]interface{} +``` + ## Get nested array values Suppose you want all the last names from the following json: @@ -138,11 +152,23 @@ You would use the path "programmers.#.lastName" like such: ```go result := gjson.Get(json, "programmers.#.lastName") -for _,name := range result.Multi { +for _,name := range result.Array() { println(name.String()) } ``` +## 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 +Parse(json).Get("name").Get("last") +Get("name").Get("last") +Get("name.last") +``` + ## Check for the existence of a value Sometimes you may want to see if the value actually existed in the json document. diff --git a/gjson.go b/gjson.go index 8001db3..a4a9f75 100644 --- a/gjson.go +++ b/gjson.go @@ -19,8 +19,6 @@ const ( True // JSON is a raw block of JSON JSON - // Multi is a subset of results - Multi ) // Result represents a json value that is returned from Get(). @@ -33,8 +31,6 @@ type Result struct { Str string // Num is the json number Num float64 - // Multi is the subset of results - Multi []Result } // String returns a string representation of the value. @@ -48,15 +44,6 @@ func (t Result) String() string { return strconv.FormatFloat(t.Num, 'f', -1, 64) case String: return t.Str - case Multi: - var str string - for i, res := range t.Multi { - if i > 0 { - str += "," - } - str += res.String() - } - return str case JSON: return t.Raw case True: @@ -64,6 +51,316 @@ func (t Result) String() string { } } +// 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" + 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, _ := strconv.ParseInt(t.Str, 10, 64) + return n + case Number: + return int64(t.Num) + } +} + +// 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 + } +} + +// Array returns back an array of children. The result must be a JSON array. +func (t Result) Array() []Result { + if t.Type != JSON { + return nil + } + a, _, _ := t.arrayOrMap('[') + return a +} + +// Map returns back an map of children. The result should be a JSON array. +func (t Result) Map() map[string]Result { + if t.Type != JSON { + return map[string]Result{} + } + _, o, _ := t.arrayOrMap('{') + return 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) +} + +func (t Result) arrayOrMap(vc byte) ([]Result, map[string]Result, byte) { + var a = []Result{} + var o = map[string]Result{} + 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] == '[' { + 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 + } + } + } + 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:]) + } else { + continue + } + case '{', '[': + value.Type = JSON + value.Raw = squash(json[i:]) + 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:]) + } + i += len(value.Raw) - 1 + + if vc == '{' { + if count%2 == 0 { + key = value + } else { + o[key.Str] = value + } + count++ + } else { + a = append(a, value) + } + } +end: + return a, o, vc +} + +// Parse parses the json and returns a result +func Parse(json string) Result { + var value Result + for i := 0; i < len(json); i++ { + 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 '{', '[': + value.Type = JSON + value.Raw = json[i:] + // we just trim the tail end + for value.Raw[len(value.Raw)-1] <= ' ' { + value.Raw = value.Raw[:len(value.Raw)-1] + } + 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 +} + +func squash(json string) string { + // expects that the lead character is a '[' or '{' + // squash the value, ignoring all nested arrays and objects. + // the first '[' or '{' has already been read + depth := 1 + for i := 1; 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 { + 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 + } + } + return json[:i+1], unescape(json[1:i]) + } + } + return json, json[1:] +} + // Exists returns true if value exists. // // if gjson.Get(json, "name.last").Exists(){ @@ -92,17 +389,26 @@ func (t Result) Value() interface{} { return false case Number: return t.Num - case Multi: - var res = make([]interface{}, len(t.Multi)) - for i, v := range t.Multi { - res[i] = v.Value() - } - return res case JSON: - return t.Raw + a, o, vc := t.arrayOrMap(0) + if vc == '{' { + var m = map[string]interface{}{} + for k, v := range o { + m[k] = v.Value() + } + return m + } else if vc == '[' { + var m = make([]interface{}, 0, len(a)) + for _, v := range a { + m = append(m, v.Value()) + } + return m + } + return nil case True: return true } + } type part struct { @@ -518,6 +824,9 @@ proc_val: // the first double-quote has already been read s = i for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } if json[i] == '"' { value.Raw = json[s-1 : i+1] value.Str = json[s:i] @@ -598,14 +907,19 @@ proc_val: case '}', ']': if arrch && parts[depth-1].key == "#" { if alogok { - result := Result{Type: Multi} + var jsons = make([]byte, 0, 64) + jsons = append(jsons, '[') for j := 0; j < len(alog); j++ { res := Get(json[alog[j]:], alogkey) if res.Exists() { - result.Multi = append(result.Multi, res) + if j > 0 { + jsons = append(jsons, ',') + } + jsons = append(jsons, []byte(res.Raw)...) } } - return result + jsons = append(jsons, ']') + return Result{Type: JSON, Raw: string(jsons)} } else { return Result{Type: Number, Num: float64(f.count)} } @@ -689,7 +1003,7 @@ func unescape(json string) string { //, error) { // The caseSensitive paramater is used when the tokens are Strings. // The order when comparing two different type is: // -// Null < False < Number < String < True < JSON < Multi +// Null < False < Number < String < True < JSON // func (t Result) Less(token Result, caseSensitive bool) bool { if t.Type < token.Type { @@ -707,17 +1021,6 @@ func (t Result) Less(token Result, caseSensitive bool) bool { if t.Type == Number { return t.Num < token.Num } - if t.Type == Multi { - for i := 0; i < len(t.Multi) && i < len(token.Multi); i++ { - if t.Multi[i].Less(token.Multi[i], caseSensitive) { - return true - } - if token.Multi[i].Less(t.Multi[i], caseSensitive) { - return false - } - } - return len(t.Multi) < len(token.Multi) - } return t.Raw < token.Raw } diff --git a/gjson_test.go b/gjson_test.go index 0e85caa..15dfa24 100644 --- a/gjson_test.go +++ b/gjson_test.go @@ -116,32 +116,64 @@ var basicJSON = `{"age":100, "name":{"here":"B\\\"R"}, "firstName": "Elliotte", "lastName": "Harold", "email": "cccc" - } + }, + { + "firstName": 1002.3 + } ] } }` func TestBasic(t *testing.T) { + var mtok Result + 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) + } + + 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 - mtok := Get(basicJSON, "loggy.programmers.#.firstName") - if mtok.Type != Multi { - t.Fatal("expected %v, got %v", Multi, mtok.Type) + if token = Parse("-102"); token.Num != -102 { + t.Fatal("expected %v, got %v", -102, token.Num) } - if len(mtok.Multi) != 3 { - t.Fatalf("expected 3, got %v", len(mtok.Multi)) + if token = Parse("102"); token.Num != 102 { + t.Fatal("expected %v, got %v", 102, token.Num) } - for i, ex := range []string{"Brett", "Jason", "Elliotte"} { - if mtok.Multi[i].String() != ex { - t.Fatalf("expected '%v', got '%v'", ex, mtok.Multi[i].String()) + if token = Parse("102.2"); token.Num != 102.2 { + t.Fatal("expected %v, got %v", 102.2, token.Num) + } + if token = Parse(`"hello"`); token.Str != "hello" { + t.Fatal("expected %v, got %v", "hello", token.Str) + } + if token = Parse(`"\"he\nllo\""`); token.Str != "\"he\nllo\"" { + t.Fatal("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 != Multi { - t.Fatal("expected %v, got %v", Multi, mtok.Type) + if mtok.Type != JSON { + t.Fatal("expected %v, got %v", JSON, mtok.Type) } - if len(mtok.Multi) != 0 { - t.Fatalf("expected 0, got %v", len(mtok.Multi)) + if len(mtok.Array()) != 0 { + t.Fatalf("expected 0, got %v", len(mtok.Array())) } + if Get(basicJSON, "items.3.tags.#").Num != 3 { t.Fatalf("expected 3, got %v", Get(basicJSON, "items.3.tags.#").Num) } @@ -201,13 +233,19 @@ func TestBasic(t *testing.T) { if token.String() != `{"what is a wren?":"a bird"}` { t.Fatal("expecting '"+`{"what is a wren?":"a bird"}`+"'", "got", token.String()) } - _ = token.Value().(string) + _ = token.Value().(map[string]interface{}) if Get(basicJSON, "").Value() != nil { t.Fatal("should be nil") } Get(basicJSON, "vals.hello") + + mm := Parse(basicJSON).Value().(map[string]interface{}) + fn := mm["loggy"].(map[string]interface{})["programmers"].([]interface{})[1].(map[string]interface{})["firstName"].(string) + if fn != "Jason" { + t.Fatalf("expecting %v, got %v", "Jason", fn) + } } func TestUnescape(t *testing.T) {