diff --git a/jsonpath.go b/jsonpath.go index 00dc6fd..0a55029 100644 --- a/jsonpath.go +++ b/jsonpath.go @@ -68,7 +68,11 @@ func (c *Compiled) String() string { } func (c *Compiled) Lookup(obj interface{}) (interface{}, error) { - var err error + var ( + err error + root = obj + ) + for _, s := range c.steps { // "key", "idx" switch s.op { @@ -128,7 +132,7 @@ func (c *Compiled) Lookup(obj interface{}) (interface{}, error) { if err != nil { return nil, err } - obj, err = get_filtered(obj, obj, s.args.(string)) + obj, err = get_filtered(obj, root, s.args.(string)) if err != nil { return nil, err } @@ -144,6 +148,7 @@ func tokenize(query string) ([]string, error) { // token_start := false // token_end := false token := "" + open := 0 // fmt.Println("-------------------------------------------------- start") for idx, x := range query { @@ -171,14 +176,22 @@ func tokenize(query string) ([]string, error) { if strings.Contains(token, "[") { // fmt.Println(" contains [ ") if x == ']' && !strings.HasSuffix(token, "\\]") { - if token[0] == '.' { - tokens = append(tokens, token[1:]) - } else { - tokens = append(tokens, token[:]) + open-- + + if open == 0 { + if token[0] == '.' { + tokens = append(tokens, token[1:]) + } else { + tokens = append(tokens, token[:]) + } + token = "" } - token = "" continue } + + if x == '[' && !strings.HasSuffix(token, "\\[") { + open++ + } } else { // fmt.Println(" doesn't contains [ ") if x == '.' { @@ -238,12 +251,14 @@ func parse_token(token string) (op string, key string, args interface{}, err err tail = tail[1 : len(tail)-1] //fmt.Println(key, tail) - if strings.Contains(tail, "?") { + if strings.HasPrefix(tail, "?") { // filter ------------------------------------------------- op = "filter" - if strings.HasPrefix(tail, "?(") && strings.HasSuffix(tail, ")") { - args = strings.Trim(tail[2:len(tail)-1], " ") + if !strings.HasPrefix(tail, "?(") || !strings.HasSuffix(tail, ")") { + err = fmt.Errorf("filter expression needs to be enclosed in parentheses: %v", tail) + return } + args = strings.Trim(tail[2:len(tail)-1], " ") return } else if strings.Contains(tail, ":") { // range ---------------------------------------------- @@ -335,7 +350,8 @@ func get_key(obj interface{}, key string) (interface{}, error) { if reflect.TypeOf(obj) == nil { return nil, ErrGetFromNullObj } - switch reflect.TypeOf(obj).Kind() { + value := reflect.ValueOf(obj) + switch value.Kind() { case reflect.Map: // if obj came from stdlib json, its highly likely to be a map[string]interface{} // in which case we can save having to iterate the map keys to work out if the @@ -347,23 +363,74 @@ func get_key(obj interface{}, key string) (interface{}, error) { } return val, nil } - for _, kv := range reflect.ValueOf(obj).MapKeys() { + for _, kv := range value.MapKeys() { //fmt.Println(kv.String()) if kv.String() == key { - return reflect.ValueOf(obj).MapIndex(kv).Interface(), nil + return value.MapIndex(kv).Interface(), nil } } return nil, fmt.Errorf("key error: %s not found in object", key) case reflect.Slice: // slice we should get from all objects in it. res := []interface{}{} - for i := 0; i < reflect.ValueOf(obj).Len(); i++ { + for i := 0; i < value.Len(); i++ { tmp, _ := get_idx(obj, i) - if v, err := get_key(tmp, key); err == nil { - res = append(res, v) + if key == "" { + res = append(res, tmp) + } else { + if v, err := get_key(tmp, key); err == nil { + res = append(res, v) + } } } return res, nil + case reflect.Ptr: + // Unwrap pointer + realValue := value.Elem() + + if !realValue.IsValid() { + return nil, fmt.Errorf("null pointer") + } + + return get_key(realValue.Interface(), key) + case reflect.Interface: + // Unwrap interface value + realValue := value.Elem() + + return get_key(realValue.Interface(), key) + case reflect.Struct: + for i := 0; i < value.NumField(); i++ { + valueField := value.Field(i) + structField := value.Type().Field(i) + + // Embeded struct + if valueField.Kind() == reflect.Struct && structField.Anonymous { + v, _ := get_key(valueField.Interface(), key) + if v != nil { + return v, nil + } + } else { + if structField.Name == key { + return valueField.Interface(), nil + } + + if tag := structField.Tag.Get("json"); tag != "" { + values := strings.Split(tag, ",") + for _, tagValue := range values { + // In the following cases json tag names should not be checked: + // ",omitempty", "-", "-," + if (tagValue == "" && len(values) == 2) || tagValue == "-" { + break + } + if tagValue != "omitempty" && tagValue == key { + return valueField.Interface(), nil + } + } + } + } + } + + return nil, fmt.Errorf("key error: %s not found in struct", key) default: return nil, fmt.Errorf("object is not map") } @@ -663,7 +730,7 @@ func eval_filter(obj, root interface{}, lp, op, rp string) (res bool, err error) var rp_v interface{} if strings.HasPrefix(rp, "@.") { rp_v, err = filter_get_from_explicit_path(obj, rp) - } else if strings.HasPrefix(rp, "$.") { + } else if strings.HasPrefix(rp, "$.") || strings.HasPrefix(rp, "$[") { rp_v, err = filter_get_from_explicit_path(root, rp) } else { rp_v = rp diff --git a/jsonpath_test.go b/jsonpath_test.go index 90f05b7..2fe85c9 100644 --- a/jsonpath_test.go +++ b/jsonpath_test.go @@ -10,7 +10,46 @@ import ( "testing" ) -var json_data interface{} +type Data struct { + Store *Store `json:"store"` + Expensive float64 `json:"expensive"` +} + +type Store struct { + Book []Goods `json:"book"` + Bicycle []Goods `json:"bicycle"` +} + +type Goods interface { + GetPrice() float64 +} + +type Book struct { + Category string `json:"category,omitempty"` + Author string `json:"author"` + Title string `json:"title"` + Price float64 `json:"price"` + ISBN string `json:"isbn"` + Tags []string `json:"-"` +} + +func (b *Book) GetPrice() float64 { + return b.Price +} + +type Bicycle struct { + Color string `json:"colour"` + Price float64 +} + +func (b *Bicycle) GetPrice() float64 { + return b.Price +} + +var ( + json_data interface{} + structData *Data +) func init() { data := ` @@ -53,6 +92,51 @@ func init() { } ` json.Unmarshal([]byte(data), &json_data) + + structData = &Data{ + Store: &Store{ + Book: []Goods{ + &Book{ + Category: "reference", + Author: "Nigel Rees", + Title: "Sayings of the Century", + Price: 8.95, + }, + &Book{ + Category: "fiction", + Author: "Evelyn Waugh", + Title: "Sword of Honour", + Price: 12.99, + Tags: []string{"fiction", "best-seller", "best-deal"}, + }, + &Book{ + Category: "fiction", + Author: "Herman Melville", + Title: "Moby Dick", + ISBN: "0-553-21311-3", + Price: 8.99, + }, + &Book{ + Category: "fiction", + Author: "J. R. R. Tolkien", + Title: "The Lord of the Rings", + ISBN: "0-395-19395-8", + Price: 22.99, + }, + }, + Bicycle: []Goods{ + &Bicycle{ + Color: "red", + Price: 19.95, + }, + &Bicycle{ + Color: "brown", + Price: 9.99, + }, + }, + }, + Expensive: 10, + } } func Test_jsonpath_JsonPathLookup_1(t *testing.T) { @@ -65,7 +149,7 @@ func Test_jsonpath_JsonPathLookup_1(t *testing.T) { // single index res, _ = JsonPathLookup(json_data, "$.store.book[0].price") if res_v, ok := res.(float64); ok != true || res_v != 8.95 { - t.Errorf("$.store.book[0].price should be 8.95") + t.Errorf("$.store.book[0].price should be 8.95, received: %v", res) } // nagtive single index @@ -96,7 +180,7 @@ func Test_jsonpath_JsonPathLookup_1(t *testing.T) { if res_v, ok := res.([]interface{}); ok != true || res_v[0].(float64) != 8.95 || res_v[1].(float64) != 12.99 || res_v[2].(float64) != 8.99 || res_v[3].(float64) != 22.99 { t.Errorf("exp: [8.95, 12.99, 8.99, 22.99], got: %v", res) } - + // range res, err = JsonPathLookup(json_data, "$.store.book[0:1].price") t.Log(err, res) @@ -114,6 +198,65 @@ func Test_jsonpath_JsonPathLookup_1(t *testing.T) { } } +func Test_jsonpath_JsonPathLookup_structs_1(t *testing.T) { + // key from root + res, _ := JsonPathLookup(structData, "$.expensive") + if res_v, ok := res.(float64); ok != true || res_v != 10.0 { + t.Errorf("expensive should be 10") + } + + // single index + res, _ = JsonPathLookup(structData, "$.store.book[0].price") + if res_v, ok := res.(float64); ok != true || res_v != 8.95 { + t.Errorf("$.store.book[0].price should be 8.95, received: %v", res) + } + + // nagtive single index + res, _ = JsonPathLookup(structData, "$.store.book[-1].isbn") + if res_v, ok := res.(string); ok != true || res_v != "0-395-19395-8" { + t.Errorf("$.store.book[-1].isbn should be \"0-395-19395-8\", received: %v", res) + } + + // multiple index + res, err := JsonPathLookup(structData, "$.store.book[0,1].price") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true || res_v[0].(float64) != 8.95 || res_v[1].(float64) != 12.99 { + t.Errorf("exp: [8.95, 12.99], got: %v", res) + } + + // multiple index + res, err = JsonPathLookup(structData, "$.store.book[0,1].title") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + if res_v[0].(string) != "Sayings of the Century" || res_v[1].(string) != "Sword of Honour" { + t.Errorf("title are wrong: %v", res) + } + } + + // full array + res, err = JsonPathLookup(structData, "$.store.book[0:].price") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true || res_v[0].(float64) != 8.95 || res_v[1].(float64) != 12.99 || res_v[2].(float64) != 8.99 || res_v[3].(float64) != 22.99 { + t.Errorf("exp: [8.95, 12.99, 8.99, 22.99], got: %v", res) + } + + // range + res, err = JsonPathLookup(structData, "$.store.book[0:1].price") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true || res_v[0].(float64) != 8.95 || res_v[1].(float64) != 12.99 { + t.Errorf("exp: [8.95, 12.99], got: %v", res) + } + + // range + res, err = JsonPathLookup(structData, "$.store.book[0:1].title") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + if res_v[0].(string) != "Sayings of the Century" || res_v[1].(string) != "Sword of Honour" { + t.Errorf("title are wrong: %v", res) + } + } +} + func Test_jsonpath_JsonPathLookup_filter(t *testing.T) { res, err := JsonPathLookup(json_data, "$.store.book[?(@.isbn)].isbn") t.Log(err, res) @@ -124,7 +267,7 @@ func Test_jsonpath_JsonPathLookup_filter(t *testing.T) { } } - res, err = JsonPathLookup(json_data, "$.store.book[?(@.price > 10)].title") + res, err = JsonPathLookup(json_data, "$.store.book[?(@.price > 10)].Title") t.Log(err, res) if res_v, ok := res.([]interface{}); ok != true { if res_v[0].(string) != "Sword of Honour" || res_v[1].(string) != "The Lord of the Rings" { @@ -134,11 +277,172 @@ func Test_jsonpath_JsonPathLookup_filter(t *testing.T) { res, err = JsonPathLookup(json_data, "$.store.book[?(@.price > 10)]") t.Log(err, res) + if res_v, ok := res.([]interface{}); !ok { + t.Errorf("expected: []interface{}, received: %v", res) + } else { + if len(res_v) != 2 { + t.Errorf("length of result should be 2, but actual length is %d: %v", len(res_v), res_v) + } else { + prices := []interface{}{res_v[0].(map[string]interface{})["price"], res_v[1].(map[string]interface{})["price"]} + if prices[0] != 12.99 || prices[1] != 22.99 { + t.Errorf("expected book prices: [12.99, 22.99] but received: %v, result: %v", prices, res_v) + } + } + } res, err = JsonPathLookup(json_data, "$.store.book[?(@.price > $.expensive)].price") t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + t.Errorf("expected: []interface{}, received: %v", res) + } else { + if len(res_v) != 2 { + t.Errorf("length of result should be 2, but actual length is %d: %v", len(res_v), res_v) + } else { + if res_v[0].(float64) != 12.99 || res_v[1].(float64) != 22.99 { + t.Errorf("expected result: [12.99, 22.99] but received: %v", res_v) + } + } + } + res, err = JsonPathLookup(json_data, "$.store.book[?(@.price < $.expensive)].price") t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + t.Errorf("expected: []Goods, received: %v", res) + } else { + if len(res_v) != 2 { + t.Errorf("length of result should be 2, but actual length is %d: %v", len(res_v), res_v) + } else { + prices := []float64{res_v[0].(float64), res_v[1].(float64)} + if prices[0] != 8.95 || prices[1] != 8.99 { + t.Errorf("expected result: [8.95, 8.99] but received: %v", prices) + } + } + } + + res, err = JsonPathLookup(json_data, "$.store.book[?@.isbn)]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(json_data, "$.store.book[?(@.isbn]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(json_data, "$.store.book[?@.isbn]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(json_data, "$.store.book[? (@.isbn)]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(json_data, "$.store.book[ ?(@.isbn)]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + +} + +func Test_jsonpath_JsonPathLookup_struct_filter(t *testing.T) { + res, err := JsonPathLookup(structData, "$.store.book[?(@.isbn)].ISBN") + t.Log(err, res) + + if res_v, ok := res.([]interface{}); ok != true { + if res_v[0].(string) != "0-553-21311-3" || res_v[1].(string) != "0-395-19395-8" { + t.Errorf("error: %v", res) + } + } + + res, err = JsonPathLookup(structData, "$.store.book[?(@.price > 10)].title") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + if res_v[0].(string) != "Sword of Honour" || res_v[1].(string) != "The Lord of the Rings" { + t.Errorf("error: %v", res) + } + } + + res, err = JsonPathLookup(structData, "$.store.book[?(@.price > 10)]") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + t.Errorf("expected: []interface{}, received: %v", res) + } else { + if len(res_v) != 2 { + t.Errorf("length of result should be 2, but actual length is %d: %v", len(res_v), res_v) + } else { + prices := []*Book{res_v[0].(*Book), res_v[1].(*Book)} + if prices[0].Price != 12.99 || prices[1].Price != 22.99 { + t.Errorf("expected book prices: [12.99, 22.99] but received: %v, result: %v", prices, res_v) + } + } + } + + res, err = JsonPathLookup(structData, "$.store.book[?(@.price > $.expensive)].price") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + t.Errorf("expected: []interface{}, received: %v", res) + } else { + if len(res_v) != 2 { + t.Errorf("length of result should be 2, but actual length is %d: %v", len(res_v), res_v) + } else { + if res_v[0].(float64) != 12.99 || res_v[1].(float64) != 22.99 { + t.Errorf("expected result: [12.99, 22.99] but received: %v", res_v) + } + } + } + + res, err = JsonPathLookup(structData, "$.store.book[?(@.price < $.expensive)].price") + t.Log(err, res) + if res_v, ok := res.([]interface{}); ok != true { + t.Errorf("expected: []Goods, received: %v", res) + } else { + if len(res_v) != 2 { + t.Errorf("length of result should be 2, but actual length is %d: %v", len(res_v), res_v) + } else { + prices := []float64{res_v[0].(float64), res_v[1].(float64)} + if prices[0] != 8.95 || prices[1] != 8.99 { + t.Errorf("expected result: [8.95, 8.99] but received: %v", prices) + } + } + } + + res, err = JsonPathLookup(structData, "$.store.book[?@.isbn)]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(structData, "$.store.book[?(@.isbn]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(structData, "$.store.book[?@.isbn]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(structData, "$.store.book[? (@.isbn)]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + + res, err = JsonPathLookup(structData, "$.store.book[ ?(@.isbn)]") + t.Log(err, res) + if _, ok := res.([]interface{}); ok == true { + t.Errorf("expected: error, received: %v", res) + } + } func Test_jsonpath_authors_of_all_books(t *testing.T) { @@ -453,7 +757,49 @@ func Test_jsonpath_get_key(t *testing.T) { }, } res, err = get_key(obj4, "a") + if res_v, ok := res.([]interface{}); ok != true || len(res_v) != 2 || res_v[0] != 1 || res_v[1] != 2 { + fmt.Println(err, res) + t.Errorf("[]map[string]interface{} support failed") + } +} + +func Test_jsonpath_get_key_struct(t *testing.T) { + res, err := get_key(structData, "Store") fmt.Println(err, res) + if err != nil { + t.Errorf("failed to get struct key: %v", err) + return + } + if res_v, ok := res.(*Store); !ok || len(res_v.Bicycle) != 2 || len(res_v.Book) != 4 { + t.Error("get field of struct failed") + return + } + + res, err = get_key(structData, "hah") + if err == nil { + t.Error("failed to raise missing key error") + return + } + + res, err = get_key(structData, "store") + if err != nil { + t.Errorf("failed to get struct key: %v", err) + return + } + if res_v, ok := res.(*Store); !ok || len(res_v.Bicycle) != 2 || len(res_v.Book) != 4 { + t.Error("get field of struct by json tag name failed") + return + } + + res, err = get_key(structData.Store.Book[0], "Category") + if err != nil { + t.Errorf("failed to get field of struct masked by interface: %v", err) + return + } + if res.(string) != "reference" { + t.Errorf("not expected value returned: %v", res) + return + } } func Test_jsonpath_get_idx(t *testing.T) { @@ -1179,13 +1525,13 @@ func Test_jsonpath_rootnode_is_array_range(t *testing.T) { t.Logf("idx: %v, v: %v", idx, v) } if len(ares) != 2 { - t.Fatal("len is not 2. got: %v", len(ares)) + t.Fatalf("len is not 2. got: %v", len(ares)) } if ares[0].(float64) != 12.34 { - t.Fatal("idx: 0, should be 12.34. got: %v", ares[0]) + t.Fatalf("idx: 0, should be 12.34. got: %v", ares[0]) } if ares[1].(float64) != 13.34 { - t.Fatal("idx: 0, should be 12.34. got: %v", ares[1]) + t.Fatalf("idx: 0, should be 12.34. got: %v", ares[1]) } } @@ -1232,7 +1578,7 @@ func Test_jsonpath_rootnode_is_nested_array_range(t *testing.T) { t.Logf("idx: %v, v: %v", idx, v) } if len(ares) != 2 { - t.Fatal("len is not 2. got: %v", len(ares)) + t.Fatalf("len is not 2. got: %v", len(ares)) } //FIXME: `$[:1].[0].test` got wrong result @@ -1243,3 +1589,71 @@ func Test_jsonpath_rootnode_is_nested_array_range(t *testing.T) { // t.Fatal("idx: 0, should be 3.1, got: %v", ares[1]) //} } + +func Test_root_array(t *testing.T) { + var ( + err error + books = []map[string]interface{}{ + map[string]interface{}{ + "category": "reference", + "meta": map[string]interface{}{ + "language": "en", + "release_year": 1984, + "available_for_order": false, + }, + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + }, + map[string]interface{}{ + "category": "fiction", + "meta": map[string]interface{}{ + "language": "en", + "release_year": 2012, + "availabe_for_order": true, + }, + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99, + }, + map[string]interface{}{ + "category": "fiction", + "meta": nil, + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + }, + } + ) + + res, err := JsonPathLookup(books, "$[?(@.meta.language == 'en')]") + if res_v, ok := res.([]interface{}); err != nil || ok == false || len(res_v) != 2 { + fmt.Println(res, err) + t.Error("root array support is broken") + } + + res2, err := JsonPathLookup(books, "$[?(@.meta)]") + if res_v, ok := res2.([]interface{}); err != nil || ok == false || len(res_v) != 2 { + fmt.Println(res2, err) + t.Error("root array support broken") + } + + res3, err := JsonPathLookup(books, "$[-1]") + if res_v, ok := res3.(map[string]interface{}); err != nil || ok == false || len(res_v) != 6 || res_v["meta"] != nil { + fmt.Println(res3, err) + t.Error("root array support broken") + } + + res4, err := JsonPathLookup(books, "$[*]") + if res_v, ok := res4.([]map[string]interface{}); err != nil || ok == false || len(res_v) != 3 { + fmt.Println(res4, err) + t.Error("root array support broken") + } + + res5, err := JsonPathLookup(books, "$[?(@.meta.language == 'en')].meta.release_year") + if res_v, ok := res5.([]interface{}); err != nil || ok == false || len(res_v) != 2 { + fmt.Println(res5, err) + t.Error("root array support is broken") + } +}