diff --git a/go.mod b/go.mod index 2c6a84f..3ecdde3 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/PaesslerAG/jsonpath -go 1.15 +go 1.23.4 require ( github.com/PaesslerAG/gval v1.2.2 github.com/google/go-cmp v0.5.9 gopkg.in/yaml.v3 v3.0.1 ) + +require github.com/shopspring/decimal v1.3.1 // indirect diff --git a/jsonpath_test.go b/jsonpath_test.go index d269be8..d4f66a3 100644 --- a/jsonpath_test.go +++ b/jsonpath_test.go @@ -15,7 +15,7 @@ import ( type jsonpathTest struct { name string path string - data string + data any lang gval.Language reorder bool want interface{} @@ -24,8 +24,8 @@ type jsonpathTest struct { wantParseErr bool } -type obj = map[string]interface{} -type arr = []interface{} +type obj = map[string]any +type arr = []any func TestJsonPath(t *testing.T) { @@ -48,6 +48,15 @@ func TestJsonPath(t *testing.T) { "$": obj{"a": "aa"}, }, }, + { + name: "root map", + path: "$", + data: obj{"a": "aa"}, + want: obj{"a": "aa"}, + wantWithPaths: obj{ + "$": obj{"a": "aa"}, + }, + }, { name: "simple select array", path: "$[1]", @@ -57,6 +66,15 @@ func TestJsonPath(t *testing.T) { `$["1"]`: "hey", }, }, + { + name: "simple select array2", + path: "$[1]", + data: arr{7, "hey"}, + want: "hey", + wantWithPaths: obj{ + `$["1"]`: "hey", + }, + }, { name: "negative select array", path: "$[-1]", @@ -66,6 +84,15 @@ func TestJsonPath(t *testing.T) { `$["-1"]`: "hey", }, }, + { + name: "negative select array2", + path: "$[-1]", + data: arr{7, "hey"}, + want: "hey", + wantWithPaths: obj{ + `$["-1"]`: "hey", + }, + }, { name: "negative select on short array", path: "$[-2]", @@ -84,6 +111,15 @@ func TestJsonPath(t *testing.T) { `$["1"]`: "aa", }, }, + { + name: "simple select object2", + path: "$[1]", + data: obj{"1": "aa"}, + want: "aa", + wantWithPaths: obj{ + `$["1"]`: "aa", + }, + }, { name: "simple select out of bounds", path: "$[1]", @@ -93,12 +129,27 @@ func TestJsonPath(t *testing.T) { `$["1"]`: nil, }, }, + { + name: "simple select out of bounds2", + path: "$[1]", + data: arr{"hey"}, + want: nil, + wantWithPaths: obj{ + `$["1"]`: nil, + }, + }, { name: "simple select unknown key", path: "$[1]", data: `{"2":"aa"}`, wantErr: true, }, + { + name: "simple select unknown key2", + path: "$[1]", + data: obj{"2": "aa"}, + wantErr: true, + }, { name: "select array", path: "$[3].a", @@ -108,6 +159,15 @@ func TestJsonPath(t *testing.T) { `$["3"]["a"]`: "bb", }, }, + { + name: "select array2", + path: "$[3].a", + data: arr{55, 41, 70, obj{"a": "bb"}}, + want: "bb", + wantWithPaths: obj{ + `$["3"]["a"]`: "bb", + }, + }, { name: "select object", path: "$[3].a", @@ -117,6 +177,24 @@ func TestJsonPath(t *testing.T) { `$["3"]["a"]`: "aa", }, }, + { + name: "select object2", + path: "$[3].a", + data: obj{"3": obj{"a": "aa"}}, + want: "aa", + wantWithPaths: obj{ + `$["3"]["a"]`: "aa", + }, + }, + { + name: "select object3", + path: "$[3].a", + data: map[string]obj{"3": {"a": "aa"}}, + want: "aa", + wantWithPaths: obj{ + `$["3"]["a"]`: "aa", + }, + }, { name: "range array", path: "$[2:6].a", @@ -126,6 +204,15 @@ func TestJsonPath(t *testing.T) { `$["3"]["a"]`: "bb", }, }, + { + name: "range array2", + path: "$[2:6].a", + data: arr{55, 41, 70, obj{"a": "bb"}}, + want: arr{"bb"}, + wantWithPaths: obj{ + `$["3"]["a"]`: "bb", + }, + }, { name: "range object", //no range over objects path: "$[2:6].a", @@ -133,6 +220,20 @@ func TestJsonPath(t *testing.T) { want: arr{}, wantWithPaths: obj{}, }, + { + name: "range object2", //no range over objects + path: "$[2:6].a", + data: obj{"3": obj{"a": "aa"}}, + want: arr{}, + wantWithPaths: obj{}, + }, + { + name: "range object3", //no range over objects + path: "$[2:6].a", + data: map[string]obj{"3": {"a": "aa"}}, + want: arr{}, + wantWithPaths: obj{}, + }, { name: "range multi match", path: "$[2:6].a", @@ -148,6 +249,36 @@ func TestJsonPath(t *testing.T) { `$["5"]["a"]`: "b3", }, }, + { + name: "range multi match2", + path: "$[2:6].a", + data: arr{obj{"a": "xx"}, 41, obj{"a": "b1"}, obj{"a": "b2"}, 55, obj{"a": "b3"}, obj{"a": "x2"}}, + want: arr{ + "b1", + "b2", + "b3", + }, + wantWithPaths: obj{ + `$["2"]["a"]`: "b1", + `$["3"]["a"]`: "b2", + `$["5"]["a"]`: "b3", + }, + }, + { + name: "range multi match3", + path: "$[2:6].a", + data: []obj{{"a": "xx"}, {"b": 41}, {"a": "b1"}, {"a": "b2"}, {"b": 55}, {"a": "b3"}, {"a": "x2"}}, + want: arr{ + "b1", + "b2", + "b3", + }, + wantWithPaths: obj{ + `$["2"]["a"]`: "b1", + `$["3"]["a"]`: "b2", + `$["5"]["a"]`: "b3", + }, + }, { name: "range all", path: "$[:]", @@ -165,6 +296,23 @@ func TestJsonPath(t *testing.T) { `$["3"]`: obj{"a": "bb"}, }, }, + { + name: "range all2", + path: "$[:]", + data: arr{55, 41, 70, obj{"a": "bb"}}, + want: arr{ + 55, + 41, + 70, + obj{"a": "bb"}, + }, + wantWithPaths: obj{ + `$["0"]`: 55, + `$["1"]`: 41, + `$["2"]`: 70, + `$["3"]`: obj{"a": "bb"}, + }, + }, { name: "range all even", path: "$[::2]", @@ -178,6 +326,19 @@ func TestJsonPath(t *testing.T) { `$["2"]`: 70., }, }, + { + name: "range all even2", + path: "$[::2]", + data: arr{55, 41, 70, obj{"a": "bb"}}, + want: arr{ + 55, + 70, + }, + wantWithPaths: obj{ + `$["0"]`: 55, + `$["2"]`: 70, + }, + }, { name: "range all even reverse", path: "$[::-2]", @@ -191,6 +352,19 @@ func TestJsonPath(t *testing.T) { `$["1"]`: 41., }, }, + { + name: "range all even reverse2", + path: "$[::-2]", + data: arr{55, 41, 70, obj{"a": "bb"}}, + want: arr{ + obj{"a": "bb"}, + 41, + }, + wantWithPaths: obj{ + `$["3"]`: obj{"a": "bb"}, + `$["1"]`: 41, + }, + }, { name: "range reverse", path: "$[2:6:-1].a", @@ -664,7 +838,11 @@ func TestJsonPath(t *testing.T) { }, }, } + runCase := "" for _, tt := range tests { + if runCase != "" && tt.name != runCase { + continue + } tt.lang = jsonpath.Language() t.Run(tt.name, tt.test) } @@ -678,8 +856,15 @@ func (tt jsonpathTest) test(t *testing.T) { if tt.wantParseErr { return } - var v interface{} - err = json.Unmarshal([]byte(tt.data), &v) + var v any + switch d := tt.data.(type) { + case string: + err = json.Unmarshal([]byte(d), &v) + case []byte: + err = json.Unmarshal(d, &v) + default: + v = d + } if err != nil { t.Fatalf("[%s]: could not parse json input: %v", tt.name, err) } diff --git a/path.go b/path.go index d42360a..71db5d9 100644 --- a/path.go +++ b/path.go @@ -109,11 +109,12 @@ func (p *ambiguousPath) visitMatchs(ctx context.Context, r interface{}, visit pa }) } -func (p *ambiguousPath) branchMatcher(ctx context.Context, r interface{}, m ambiguousMatcher) ambiguousMatcher { - return func(k, v interface{}) { - p.branch(ctx, r, v, m) - } -} +// unused +// func (p *ambiguousPath) branchMatcher(ctx context.Context, r interface{}, m ambiguousMatcher) ambiguousMatcher { +// return func(k, v interface{}) { +// p.branch(ctx, r, v, m) +// } +// } func (p *ambiguousPath) withPlainSelector(selector plainSelector) path { p.ending = append(p.ending, selector) diff --git a/placeholder.go b/placeholder.go index d1cd063..0e5fb8a 100644 --- a/placeholder.go +++ b/placeholder.go @@ -41,9 +41,7 @@ func parseJSONObject(ctx context.Context, p *gval.Parser) (gval.Evaluable, error return nil, err } if p.Scan() != ':' { - if err != nil { - return nil, p.Expected("object", ':') - } + return nil, p.Expected("object", ':') } e, err := parseJSONObjectElement(ctx, p, hasWildcard, key) if err != nil { diff --git a/selector.go b/selector.go index a4fa519..3699a41 100644 --- a/selector.go +++ b/selector.go @@ -3,7 +3,9 @@ package jsonpath import ( "context" "fmt" + "reflect" "strconv" + "strings" "github.com/PaesslerAG/gval" ) @@ -125,8 +127,9 @@ func selectValue(c context.Context, key gval.Evaluable, r, v interface{}) (value return r, k, nil default: - return nil, "", fmt.Errorf("unsupported value type %T for select, expected map[string]interface{}, []interface{} or Array", o) + return selectValueByReflect(c, key, r, v) } + } // .. @@ -143,6 +146,92 @@ func mapper(c context.Context, r, v interface{}, match ambiguousMatcher) { }) } +func selectValueByReflect(c context.Context, key gval.Evaluable, r, v interface{}) (value interface{}, jkey string, err error) { + + c = currentContext(c, v) + + // Use reflect to determine type + val := reflect.ValueOf(v) + + // Handle pointer types + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, "", fmt.Errorf("nil pointer") + } + val = val.Elem() + } + + switch val.Kind() { + case reflect.Slice, reflect.Array: + // Handle slice and array types + i, err := key.EvalInt(c, r) + if err != nil { + return nil, "", fmt.Errorf("could not select value, invalid key: %s", err) + } + + length := val.Len() + p := i + if i < 0 { + p = length + i + } + if p < 0 || p >= length { + return nil, strconv.Itoa(i), nil + } + + // Get element value + elem := val.Index(p) + if elem.CanInterface() { + return elem.Interface(), strconv.Itoa(i), nil + } + return nil, strconv.Itoa(i), fmt.Errorf("cannot access element at index %d", i) + + case reflect.Map: + // Handle map types + k, err := key.EvalString(c, r) + if err != nil { + return nil, "", fmt.Errorf("could not select value, invalid key: %s", err) + } + + // Create reflect.Value for key + keyVal := reflect.ValueOf(k) + if !keyVal.Type().AssignableTo(val.Type().Key()) { + return nil, "", fmt.Errorf("key type %T is not assignable to map key type %v", k, val.Type().Key()) + } + + // Find value in map + elem := val.MapIndex(keyVal) + if !elem.IsValid() { + return nil, "", fmt.Errorf("unknown key %s", k) + } + + if elem.CanInterface() { + return elem.Interface(), k, nil + } + return nil, "", fmt.Errorf("cannot access value for key %s", k) + + case reflect.Struct: + // Handle struct types + k, err := key.EvalString(c, r) + if err != nil { + return nil, "", fmt.Errorf("could not select value, invalid key: %s", err) + } + + // Find field, supporting both field name and JSON tag + field, fieldName := findStructField(val, k) + if !field.IsValid() { + return nil, "", fmt.Errorf("unknown field %s", k) + } + + if field.CanInterface() { + return field.Interface(), fieldName, nil + } + return nil, "", fmt.Errorf("cannot access field %s", k) + + default: + return nil, "", fmt.Errorf("unsupported value type %T for select, expected slice, array, map, struct, map[string]interface{}, []interface{}", val.Kind()) + } +} + func visitAll(v interface{}, visit func(key string, v interface{})) { switch v := v.(type) { @@ -163,6 +252,77 @@ func visitAll(v interface{}, visit func(key string, v interface{})) { case Object: v.ForEach(visit) + default: + visitAllByReflect(v, visit) + } + +} + +func visitAllByReflect(v interface{}, visit func(key string, v interface{})) { + + // Use reflect to determine type + val := reflect.ValueOf(v) + + // Handle pointer types + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return + } + val = val.Elem() + } + + switch val.Kind() { + case reflect.Slice, reflect.Array: + // Handle slice and array types + for i := 0; i < val.Len(); i++ { + k := strconv.Itoa(i) + elem := val.Index(i) + if elem.CanInterface() { + visit(k, elem.Interface()) + } + } + + case reflect.Map: + // Handle map types + iter := val.MapRange() + for iter.Next() { + key := iter.Key() + value := iter.Value() + if key.CanInterface() && value.CanInterface() { + // Convert key to string + var keyStr string + switch k := key.Interface().(type) { + case string: + keyStr = k + default: + keyStr = fmt.Sprintf("%v", k) + } + visit(keyStr, value.Interface()) + } + } + + case reflect.Struct: + // Handle struct types + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := val.Type().Field(i) + if field.CanInterface() { + // Prioritize JSON tag, if not, use field name + key := fieldType.Name + jsonTag := fieldType.Tag.Get("json") + if jsonTag != "" { + // Handle JSON tag, which may include options like "omitempty" + if commaIndex := strings.Index(jsonTag, ","); commaIndex != -1 { + jsonTag = jsonTag[:commaIndex] + } + if jsonTag != "" && jsonTag != "-" { + key = jsonTag + } + } + visit(key, field.Interface()) + } + } + } } @@ -249,6 +409,44 @@ func rangeSelector(min, max, step gval.Evaluable) ambiguousSelector { match(k, r) } } + default: + val := reflect.ValueOf(v) + + // Handle pointer types + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return + } + val = val.Elem() + } + if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { + return + } + + // Handle slice and array types + n := val.Len() + min = negmax(min, n) + max = negmax(max, n) + + if min > max { + return + } + + if step > 0 { + for i := min; i < max; i += step { + elem := val.Index(i) + if elem.CanInterface() { + match(strconv.Itoa(i), elem.Interface()) + } + } + } else { + for i := max - 1; i >= min; i += step { + elem := val.Index(i) + if elem.CanInterface() { + match(strconv.Itoa(i), elem.Interface()) + } + } + } } } } @@ -272,3 +470,35 @@ func newScript(script gval.Evaluable) plainSelector { return nil, value, err } } + +// findStructField finds struct field, supporting both field name and JSON tag +func findStructField(val reflect.Value, key string) (reflect.Value, string) { + typ := val.Type() + + // First try direct field name matching + if field := val.FieldByName(key); field.IsValid() { + return field, key + } + + // Then try JSON tag matching + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // Get JSON tag + jsonTag := fieldType.Tag.Get("json") + if jsonTag != "" { + // Handle JSON tag, which may include options like "omitempty" + if commaIndex := strings.Index(jsonTag, ","); commaIndex != -1 { + jsonTag = jsonTag[:commaIndex] + } + + if jsonTag == key { + return field, fieldType.Name + } + } + } + + // If not found, return invalid value + return reflect.Value{}, "" +} diff --git a/selector_test.go b/selector_test.go new file mode 100644 index 0000000..aca075f --- /dev/null +++ b/selector_test.go @@ -0,0 +1,106 @@ +package jsonpath + +import ( + "context" + "reflect" + "testing" +) + +func Test_selectValueByReflect(t *testing.T) { + type args struct { + name string + description string + data any + path string + want any + } + tests := []args{ + { + name: "test_map", + description: "test map", + data: map[string]interface{}{"a": 1}, + path: "$.a", + want: 1, + }, + { + name: "test_slice", + description: "test slice", + data: []interface{}{1, 2, 3}, + path: "$[1]", + want: 2, + }, + { + name: "test_map_slice", + description: "test map-> slice", + data: map[string]interface{}{"a": []interface{}{1, 2, 3}}, + path: "$['a'][1]", + want: 2, + }, + { + name: "test_map_slice_map", + description: "test map-> slice-> map", + data: map[string][]map[string]any{"a": {{"b": 1}}}, + path: "$['a'][0]['b']", + want: 1, + }, + { + name: "test_struct", + description: "test struct", + data: struct{ A int }{A: 1}, + path: "$['A']", + want: 1, + }, + { + name: "test_struct_slice", + description: "test struct-> slice", + data: struct{ A []int }{A: []int{1, 2, 3}}, + path: "$['A'][1]", + want: 2, + }, + { + name: "test_struct_slice_map", + description: "test struct-> slice-> map", + data: struct{ A []map[string]int }{A: []map[string]int{{"b": 1}}}, + path: "$['A'][0]['b']", + want: 1, + }, + { + name: "test_struct_with_json_tag", + description: "test struct with json tag", + data: struct { + A int `json:"a"` + }{A: 1}, + path: "$['a']", + want: 1, + }, + { + name: "test_struct_with_json_tag2", + description: "test struct with json tag", + data: struct { + A int `json:"a"` + }{A: 1}, + path: "$['A']", + want: 1, + }, + } + runCase := "" + for _, tt := range tests { + if runCase != "" && tt.name != runCase { + continue + } + t.Run(tt.name, func(t *testing.T) { + lang := Language() + get, err := lang.NewEvaluable(tt.path) + if err != nil { + t.Fatal(err) + } + result, err := get(context.Background(), tt.data) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(result, tt.want) { + t.Errorf("selectValueByReflect() got = %v, want %v", result, tt.want) + } + }) + } +}