Skip to content

Commit

Permalink
Expand public spec APIs, fix FilterSelector string
Browse files Browse the repository at this point in the history
*   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)
  • Loading branch information
theory committed Nov 13, 2024
1 parent 4fdb132 commit 5a08ccb
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 26 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions spec/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
Expand Down
4 changes: 4 additions & 0 deletions spec/segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
3 changes: 3 additions & 0 deletions spec/segment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 22 additions & 12 deletions spec/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}
}
Expand All @@ -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 }
22 changes: 11 additions & 11 deletions spec/selector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -543,7 +543,7 @@ func TestFilterSelector(t *testing.T) {
name: "no_filter",
filter: Filter(LogicalOr{}),
exp: []any{},
str: "",
str: "?",
},
{
name: "array_root",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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,
},
{
Expand All @@ -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,
},
{
Expand All @@ -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",
Expand All @@ -603,7 +603,7 @@ func TestFilterSelector(t *testing.T) {
}}}),
current: []any{[]any{42}},
exp: []any{},
str: `@[1]`,
str: `?@[1]`,
},
{
name: "object_current",
Expand All @@ -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",
Expand All @@ -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) {
Expand Down

0 comments on commit 5a08ccb

Please sign in to comment.