diff --git a/CHANGELOG.md b/CHANGELOG.md index de2fb7d..62f16f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm The structure and content of this file follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.21.0] - unreleased +### Added +- Added the Expr function `BracketString` to force the use of bracket + notation to for normalized paths as described by the draft IETF + JSONPath document in section 2.7. +- Added `jp.Expr.Locate()` function that returns normalized paths for JSONPath expression. +### Fixed +- TBD Unmarshal now supports arrays such as `[4]int`. + ## [1.20.3] - 2023-11-09 ### Added - Added an option to jp.Walk to just callback on leaves making the function more useable. diff --git a/alt/diff.go b/alt/diff.go index aa82a40..7183444 100644 --- a/alt/diff.go +++ b/alt/diff.go @@ -3,6 +3,7 @@ package alt import ( + "fmt" "reflect" "time" "unsafe" @@ -18,6 +19,24 @@ var TimeTolerance = time.Millisecond // is a wildcard that matches either. type Path []any +// String representation of the Path. +func (p Path) String() string { + var b []byte + + for i, a := range p { + switch ta := a.(type) { + case int: + b = fmt.Appendf(b, "[%d]", ta) + case string: + if 0 < i { + b = append(b, '.') + } + b = append(b, ta...) + } + } + return string(b) +} + // Diff returns the paths to the differences between two values. Any ignore // paths are ignored in the comparison. func Diff(v0, v1 any, ignores ...Path) (diffs []Path) { diff --git a/alt/diff_test.go b/alt/diff_test.go index 77a18d1..5fa0b16 100644 --- a/alt/diff_test.go +++ b/alt/diff_test.go @@ -237,3 +237,7 @@ func TestDiffArrayIgnores2(t *testing.T) { ) tt.Equal(t, "[[0 b]]", pretty.SEN(diffs)) } + +func TestPathString(t *testing.T) { + tt.Equal(t, "a[3].b", alt.Path{"a", 3, "b"}.String()) +} diff --git a/alt/example_diff_test.go b/alt/example_diff_test.go index c5139f3..aee23a0 100644 --- a/alt/example_diff_test.go +++ b/alt/example_diff_test.go @@ -20,7 +20,7 @@ func ExampleDiff() { }) fmt.Printf("diff: %v\n", diffs) - // Output: diff: [[y] [z 1] [z 2]] + // Output: diff: [y z[1] z[2]] } func ExampleCompare() { @@ -30,7 +30,7 @@ func ExampleCompare() { ) fmt.Printf("diff: %v\n", diff) - // Output: diff: [z 1] + // Output: diff: z[1] } func ExampleMatch() { diff --git a/jp/at.go b/jp/at.go index b896036..6c5a6c0 100644 --- a/jp/at.go +++ b/jp/at.go @@ -11,3 +11,10 @@ func (f At) Append(buf []byte, bracket, first bool) []byte { buf = append(buf, '@') return buf } + +func (f At) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + if 0 < len(rest) { + locs = rest[0].locate(append(pp, f), data, rest[1:], max) + } + return +} diff --git a/jp/bracket.go b/jp/bracket.go index 84b2845..2737330 100644 --- a/jp/bracket.go +++ b/jp/bracket.go @@ -11,3 +11,10 @@ type Bracket byte func (f Bracket) Append(buf []byte, bracket, first bool) []byte { return buf } + +func (f Bracket) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + if 0 < len(rest) { + locs = rest[0].locate(pp, data, rest[1:], max) + } + return +} diff --git a/jp/child.go b/jp/child.go index 752eb7b..e35a70d 100644 --- a/jp/child.go +++ b/jp/child.go @@ -63,3 +63,24 @@ func (f Child) remove(value any) (out any, changed bool) { } return } + +func (f Child) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + var ( + v any + has bool + ) + switch td := data.(type) { + case map[string]any: + v, has = td[string(f)] + case gen.Object: + v, has = td[string(f)] + case Keyed: + v, has = td.ValueForKey(string(f)) + default: + v, has = pp.reflectGetChild(td, string(f)) + } + if has { + locs = locateNthChildHas(pp, f, v, rest, max) + } + return +} diff --git a/jp/descent.go b/jp/descent.go index 35b2ec7..dcb3678 100644 --- a/jp/descent.go +++ b/jp/descent.go @@ -2,6 +2,12 @@ package jp +import ( + "reflect" + + "github.com/ohler55/ojg/gen" +) + // Descent is used as a flag to indicate the path should be displayed in a // recursive descent representation. type Descent byte @@ -16,3 +22,128 @@ func (f Descent) Append(buf []byte, bracket, first bool) []byte { } return buf } + +func (f Descent) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + if len(rest) == 0 { // last one + loc := make(Expr, len(pp)) + copy(loc, pp) + locs = append(locs, loc) + } else { + locs = locateContinueFrag(locs, pp, data, rest, max) + } + cp := append(pp, nil) // place holder + mx := max + switch td := data.(type) { + case map[string]any: + for k, v := range td { + cp[len(pp)] = Child(k) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, v, rest, mx)...) + } + case []any: + for i, v := range td { + cp[len(pp)] = Nth(i) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, v, rest, mx)...) + } + case gen.Object: + for k, v := range td { + cp[len(pp)] = Child(k) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, v, rest, mx)...) + } + case gen.Array: + for i, v := range td { + cp[len(pp)] = Nth(i) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, v, rest, mx)...) + } + case Keyed: + keys := td.Keys() + for _, k := range keys { + v, _ := td.ValueForKey(k) + cp[len(pp)] = Child(k) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, v, rest, mx)...) + } + case Indexed: + size := td.Size() + for i := 0; i < size; i++ { + v := td.ValueAtIndex(i) + cp[len(pp)] = Nth(i) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, v, rest, mx)...) + } + case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String, + int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int: + default: + rd := reflect.ValueOf(data) + rt := rd.Type() + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + rd = rd.Elem() + } + cp := append(pp, nil) // place holder + switch rt.Kind() { + case reflect.Struct: + for i := rd.NumField() - 1; 0 <= i; i-- { + rv := rd.Field(i) + if rv.CanInterface() { + cp[len(pp)] = Child(rt.Field(i).Name) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, rv.Interface(), rest, mx)...) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < rd.Len(); i++ { + rv := rd.Index(i) + if rv.CanInterface() { + cp[len(pp)] = Nth(i) + if 0 < max { + mx = max - len(locs) + if mx <= 0 { + break + } + } + locs = append(locs, f.locate(cp, rv.Interface(), rest, mx)...) + } + } + } + } + return +} diff --git a/jp/expr.go b/jp/expr.go index d9f36c0..75542ae 100644 --- a/jp/expr.go +++ b/jp/expr.go @@ -2,7 +2,9 @@ package jp -import "unsafe" +import ( + "unsafe" +) // Expr is a JSON path expression composed of fragments. An Expr implements // JSONPath as described by https://goessner.net/articles/JsonPath. Where the @@ -15,10 +17,16 @@ func (x Expr) String() string { return string(x.Append(nil)) } +// BracketString returns a string representation of the expression using the +// bracket notation. +func (x Expr) BracketString() string { + return string(x.Append(nil, true)) +} + // Append a string representation of the expression to a byte slice and return // the expanded buffer. -func (x Expr) Append(buf []byte) []byte { - bracket := false +func (x Expr) Append(buf []byte, brackets ...bool) []byte { + bracket := 0 < len(brackets) && brackets[0] for i, frag := range x { if _, ok := frag.(Bracket); ok { bracket = true diff --git a/jp/expr_test.go b/jp/expr_test.go index 65c5c17..afc317b 100644 --- a/jp/expr_test.go +++ b/jp/expr_test.go @@ -79,3 +79,8 @@ func TestExprBracket(t *testing.T) { br := jp.Bracket('x') tt.Equal(t, 0, len(br.Append([]byte{}, true, true))) } + +func TestExprBracketString(t *testing.T) { + x := jp.R().C("abc").N(1).C("def") + tt.Equal(t, "$['abc'][1]['def']", x.BracketString()) +} diff --git a/jp/filter.go b/jp/filter.go index 7a86452..ee0f415 100644 --- a/jp/filter.go +++ b/jp/filter.go @@ -250,3 +250,26 @@ func (f Filter) removeOne(value any) (out any, changed bool) { } return } + +func (f Filter) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + ns, lcs := f.evalWithRoot([]any{}, data, nil) + stack, _ := ns.([]any) + if len(rest) == 0 { // last one + for _, lc := range lcs { + locs = locateAppendFrag(locs, pp, lc) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for i, lc := range lcs { + cp[len(pp)] = lc + locs = locateContinueFrag(locs, cp, stack[i], rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + return +} diff --git a/jp/frag.go b/jp/frag.go index 0cae011..dfdcc77 100644 --- a/jp/frag.go +++ b/jp/frag.go @@ -9,4 +9,6 @@ type Frag interface { // Append a fragment string representation of the fragment to the buffer // then returning the expanded buffer. Append(buf []byte, bracket, first bool) []byte + + locate(pp Expr, data any, rest Expr, max int) (locs []Expr) } diff --git a/jp/get.go b/jp/get.go index cc2e49a..c170e58 100644 --- a/jp/get.go +++ b/jp/get.go @@ -826,7 +826,8 @@ func (x Expr) Get(data any) (results []any) { } case *Filter: before := len(stack) - stack, _ = tf.EvalWithRoot(stack, prev, data).([]any) + ns, _ := tf.evalWithRoot(stack, prev, data) + stack, _ = ns.([]any) if int(fi) == len(x)-1 { // last one for i := len(stack) - 1; before <= i; i-- { results = append(results, stack[i]) @@ -1616,7 +1617,8 @@ func (x Expr) FirstFound(data any) (any, bool) { } case *Filter: before := len(stack) - stack, _ = tf.EvalWithRoot(stack, prev, data).([]any) + ns, _ := tf.evalWithRoot(stack, prev, data) + stack, _ = ns.([]any) if int(fi) == len(x)-1 { // last one if before < len(stack) { result := stack[len(stack)-1] diff --git a/jp/has.go b/jp/has.go index 2c901e3..809ab85 100644 --- a/jp/has.go +++ b/jp/has.go @@ -755,7 +755,8 @@ func (x Expr) Has(data any) bool { } case *Filter: before := len(stack) - stack, _ = tf.EvalWithRoot(stack, prev, data).([]any) + ns, _ := tf.evalWithRoot(stack, prev, data) + stack, _ = ns.([]any) if int(fi) == len(x)-1 { // last one if before < len(stack) { stack = stack[:before] diff --git a/jp/locate.go b/jp/locate.go new file mode 100644 index 0000000..27675fe --- /dev/null +++ b/jp/locate.go @@ -0,0 +1,72 @@ +// Copyright (c) 2023, Peter Ohler, All rights reserved. + +package jp + +import ( + "reflect" + + "github.com/ohler55/ojg/gen" +) + +// Locate the values described by the Expr and return a slice of normalized +// paths to those values in the data. The returned slice is limited to the max +// specified. A max of 0 or less indicates there is no maximum. +func (x Expr) Locate(data any, max int) (locs []Expr) { + if 0 < len(x) { + locs = x[0].locate(nil, data, x[1:], max) + } + return +} + +func locateNthChildHas(pp Expr, f Frag, v any, rest Expr, max int) (locs []Expr) { + if len(rest) == 0 { // last one + loc := make(Expr, len(pp)+1) + copy(loc, pp) + loc[len(pp)] = f + locs = []Expr{loc} + } else { + switch v.(type) { + case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String, + int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int: + case map[string]any, []any, gen.Object, gen.Array, Keyed, Indexed: + locs = rest[0].locate(append(pp, f), v, rest[1:], max) + default: + if rt := reflect.TypeOf(v); rt != nil { + switch rt.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Array, reflect.Map: + locs = rest[0].locate(append(pp, f), v, rest[1:], max) + } + } + } + } + return +} + +func locateAppendFrag(locs []Expr, pp Expr, f Frag) []Expr { + loc := make(Expr, len(pp)+1) + copy(loc, pp) + loc[len(pp)] = f + + return append(locs, loc) +} + +func locateContinueFrag(locs []Expr, cp Expr, v any, rest Expr, max int) []Expr { + mx := max + if 0 < max { + mx = max - len(locs) + } + switch v.(type) { + case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String, + int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int: + case map[string]any, []any, gen.Object, gen.Array, Keyed, Indexed: + locs = append(locs, rest[0].locate(cp, v, rest[1:], mx)...) + default: + if rt := reflect.TypeOf(v); rt != nil { + switch rt.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Array, reflect.Map: + locs = append(locs, rest[0].locate(cp, v, rest[1:], mx)...) + } + } + } + return locs +} diff --git a/jp/locate_test.go b/jp/locate_test.go new file mode 100644 index 0000000..a1aa716 --- /dev/null +++ b/jp/locate_test.go @@ -0,0 +1,243 @@ +// Copyright (c) 2023, Peter Ohler, All rights reserved. + +package jp_test + +import ( + "fmt" + "sort" + "testing" + + "github.com/ohler55/ojg/alt" + "github.com/ohler55/ojg/jp" + "github.com/ohler55/ojg/tt" +) + +type locateData struct { + path string + max int + data any + expect []string + noSort bool +} + +var ( + locateTestData = []*locateData{ + {path: "", expect: []string{}}, + {path: "a.b", data: map[string]any{"a": map[string]any{"b": 2}, "x": 3}, expect: []string{"a.b"}}, + {path: "a[1]", data: map[string]any{"a": []any{1, 2, 3}}, expect: []string{"a[1]"}}, + {path: "a[-1]", data: map[string]any{"a": []any{1, 2, 3}}, expect: []string{"a[2]"}}, + {path: "a[*]", data: map[string]any{"a": []any{1, 2, 3}}, expect: []string{"a[0]", "a[1]", "a[2]"}}, + {path: "$.a.*.b", max: 2, expect: []string{"$.a[0].b", "$.a[1].b"}}, + {path: "$.a[1].*", expect: []string{"$.a[1].a", "$.a[1].b", "$.a[1].c", "$.a[1].d"}}, + {path: "$.*[1].c", expect: []string{"$.a[1].c", "$.b[1].c", "$.c[1].c", "$.d[1].c"}}, + {path: "*[*]", max: 1, data: map[string]any{"a": []any{1, 2, 3}}, expect: []string{"a[0]"}}, + {path: "*", max: 1, data: map[string]any{"a": 1}, expect: []string{"a"}}, + {path: "@.a[?(@.b == 122)].c", max: 1, expect: []string{"@.a[1].c"}}, + {path: "@.a[?(@.b == 122)]", max: 1, expect: []string{"@.a[1]"}}, + {path: "a[1:3].a", noSort: true, expect: []string{"a[1].a", "a[2].a"}}, + {path: "a[1:3].a", max: 1, expect: []string{"a[1].a"}}, + {path: "a[2:0:-1].a", max: 1, expect: []string{"a[2].a"}}, + {path: "a[1:3]", noSort: true, expect: []string{"a[1]", "a[2]"}}, + {path: "a[2:0:-1].a", noSort: true, expect: []string{"a[2].a", "a[1].a"}}, + {path: "a[-2:0:-1]", noSort: true, expect: []string{"a[2]", "a[1]"}}, + {path: "a[5:0:-1]", noSort: true, expect: []string{"a[3]", "a[2]", "a[1]"}}, + {path: "a[1:-7:-1]", noSort: true, expect: []string{"a[1]", "a[0]"}}, + {path: "a[-6:6:]", noSort: true, expect: []string{"a[0]", "a[1]", "a[2]", "a[3]"}}, + {path: "a[2:0:0]", expect: []string{}}, + {path: "a[1,3].a", noSort: true, expect: []string{"a[1].a", "a[3].a"}}, + {path: "a[1,3]", noSort: true, expect: []string{"a[1]", "a[3]"}}, + {path: "a[-1,1]", noSort: true, expect: []string{"a[3]", "a[1]"}}, + {path: "a[-1,1]", max: 1, expect: []string{"a[3]"}}, + {path: "a[1]['a','c']", noSort: true, expect: []string{"a[1].a", "a[1].c"}}, + { + path: "$..", + noSort: true, + data: map[string]any{"a": []any{1, map[string]any{"x": 3}}}, + expect: []string{"$", "$.a", "$.a[0]", "$.a[1]", "$.a[1].x"}, + }, + { + path: "$..", + max: 3, + noSort: true, + data: map[string]any{"a": []any{1, map[string]any{"x": 3}}}, + expect: []string{"$", "$.a", "$.a[0]"}, + }, + { + path: "$..", max: 2, noSort: true, data: []any{map[string]any{"a": 3}}, expect: []string{"$", "$[0]"}, + }, + {path: "$..a", expect: []string{ + "$.a", + "$.a[0].a", + "$.a[1].a", + "$.a[2].a", + "$.a[3].a", + "$.b[0].a", + "$.b[1].a", + "$.b[2].a", + "$.b[3].a", + "$.c[0].a", + "$.c[1].a", + "$.c[2].a", + "$.c[3].a", + "$.d[0].a", + "$.d[1].a", + "$.d[2].a", + "$.d[3].a", + }}, + } +) + +func testDiffString(expect, actual []string, diff alt.Path) string { + var b []byte + + b = fmt.Appendf(b, "\n diff at %s\n", diff) + b = append(b, " expect: ["...) + for _, str := range expect { + b = append(b, "\n "...) + b = append(b, str...) + } + b = append(b, "\n ]\n actual: ["...) + for _, str := range actual { + b = append(b, "\n "...) + b = append(b, str...) + } + b = append(b, "\n ]\n"...) + + return string(b) +} + +func TestExprLocateAny(t *testing.T) { + data := buildTree(4, 3, 0) + for i, d := range locateTestData { + if testing.Verbose() { + fmt.Printf("... %d: %s\n", i, d.path) + } + x, err := jp.ParseString(d.path) + tt.Nil(t, err) + var locs []jp.Expr + if d.data == nil { + locs = x.Locate(data, d.max) + } else { + locs = x.Locate(d.data, d.max) + } + var results []string + for _, loc := range locs { + results = append(results, loc.String()) + } + if !d.noSort { + sort.Strings(results) + } + diff := alt.Compare(d.expect, results) + if 0 < len(diff) { + t.Fatal(testDiffString(d.expect, results, diff)) + } + } +} + +func TestExprLocateNode(t *testing.T) { + data := alt.Generify(buildTree(4, 3, 0)) + for i, d := range locateTestData { + if testing.Verbose() { + fmt.Printf("... %d: %s\n", i, d.path) + } + x, err := jp.ParseString(d.path) + tt.Nil(t, err) + var locs []jp.Expr + if d.data == nil { + locs = x.Locate(data, d.max) + } else { + locs = x.Locate(alt.Generify(d.data), d.max) + } + var results []string + for _, loc := range locs { + results = append(results, loc.String()) + } + if !d.noSort { + sort.Strings(results) + } + diff := alt.Compare(d.expect, results) + if 0 < len(diff) { + t.Fatal(testDiffString(d.expect, results, diff)) + } + } +} + +func TestExprLocateOrdered(t *testing.T) { + data := orderedFromSimple(buildTree(4, 3, 0)) + for i, d := range locateTestData { + if testing.Verbose() { + fmt.Printf("... %d: %s\n", i, d.path) + } + x, err := jp.ParseString(d.path) + tt.Nil(t, err) + var locs []jp.Expr + if d.data == nil { + locs = x.Locate(data, d.max) + } else { + locs = x.Locate(orderedFromSimple(d.data), d.max) + } + var results []string + for _, loc := range locs { + results = append(results, loc.String()) + } + if !d.noSort { + sort.Strings(results) + } + diff := alt.Compare(d.expect, results) + if 0 < len(diff) { + t.Fatal(testDiffString(d.expect, results, diff)) + } + } +} + +func TestExprLocateReflect(t *testing.T) { + for i, d := range []*locateData{ + {path: "a", data: &Sample{A: 3, B: "sample"}, expect: []string{"a"}}, + {path: "[1]", data: []int{1, 2, 3}, expect: []string{"[1]"}}, + {path: "['a','b']", data: &Sample{A: 3, B: "sample"}, expect: []string{"a", "b"}}, + {path: "[1,2]", data: []int{1, 2, 3}, expect: []string{"[1]", "[2]"}}, + {path: "[1:2]", data: nil, expect: []string{}}, + {path: "[1:3]", data: []int{1, 2, 3}, expect: []string{"[1]", "[2]"}}, + {path: "[1:3]", max: 1, data: []int{1, 2, 3}, expect: []string{"[1]"}}, + {path: "[2:0:-1]", max: 1, data: []int{1, 2, 3}, expect: []string{"[2]"}}, + {path: "[0:3].b", max: 1, data: []map[string]any{{"a": 1}, {"b": 1}}, expect: []string{"[1].b"}}, + {path: "[2:0:-1].b", max: 1, data: []map[string]any{{"a": 1}, {"b": 1}}, expect: []string{"[1].b"}}, + {path: "$.*", data: nil, expect: []string{}}, + {path: "$.*", data: &Sample{A: 3, B: "sample"}, expect: []string{"$.A", "$.B"}}, + {path: "$.*", max: 1, data: &Sample{A: 3, B: "sample"}, expect: []string{"$.B"}}, + {path: "$.*.a", data: &Any{X: map[string]any{"a": 1}}, expect: []string{"$.X.a"}}, + {path: "$.*.a", max: 1, data: &Any{X: map[string]any{"a": 1}}, expect: []string{"$.X.a"}}, + {path: "$.*", max: 2, data: []int{1, 2, 3}, expect: []string{"$[0]", "$[1]"}}, + {path: "$.*.a", max: 1, data: []map[string]any{{"a": 1}}, expect: []string{"$[0].a"}}, + {path: "$..", data: nil, expect: []string{"$"}}, + {path: "$..", data: &Sample{A: 3, B: "sample"}, expect: []string{"$", "$.A", "$.B"}}, + {path: "$..", max: 2, data: &Sample{A: 3, B: "sample"}, expect: []string{"$", "$.B"}}, + {path: "$..", max: 2, data: []int{1, 2, 3}, expect: []string{"$", "$[0]"}}, + {path: "[0][1]", data: []any{[]int{1, 2, 3}}, expect: []string{"[0][1]"}}, + {path: "[0:2][1]", data: []any{[]int{1, 2, 3}}, expect: []string{"[0][1]"}}, + } { + if testing.Verbose() { + fmt.Printf("... %d: %s\n", i, d.path) + } + x, err := jp.ParseString(d.path) + tt.Nil(t, err) + locs := x.Locate(d.data, d.max) + var results []string + for _, loc := range locs { + results = append(results, loc.String()) + } + if !d.noSort { + sort.Strings(results) + } + diff := alt.Compare(d.expect, results) + if 0 < len(diff) { + t.Fatal(testDiffString(d.expect, results, diff)) + } + } +} + +func TestExprLocateBracket(t *testing.T) { + data := []any{map[string]any{"b": 1, "c": 2}, []any{1, 2, 3}} + x := jp.B().N(0).C("b") + tt.Equal(t, "[0]['b']", x.Locate(data, 0)[0].BracketString()) +} diff --git a/jp/modify.go b/jp/modify.go index 964bb46..30839b3 100644 --- a/jp/modify.go +++ b/jp/modify.go @@ -795,7 +795,8 @@ done: } } } else { - stack, _ = tf.EvalWithRoot(stack, prev, data).([]any) + ns, _ := tf.evalWithRoot(stack, prev, data) + stack, _ = ns.([]any) } case Descent: di, _ := stack[len(stack)-1].(fragIndex) diff --git a/jp/node.go b/jp/node.go index bec8a99..d7003a1 100644 --- a/jp/node.go +++ b/jp/node.go @@ -322,7 +322,8 @@ func (x Expr) GetNodes(n gen.Node) (results []gen.Node) { } case *Filter: before := len(stack) - stack, _ = tf.EvalWithRoot(stack, prev, n).([]gen.Node) + ns, _ := tf.evalWithRoot(stack, prev, n) + stack, _ = ns.([]gen.Node) if int(fi) == len(x)-1 { // last one for i := before; i < len(stack); i++ { results = append(results, stack[i]) @@ -589,7 +590,8 @@ func (x Expr) FirstNode(n gen.Node) (result gen.Node) { } case *Filter: before := len(stack) - stack, _ = tf.EvalWithRoot(stack, prev, n).([]gen.Node) + ns, _ := tf.evalWithRoot(stack, prev, n) + stack, _ = ns.([]gen.Node) if int(fi) == len(x)-1 { // last one if before < len(stack) { result := stack[before] diff --git a/jp/nth.go b/jp/nth.go index ba88d6b..8310735 100644 --- a/jp/nth.go +++ b/jp/nth.go @@ -85,3 +85,43 @@ func (f Nth) remove(value any) (out any, changed bool) { } return } + +func (f Nth) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + var ( + v any + has bool + ) + i := int(f) + switch td := data.(type) { + case []any: + if i < 0 { + i = len(td) + i + } + if 0 <= i && i < len(td) { + v = td[i] + has = true + } + case gen.Array: + if i < 0 { + i = len(td) + i + } + if 0 <= i && i < len(td) { + v = td[i] + has = true + } + case Indexed: + if i < 0 { + i = td.Size() + i + } + if 0 <= i && i < td.Size() { + v = td.ValueAtIndex(i) + has = true + } + default: + v, has = pp.reflectGetNth(td, i) + } + if has { + locs = locateNthChildHas(pp, Nth(i), v, rest, max) + } + return +} diff --git a/jp/ordered_test.go b/jp/ordered_test.go index 384c2c2..6091b31 100644 --- a/jp/ordered_test.go +++ b/jp/ordered_test.go @@ -90,3 +90,23 @@ type keydex struct { func (o *keydex) Size() int { return len(o.entries) } + +func orderedFromSimple(v any) (o any) { + switch tv := v.(type) { + case []any: + ind := indexed{} + for _, v2 := range tv { + ind.entries = append(ind.entries, &entry{value: orderedFromSimple(v2)}) + } + o = &ind + case map[string]any: + kd := keyed{} + for k, v2 := range tv { + kd.entries = append(kd.entries, &entry{key: k, value: orderedFromSimple(v2)}) + } + o = &kd + default: + o = v + } + return +} diff --git a/jp/root.go b/jp/root.go index a28f305..eb8dac3 100644 --- a/jp/root.go +++ b/jp/root.go @@ -11,3 +11,10 @@ func (f Root) Append(buf []byte, bracket, first bool) []byte { buf = append(buf, '$') return buf } + +func (f Root) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + if 0 < len(rest) { + locs = rest[0].locate(append(pp, f), data, rest[1:], max) + } + return +} diff --git a/jp/script.go b/jp/script.go index be7d6f7..12d1f5a 100644 --- a/jp/script.go +++ b/jp/script.go @@ -160,24 +160,30 @@ func (s *Script) String() string { func (s *Script) Match(data any) bool { stack := []any{} if node, ok := data.(gen.Node); ok { - stack, _ = s.EvalWithRoot(stack, gen.Array{node}, data).([]any) + ns, _ := s.evalWithRoot(stack, gen.Array{node}, data) + stack, _ = ns.([]any) } else { - stack, _ = s.EvalWithRoot(stack, []any{data}, data).([]any) + ns, _ := s.evalWithRoot(stack, []any{data}, data) + stack, _ = ns.([]any) } return 0 < len(stack) } // Eval is primarily used by the Expr parser but is public for testing. -func (s *Script) Eval(stack any, data any) any { - return s.EvalWithRoot(stack, data, nil) +func (s *Script) Eval(stack, data any) any { + ns, _ := s.evalWithRoot(stack, data, nil) + return ns } -// EvalWithRoot is primarily used by the Expr parser but is public for testing. -func (s *Script) EvalWithRoot(stack any, data, root any) any { +func (s *Script) evalWithRoot(stack, data, root any) (any, Expr) { // Checking the type each iteration adds 2.5% but allows code not to be // duplicated and not to call a separate function. Using just one more // function call for each iteration adds 6.5%. - var dlen int + var ( + dlen int + locKeys Expr + locs Expr + ) switch td := data.(type) { case []any: dlen = len(td) @@ -186,8 +192,9 @@ func (s *Script) EvalWithRoot(stack any, data, root any) any { case map[string]any: dlen = len(td) da := make([]any, 0, dlen) - for _, v := range td { + for k, v := range td { da = append(da, v) + locKeys = append(locKeys, Child(k)) } data = da case Indexed: @@ -198,24 +205,27 @@ func (s *Script) EvalWithRoot(stack any, data, root any) any { da := make([]any, dlen) for i, k := range keys { da[i], _ = td.ValueForKey(k) + locKeys = append(locKeys, Child(k)) } data = da case gen.Object: dlen = len(td) da := make(gen.Array, 0, dlen) - for _, v := range td { + for k, v := range td { da = append(da, v) + locKeys = append(locKeys, Child(k)) } data = da default: rv := reflect.ValueOf(td) if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array { - return stack + return stack, locs } dlen = rv.Len() da := make([]any, 0, dlen) for i := 0; i < dlen; i++ { da = append(da, rv.Index(i).Interface()) + locKeys = append(locKeys, Nth(i)) } data = da } @@ -634,10 +644,20 @@ func (s *Script) EvalWithRoot(stack any, data, root any) any { switch tstack := stack.(type) { case []any: tstack = append(tstack, v) + if 0 < len(locKeys) { + locs = append(locs, locKeys[vi]) + } else { + locs = append(locs, Nth(vi)) + } stack = tstack case []gen.Node: if n, ok := v.(gen.Node); ok { tstack = append(tstack, n) + if 0 < len(locKeys) { + locs = append(locs, locKeys[vi]) + } else { + locs = append(locs, Nth(vi)) + } stack = tstack } } @@ -646,7 +666,7 @@ func (s *Script) EvalWithRoot(stack any, data, root any) any { for i := range sstack { sstack[i] = nil } - return stack + return stack, locs } // Inspect the script. diff --git a/jp/set.go b/jp/set.go index d93684a..9a3a8d5 100644 --- a/jp/set.go +++ b/jp/set.go @@ -1145,7 +1145,8 @@ func (x Expr) set(data, value any, fun string, one bool) error { } } case *Filter: - stack, _ = tf.EvalWithRoot(stack, prev, data).([]any) + ns, _ := tf.evalWithRoot(stack, prev, data) + stack, _ = ns.([]any) case Root: stack = append(stack, data) case At, Bracket: diff --git a/jp/slice.go b/jp/slice.go index 7881368..bd24b04 100644 --- a/jp/slice.go +++ b/jp/slice.go @@ -345,3 +345,210 @@ func inStep(i, start, end, step int) bool { } return end <= i && i <= start && (i-end)%-step == 0 } + +func (f Slice) startEndStep(size int) (start, end, step int) { + start = 0 + end = maxEnd + step = 1 + if 0 < len(f) { + start = f[0] + } + if 1 < len(f) { + end = f[1] + } + if 2 < len(f) { + step = f[2] + if step == 0 { + return + } + } + if start < 0 { + start = size + start + } else if size <= start { + start = size - 1 + } + if start < 0 { + start = 0 + } + if end < 0 { + end = size + end + 1 + if end < 0 && step < 0 { + end = -1 + } + } else if size < end { + end = size + } + return +} + +func (f Slice) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + switch td := data.(type) { + case []any: + start, end, step := f.startEndStep(len(td)) + if step == 0 { + return + } + if 0 < step { + if len(rest) == 0 { // last one + for i := start; i < end; i += step { + locs = locateAppendFrag(locs, pp, Nth(i)) + } + } else { + cp := append(pp, nil) // place holder + for i := start; i < end; i += step { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, td[i], rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } else { + if len(rest) == 0 { // last one + for i := start; end < i; i += step { + locs = locateAppendFrag(locs, pp, Nth(i)) + } + } else { + cp := append(pp, nil) // place holder + for i := start; end < i; i += step { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, td[i], rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } + case gen.Array: + start, end, step := f.startEndStep(len(td)) + if step == 0 { + return + } + if 0 < step { + if len(rest) == 0 { // last one + for i := start; i < end; i += step { + locs = locateAppendFrag(locs, pp, Nth(i)) + } + } else { + cp := append(pp, nil) // place holder + for i := start; i < end; i += step { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, td[i], rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } else { + if len(rest) == 0 { // last one + for i := start; end < i; i += step { + locs = locateAppendFrag(locs, pp, Nth(i)) + } + } else { + cp := append(pp, nil) // place holder + for i := start; end < i; i += step { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, td[i], rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } + case Indexed: + start, end, step := f.startEndStep(td.Size()) + if step == 0 { + return + } + if 0 < step { + if len(rest) == 0 { // last one + for i := start; i < end; i += step { + locs = locateAppendFrag(locs, pp, Nth(i)) + } + } else { + cp := append(pp, nil) // place holder + for i := start; i < end; i += step { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, td.ValueAtIndex(i), rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } else { + if len(rest) == 0 { // last one + for i := start; end < i; i += step { + locs = locateAppendFrag(locs, pp, Nth(i)) + } + } else { + cp := append(pp, nil) // place holder + for i := start; end < i; i += step { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, td.ValueAtIndex(i), rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } + case nil: + // no match + default: + rd := reflect.ValueOf(data) + rt := rd.Type() + switch rt.Kind() { + case reflect.Slice, reflect.Array: + start, end, step := f.startEndStep(rd.Len()) + if 0 < step { + if len(rest) == 0 { // last one + for i := start; i < end; i += step { + rv := rd.Index(i) + if rv.CanInterface() { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } + } else { + cp := append(pp, nil) // place holder + for i := start; i < end; i += step { + cp[len(pp)] = Nth(i) + rv := rd.Index(i) + if rv.CanInterface() { + locs = locateContinueFrag(locs, cp, rv.Interface(), rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } + } else { + if len(rest) == 0 { // last one + for i := start; end < i; i += step { + rv := rd.Index(i) + if rv.CanInterface() { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } + } else { + cp := append(pp, nil) // place holder + for i := start; end < i; i += step { + cp[len(pp)] = Nth(i) + rv := rd.Index(i) + if rv.CanInterface() { + locs = locateContinueFrag(locs, cp, rv.Interface(), rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } + } + } + } + return +} diff --git a/jp/union.go b/jp/union.go index fee01a9..6a04bd1 100644 --- a/jp/union.go +++ b/jp/union.go @@ -259,3 +259,70 @@ func (f Union) remove(value any) (out any, changed bool) { } return } + +func (f Union) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + var ( + v any + has bool + lf Frag + ) + for _, u := range f { + has = false + switch tu := u.(type) { + case string: + switch td := data.(type) { + case map[string]any: + v, has = td[tu] + case Keyed: + v, has = td.ValueForKey(tu) + case gen.Object: + v, has = td[tu] + default: + v, has = pp.reflectGetChild(td, tu) + } + lf = Child(tu) + case int64: + i := int(tu) + switch td := data.(type) { + case []any: + if i < 0 { + i = len(td) + i + } + if 0 <= i && i < len(td) { + v = td[i] + has = true + } + case Indexed: + if i < 0 { + i = td.Size() + i + } + if 0 <= i && i < td.Size() { + v = td.ValueAtIndex(i) + has = true + } + case gen.Array: + if i < 0 { + i = len(td) + i + } + if 0 <= i && i < len(td) { + v = td[i] + has = true + } + default: + v, has = pp.reflectGetNth(td, i) + } + lf = Nth(i) + } + if has { + if len(rest) == 0 { // last one + locs = locateAppendFrag(locs, pp, lf) + } else { + locs = locateContinueFrag(locs, append(pp, lf), v, rest, max) + } + if 0 < max && max <= len(locs) { + break + } + } + } + return +} diff --git a/jp/wildcard.go b/jp/wildcard.go index 7300599..fcb2636 100644 --- a/jp/wildcard.go +++ b/jp/wildcard.go @@ -127,3 +127,180 @@ func (f Wildcard) removeOne(value any) (out any, changed bool) { } return } + +func (f Wildcard) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + switch td := data.(type) { + case map[string]any: + if len(rest) == 0 { // last one + for k := range td { + locs = locateAppendFrag(locs, pp, Child(k)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for k, v := range td { + cp[len(pp)] = Child(k) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case []any: + if len(rest) == 0 { // last one + for i := range td { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for i, v := range td { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case gen.Object: + if len(rest) == 0 { // last one + for k := range td { + locs = locateAppendFrag(locs, pp, Child(k)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for k, v := range td { + cp[len(pp)] = Child(k) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case gen.Array: + if len(rest) == 0 { // last one + for i := range td { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for i, v := range td { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case Keyed: + keys := td.Keys() + if len(rest) == 0 { // last one + for _, k := range keys { + locs = locateAppendFrag(locs, pp, Child(k)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for _, k := range keys { + v, _ := td.ValueForKey(k) + cp[len(pp)] = Child(k) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case Indexed: + size := td.Size() + if len(rest) == 0 { // last one + for i := 0; i < size; i++ { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for i := 0; i < size; i++ { + v := td.ValueAtIndex(i) + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case nil: + // no match + default: + rd := reflect.ValueOf(data) + rt := rd.Type() + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + rd = rd.Elem() + } + if len(rest) == 0 { // last one + switch rt.Kind() { + case reflect.Struct: + for i := rd.NumField() - 1; 0 <= i; i-- { + rv := rd.Field(i) + if rv.CanInterface() { + locs = locateAppendFrag(locs, pp, Child(rt.Field(i).Name)) + if 0 < max && max <= len(locs) { + break + } + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < rd.Len(); i++ { + rv := rd.Index(i) + if rv.CanInterface() { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } + } + } else { + cp := append(pp, nil) // place holder + switch rt.Kind() { + case reflect.Struct: + for i := rd.NumField() - 1; 0 <= i; i-- { + rv := rd.Field(i) + if rv.CanInterface() { + cp[len(pp)] = Child(rt.Field(i).Name) + locs = locateContinueFrag(locs, cp, rv.Interface(), rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < rd.Len(); i++ { + rv := rd.Index(i) + if rv.CanInterface() { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, rv.Interface(), rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + } + } + } + return +}