From 5a08ccb43a18a83e5a16897be34c54367758595e Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" Date: Wed, 13 Nov 2024 11:49:19 -0500 Subject: [PATCH] Expand public spec APIs, fix FilterSelector string * Add `spec.Filter.Eval` to allow public evaluation of a single JSON node. Used internally by `spec.FilterSelector.Select`. * Add `spec.Segment.IsDescendant` to tell wether a segments selects just from the current child node or also recursively selects from all of its descendants. * Make `spec.SliceSelector.Bounds` public. * Make the underlying struct defining `spec.Wildcard` public with the name `spec.WildcardSelector`. Other changes: * Add missing "?" to the stringification of `spec.FilterSelector`. * Upgrade to `golangci-lint` v1.62 and disable `gosec` G602 false positives (securego/gosec#1250) --- CHANGELOG.md | 22 ++++++++++++++++++++++ Makefile | 2 +- spec/query_test.go | 4 ++-- spec/segment.go | 4 ++++ spec/segment_test.go | 3 +++ spec/selector.go | 34 ++++++++++++++++++++++------------ spec/selector_test.go | 22 +++++++++++----------- 7 files changed, 65 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d1b4e0..7480e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ All notable changes to this project will be documented in this file. It uses the [Semantic Versioning]: https://semver.org/spec/v2.0.0.html "Semantic Versioning 2.0.0" +## [v0.1.3] — Unreleased + +### ⚡ Improvements + +* Added `spec.Filter.Eval` to allow public evaluation of a single JSON node. + Used internally by `spec.FilterSelector.Select`. +* Added `spec.Segment.IsDescendant` to tell wether a segments selects just + from the current child node or also recursively selects from all of its + descendants. + +### 🪲 Bug Fixes + +* Added missing "?" to the stringification of `spec.FilterSelector`. + +### 📔 Notes + +* Made `spec.SliceSelector.Bounds` public. +* Made the underlying struct defining `spec.Wildcard` public, named it + `spec.WildcardSelector`. + + [v0.1.3]: https://github.com/theory/jsonpath/compare/v0.1.2...v0.1.3 + ## [v0.1.2] — 2024-10-28 ### 🪲 Bug Fixes diff --git a/Makefile b/Makefile index 0303c09..39a8bf3 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ brew-lint-depends: .PHONY: debian-lint-depends # Install linting tools on Debian debian-lint-depends: - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/bin v1.59.0 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/bin v1.62.0 .PHONY: install-generators # Install Go code generators install-generators: diff --git a/spec/query_test.go b/spec/query_test.go index 9324fa9..33fb13b 100644 --- a/spec/query_test.go +++ b/spec/query_test.go @@ -787,14 +787,14 @@ func TestQueryDescendants(t *testing.T) { for _, tc := range []queryTestCase{ { - name: "descendent_name", + name: "descendant_name", segs: []*Segment{Descendant(Name("j"))}, input: json, exp: []any{1, 4}, rand: true, }, { - name: "un_descendent_name", + name: "un_descendant_name", segs: []*Segment{Descendant(Name("o"))}, input: json, exp: []any{map[string]any{"j": 1, "k": 2}}, diff --git a/spec/segment.go b/spec/segment.go index e907e99..e737471 100644 --- a/spec/segment.go +++ b/spec/segment.go @@ -85,3 +85,7 @@ func (s *Segment) isSingular() bool { } return s.selectors[0].isSingular() } + +// IsDescendant returns true if the segment is a descendant selector that +// recursively select the children of a JSON value. +func (s *Segment) IsDescendant() bool { return s.descendant } diff --git a/spec/segment_test.go b/spec/segment_test.go index 9f37494..a6a6b0d 100644 --- a/spec/segment_test.go +++ b/spec/segment_test.go @@ -83,6 +83,7 @@ func TestSegmentString(t *testing.T) { t.Parallel() a.Equal(tc.str, tc.seg.String()) a.Equal(tc.sing, tc.seg.isSingular()) + a.Equal(tc.seg.descendant, tc.seg.IsDescendant()) }) } } @@ -237,6 +238,7 @@ func TestSegmentQuery(t *testing.T) { t.Parallel() a.Equal(tc.seg.selectors, tc.seg.Selectors()) a.Equal(tc.sing, tc.seg.isSingular()) + a.Equal(tc.seg.descendant, tc.seg.IsDescendant()) if tc.rand { a.ElementsMatch(tc.exp, tc.seg.Select(tc.src, nil)) } else { @@ -433,6 +435,7 @@ func TestDescendantSegmentQuery(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() a.False(tc.seg.isSingular()) + a.True(tc.seg.IsDescendant()) if tc.rand { a.ElementsMatch(tc.exp, tc.seg.Select(tc.src, nil)) } else { diff --git a/spec/selector.go b/spec/selector.go index 9e5f1c4..c38308e 100644 --- a/spec/selector.go +++ b/spec/selector.go @@ -59,28 +59,28 @@ func (n Name) Select(input, _ any) []any { return make([]any, 0) } -// wc is the underlying nil value used by [Wildcard]. -type wc struct{} +// WildcardSelector is the underlying nil value used by [Wildcard]. +type WildcardSelector struct{} // Wildcard is a wildcard selector, e.g., * or [*]. // //nolint:gochecknoglobals -var Wildcard = wc{} +var Wildcard = WildcardSelector{} // writeTo writes "*" to buf. -func (wc) writeTo(buf *strings.Builder) { buf.WriteByte('*') } +func (WildcardSelector) writeTo(buf *strings.Builder) { buf.WriteByte('*') } // String returns "*". -func (wc) String() string { return "*" } +func (WildcardSelector) String() string { return "*" } // isSingular returns false because a wild card can select more than one value // from an object or array. Defined by the [Selector] interface. -func (wc) isSingular() bool { return false } +func (WildcardSelector) isSingular() bool { return false } // Select selects the values from input and returns them in a slice. Returns // an empty slice if input is not []any map[string]any. Defined by the // [Selector] interface. -func (wc) Select(input, _ any) []any { +func (WildcardSelector) Select(input, _ any) []any { switch val := input.(type) { case []any: return val @@ -152,6 +152,7 @@ func Slice(args ...any) SliceSelector { s := SliceSelector{0, math.MaxInt, 1} switch len(args) - 1 { case stepArg: + //nolint:gosec // disable G602 https://github.com/securego/gosec/issues/1250 switch step := args[stepArg].(type) { case int: s.step = step @@ -162,6 +163,7 @@ func Slice(args ...any) SliceSelector { } fallthrough case endArg: + //nolint:gosec // disable G602 https://github.com/securego/gosec/issues/1250 switch end := args[endArg].(type) { case int: s.end = end @@ -218,7 +220,7 @@ func (s SliceSelector) String() string { // [Selector] interface. func (s SliceSelector) Select(input, _ any) []any { if val, ok := input.([]any); ok { - lower, upper := s.bounds(len(val)) + lower, upper := s.Bounds(len(val)) res := make([]any, 0, len(val)) switch { case s.step > 0: @@ -250,9 +252,9 @@ func (s SliceSelector) Step() int { return s.step } -// bounds returns the lower and upper bounds for selecting from a slice of +// Bounds returns the lower and upper bounds for selecting from a slice of // length. -func (s SliceSelector) bounds(length int) (int, int) { +func (s SliceSelector) Bounds(length int) (int, int) { start := normalize(s.start, length) end := normalize(s.end, length) switch { @@ -293,6 +295,7 @@ func (f *FilterSelector) String() string { // writeTo writes a string representation of f to buf. func (f *FilterSelector) writeTo(buf *strings.Builder) { + buf.WriteRune('?') f.LogicalOr.writeTo(buf) } @@ -304,13 +307,13 @@ func (f *FilterSelector) Select(current, root any) []any { switch current := current.(type) { case []any: for _, v := range current { - if f.LogicalOr.testFilter(v, root) { + if f.Eval(v, root) { ret = append(ret, v) } } case map[string]any: for _, v := range current { - if f.LogicalOr.testFilter(v, root) { + if f.Eval(v, root) { ret = append(ret, v) } } @@ -319,6 +322,13 @@ func (f *FilterSelector) Select(current, root any) []any { return ret } +// Eval evaluates the f's logical expression against node and root. Used +// [Select] as it iterates over nodes, and always passes the root value($) for +// filter expressions that reference it. +func (f *FilterSelector) Eval(node, root any) bool { + return f.LogicalOr.testFilter(node, root) +} + // isSingular returns false because Filters can return more than one value. // Defined by the [Selector] interface. func (f *FilterSelector) isSingular() bool { return false } diff --git a/spec/selector_test.go b/spec/selector_test.go index 36d6f18..1cdc68b 100644 --- a/spec/selector_test.go +++ b/spec/selector_test.go @@ -151,7 +151,7 @@ func TestSliceBounds(t *testing.T) { json := []any{"a", "b", "c", "d", "e", "f", "g"} extract := func(s SliceSelector) []any { - lower, upper := s.bounds(len(json)) + lower, upper := s.Bounds(len(json)) res := make([]any, 0, len(json)) switch { case s.step > 0: @@ -273,7 +273,7 @@ func TestSliceBounds(t *testing.T) { t.Parallel() a.False(tc.slice.isSingular()) for _, lc := range tc.cases { - lower, upper := tc.slice.bounds(lc.length) + lower, upper := tc.slice.Bounds(lc.length) a.Equal(lc.lower, lower) a.Equal(lc.upper, upper) } @@ -543,7 +543,7 @@ func TestFilterSelector(t *testing.T) { name: "no_filter", filter: Filter(LogicalOr{}), exp: []any{}, - str: "", + str: "?", }, { name: "array_root", @@ -553,7 +553,7 @@ func TestFilterSelector(t *testing.T) { root: []any{42, true, "hi"}, current: map[string]any{"x": 2}, exp: []any{2}, - str: `$[0]`, + str: `?$[0]`, }, { name: "array_root_false", @@ -563,7 +563,7 @@ func TestFilterSelector(t *testing.T) { root: []any{42, true, "hi"}, current: map[string]any{"x": 2}, exp: []any{}, - str: `$[4]`, + str: `?$[4]`, }, { name: "object_root", @@ -573,7 +573,7 @@ func TestFilterSelector(t *testing.T) { root: map[string]any{"x": 42, "y": "hi"}, current: map[string]any{"a": 2, "b": 3}, exp: []any{2, 3}, - str: `$["y"]`, + str: `?$["y"]`, rand: true, }, { @@ -584,7 +584,7 @@ func TestFilterSelector(t *testing.T) { root: map[string]any{"x": 42, "y": "hi"}, current: map[string]any{"a": 2, "b": 3}, exp: []any{}, - str: `$["z"]`, + str: `?$["z"]`, rand: true, }, { @@ -594,7 +594,7 @@ func TestFilterSelector(t *testing.T) { }}}), current: []any{[]any{42}}, exp: []any{[]any{42}}, - str: `@[0]`, + str: `?@[0]`, }, { name: "array_current_false", @@ -603,7 +603,7 @@ func TestFilterSelector(t *testing.T) { }}}), current: []any{[]any{42}}, exp: []any{}, - str: `@[1]`, + str: `?@[1]`, }, { name: "object_current", @@ -612,7 +612,7 @@ func TestFilterSelector(t *testing.T) { }}}), current: []any{map[string]any{"x": 42}}, exp: []any{map[string]any{"x": 42}}, - str: `@["x"]`, + str: `?@["x"]`, }, { name: "object_current_false", @@ -621,7 +621,7 @@ func TestFilterSelector(t *testing.T) { }}}), current: []any{map[string]any{"x": 42}}, exp: []any{}, - str: `@["y"]`, + str: `?@["y"]`, }, } { t.Run(tc.name, func(t *testing.T) {