From 73e6d28be4a6278c1d3323b00dd743c4092ab531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsa=20P=C3=A9ter?= Date: Mon, 3 Sep 2018 22:04:38 +0200 Subject: [PATCH 1/3] Add struct support --- jsonpath.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/jsonpath.go b/jsonpath.go index 00dc6fd..db57d7d 100644 --- a/jsonpath.go +++ b/jsonpath.go @@ -335,7 +335,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 +348,70 @@ 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) } } 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, ok := structField.Tag.Lookup("json"); ok { + 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") } From 520a965cfcae2932225ceba40741a8d4d60a4bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsa=20P=C3=A9ter?= Date: Mon, 3 Sep 2018 23:14:18 +0200 Subject: [PATCH 2/3] Use StructTag.Get() method to not change go version requirements. --- jsonpath.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpath.go b/jsonpath.go index db57d7d..3a9a095 100644 --- a/jsonpath.go +++ b/jsonpath.go @@ -395,7 +395,7 @@ func get_key(obj interface{}, key string) (interface{}, error) { return valueField.Interface(), nil } - if tag, ok := structField.Tag.Lookup("json"); ok { + 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: From 416356484c4dc9d40b942e41e012712f2af85024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsa=20P=C3=A9ter?= Date: Tue, 4 Sep 2018 09:34:36 +0200 Subject: [PATCH 3/3] Add test cases to test struct support --- jsonpath_test.go | 209 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 201 insertions(+), 8 deletions(-) diff --git a/jsonpath_test.go b/jsonpath_test.go index 90f05b7..44585ee 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" { @@ -141,6 +284,17 @@ func Test_jsonpath_JsonPathLookup_filter(t *testing.T) { t.Log(err, 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) + } + } +} + func Test_jsonpath_authors_of_all_books(t *testing.T) { query := "store.book[*].author" expected := []string{ @@ -456,6 +610,45 @@ func Test_jsonpath_get_key(t *testing.T) { fmt.Println(err, res) } +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) { obj := []interface{}{1, 2, 3, 4} res, err := get_idx(obj, 0) @@ -1179,13 +1372,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 +1425,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