diff --git a/CHANGELOG.md b/CHANGELOG.md index 4799cac..c1c30fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,30 @@ All notable changes to this project will be documented in this file. It uses the ## [v0.11.1] — Unreleased +### ⚡ Improvements + +* Added support for selecting values from any type of slice or string-keyed + map, not just `[]any` or `map[string]any`. Internally it still prefers + `[]any` and `map[string]any`, to optimize for values decoded by + encoding/json, but it now falls back on reflection to detect any other + kind of slice or string-keyed map. Thanks to @ndsboy for the prompt (#26). +* Updated result set creation to allocate more slots for results when the + number of results are unknown, based on the number of selectors or items + to select from, to improve memory efficiency. Encouraged by the recent + [Go blog post] describing the advantages of this pattern. + ### ⬆️ Dependency Updates -* Upgraded to `golangci-lint` v2.11.1 and made suggested slice allocation - optimization +* Upgraded to `golangci-lint` v2.11.4 and made suggested slice allocation + optimizations. + +### 📚 Documentation + +* Fixed some broken Go Doc links. [v0.11.1]: https://github.com/theory/jsonpath/compare/v0.11.0...v0.11.1 + [Go blog post]: https://go.dev/blog/allocation-optimizations + "The Go Blog: Allocating on the Stack" ## [v0.11.0] — 2026-03-02 diff --git a/Makefile b/Makefile index 0f9a9ab..0326b5f 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,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 v2.11.1 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/bin v2.11.4 .PHONY: install-generators # Install Go code generators install-generators: diff --git a/README.md b/README.md index 8b733bf..8985b80 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ RFC 9535 JSONPath in Go [![⚖️ MIT]][mit] [![📚 Docs]][docs] [![🗃️ Report Card]][card] [![🛠️ Build Status]][ci] [![📊 Coverage]][cov] The jsonpath package provides [RFC 9535 JSONPath] functionality in Go. +It operates on any type of slice or string-keyed map. ## Learn More diff --git a/path.go b/path.go index a13b4c8..90c4324 100644 --- a/path.go +++ b/path.go @@ -1,4 +1,5 @@ // Package jsonpath implements RFC 9535 JSONPath query expressions. +// It operates on any type of slice or string-keyed map. package jsonpath import ( @@ -160,7 +161,7 @@ func (list NodeList) All() iter.Seq[any] { } // LocatedNodeList is a list of nodes selected by a JSONPath query, along with -// their [NormalizedPath] locations. Returned by [Path.SelectLocated]. +// their [spec.NormalizedPath] locations. Returned by [Path.SelectLocated]. type LocatedNodeList []*spec.LocatedNode // All returns an iterator over all the nodes in list. @@ -188,7 +189,8 @@ func (list LocatedNodeList) Nodes() iter.Seq[any] { } } -// Paths returns an iterator over all the [NormalizedPath] values in list. +// Paths returns an iterator over all the [spec.NormalizedPath] values in +// list. func (list LocatedNodeList) Paths() iter.Seq[spec.NormalizedPath] { return func(yield func(spec.NormalizedPath) bool) { for _, v := range list { @@ -199,10 +201,10 @@ func (list LocatedNodeList) Paths() iter.Seq[spec.NormalizedPath] { } } -// Deduplicate deduplicates the nodes in list based on their [NormalizedPath] -// values, modifying the contents of list. It returns the modified list, which -// may have a shorter length, and zeroes the elements between the new length -// and the original length. +// Deduplicate deduplicates the nodes in list based on their +// [spec.NormalizedPath] values, modifying the contents of list. It returns +// the modified list, which may have a shorter length, and zeroes the elements +// between the new length and the original length. func (list LocatedNodeList) Deduplicate() LocatedNodeList { if len(list) <= 1 { return list @@ -221,7 +223,7 @@ func (list LocatedNodeList) Deduplicate() LocatedNodeList { return slices.Clip(uniq) } -// Sort sorts list by the [NormalizedPath] of each node. +// Sort sorts list by the [spec.NormalizedPath] of each node. func (list LocatedNodeList) Sort() { slices.SortFunc(list, func(a, b *spec.LocatedNode) int { return a.Path.Compare(b.Path) diff --git a/path_example_test.go b/path_example_test.go index 3f58768..11275a9 100644 --- a/path_example_test.go +++ b/path_example_test.go @@ -115,7 +115,7 @@ func ExampleLocatedNodeList() { func ExampleLocatedNodeList_Deduplicate() { // Load some JSON. - pallet := map[string]any{"colors": []any{"red", "blue"}} + pallet := map[string]any{"colors": []string{"red", "blue"}} // Parse a JSONPath and select from the input. p := jsonpath.MustParse("$.colors[0, 1, 1, 0]") @@ -133,7 +133,7 @@ func ExampleLocatedNodeList_Deduplicate() { func ExampleLocatedNodeList_Sort() { // Load some JSON. - pallet := map[string]any{"colors": []any{"red", "blue", "green"}} + pallet := map[string]any{"colors": []string{"red", "blue", "green"}} // Parse a JSONPath and select from the input. p := jsonpath.MustParse("$.colors[2, 0, 1]") @@ -166,7 +166,7 @@ func ExampleLocatedNodeList_Sort() { func ExampleLocatedNodeList_Clone() { // Load some JSON. - items := []any{1, 2, 3, 4, 5} + items := []int{1, 2, 3, 4, 5} // Parse a JSONPath and select from the input. p := jsonpath.MustParse("$[2, 0, 1, 0, 1]") @@ -259,9 +259,9 @@ func ExampleWithRegistry() { // Do any of these arrays start with 6? input := []any{ - []any{1, 2, 3, 4, 5}, - []any{6, 7, 8, 9}, - []any{4, 8, 12}, + []int{1, 2, 3, 4, 5}, + []int{6, 7, 8, 9}, + []int{4, 8, 12}, } nodes := path.Select(input) fmt.Printf("%v\n", nodes) diff --git a/registry/funcs.go b/registry/funcs.go index 8f9a695..52fb595 100644 --- a/registry/funcs.go +++ b/registry/funcs.go @@ -3,6 +3,7 @@ package registry import ( "errors" "fmt" + "reflect" "regexp" "regexp/syntax" "unicode/utf8" @@ -32,8 +33,8 @@ func checkLengthArgs(args []spec.FuncExprArg) error { // - if jv[0] is nil, the result is nil // - If jv[0] is a string, the result is the number of Unicode scalar values // in the string. -// - If jv[0] is a []any, the result is the number of elements in the slice. -// - If jv[0] is an map[string]any, the result is the number of members in +// - If jv[0] is a slice, the result is the number of elements in the slice. +// - If jv[0] is a string-keyed map, the result is the number of members in // the map. // - For any other value, the result is nil. func lengthFunc(jv []spec.PathValue) spec.PathValue { @@ -50,7 +51,18 @@ func lengthFunc(jv []spec.PathValue) spec.PathValue { case map[string]any: return spec.Value(len(v)) default: - return nil + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Slice: + return spec.Value(val.Len()) + case reflect.Map: + if val.Type().Key().Kind() == reflect.String { + return spec.Value(val.Len()) + } + return nil + default: + return nil + } } } diff --git a/registry/funcs_test.go b/registry/funcs_test.go index 2270b1d..9f4d5a9 100644 --- a/registry/funcs_test.go +++ b/registry/funcs_test.go @@ -98,6 +98,31 @@ func TestLengthFunc(t *testing.T) { vals: []spec.PathValue{spec.LogicalFalse}, err: "cannot convert LogicalType to ValueType", }, + { + test: "int_array", + vals: []spec.PathValue{spec.Value([]int{1, 2, 3, 4, 5})}, + exp: 5, + }, + { + test: "string_array", + vals: []spec.PathValue{spec.Value([]string{"x", "y", "z"})}, + exp: 3, + }, + { + test: "int_object", + vals: []spec.PathValue{spec.Value(map[string]int{"x": 1, "y": 0, "z": 2})}, + exp: 3, + }, + { + test: "string_object", + vals: []spec.PathValue{spec.Value(map[string]string{"x": "x", "y": "y"})}, + exp: 2, + }, + { + test: "int_keyed_object", + vals: []spec.PathValue{spec.Value(map[int]string{1: "x", 2: "c"})}, + exp: -1, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() diff --git a/registry/registry.go b/registry/registry.go index c38fe38..6a52e9c 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -46,7 +46,7 @@ func New() *Registry { } } -// ErrRegister errors are returned by [Register]. +// ErrRegister errors are returned by [Registry.Register]. var ErrRegister = errors.New("register") // Register registers a function extension. The parameters are: diff --git a/spec/function.go b/spec/function.go index 17034c2..a8e9a49 100644 --- a/spec/function.go +++ b/spec/function.go @@ -46,8 +46,8 @@ type PathValue interface { // NodesType defines a node list (a list of JSON values) for a function // expression parameters or results, as defined by [RFC 9535 Section 2.4.1]. // It can also be used in filter expressions. The underlying types should be -// string, integer, float, [json.Number], nil, true, false, []any, or -// map[string]any. Interfaces implemented: +// string, integer, float, [json.Number], nil, true, false, slice, or +// string-keyed map. Interfaces implemented: // // - [PathValue] // - [fmt.Stringer] @@ -57,7 +57,7 @@ type NodesType []any // Nodes creates a NodesType that contains val, all of which should be the Go // equivalent of the JSON data types: string, integer, float, [json.Number], -// nil, true, false, []any, or map[string]any. +// nil, true, false, slice, or string-keyed map. func Nodes(val ...any) NodesType { return NodesType(val) } @@ -83,7 +83,7 @@ func NodesFrom(value PathValue) NodesType { case *ValueType: return NodesType([]any{v.any}) case nil: - return NodesType([]any{}) + return NodesType(make([]any, 0)) case LogicalType: panic("cannot convert LogicalType to NodesType") default: @@ -137,8 +137,8 @@ func (LogicalType) FuncType() FuncType { return FuncLogical } // LogicalFrom converts value to a [LogicalType] and panics if it cannot. Use // in [github.com/theory/jsonpath/registry.Registry.Register] [Evaluator] // functions. Avoid the panic by returning an error from the accompanying -// [Validator] function when [FuncExprArg.ConvertsToLogical] returns false for -// the [FuncExprArg] that returns value. +// [Validator] function when [FuncExprArg.ConvertsTo] returns false for the +// [FuncExprArg] that returns value. // // Converts each implementation of [PathValue] as follows: // - [LogicalType]: returns value @@ -170,7 +170,7 @@ func (lt LogicalType) writeTo(buf *strings.Builder) { // ValueType encapsulates a JSON value for a function expression parameter or // result, as defined by [RFC 9535 Section 2.4.1]. It can also be used as in // filter expression. The underlying value should be a string, integer, -// [json.Number], float, nil, true, false, []any, or map[string]any. A nil +// [json.Number], float, nil, true, false, slice, or string-keyed map. A nil // ValueType pointer indicates no value. Interfaces implemented: // // - [PathValue] @@ -184,7 +184,7 @@ type ValueType struct { // Value returns a new [ValueType] for val, which must be the Go equivalent of // a JSON data type: string, integer, float, [json.Number], nil, true, false, -// []any, or map[string]any. +// slice, or string-keyed map. func Value(val any) *ValueType { return &ValueType{val} } @@ -201,8 +201,8 @@ func (*ValueType) FuncType() FuncType { return FuncValue } // ValueFrom converts value to a [ValueType] and panics if it cannot. Use in // [github.com/theory/jsonpath/registry.Registry.Register] [Evaluator] // functions. Avoid the panic by returning an error from the accompanying -// [Validator] function when [FuncExprArg.ConvertsToValue] returns false for -// the [FuncExprArg] that returns value. +// [Validator] function when [FuncExprArg.ConvertsTo] returns false for the +// [FuncExprArg] that returns value. // // Converts each implementation of [PathValue] as follows: // - [ValueType]: returns value diff --git a/spec/query_test.go b/spec/query_test.go index 9f14630..5d793bb 100644 --- a/spec/query_test.go +++ b/spec/query_test.go @@ -288,6 +288,34 @@ func TestQueryObject(t *testing.T) { exp: []any{}, loc: []*LocatedNode{}, }, + { + test: "string_map", + resType: FuncValue, + input: map[string]string{"x": "hi", "y": "y"}, + segs: []*Segment{Child(Name("x"))}, + exp: []any{"hi"}, + loc: []*LocatedNode{ + {Path: Normalized(Name("x")), Node: "hi"}, + }, + }, + { + test: "int_map", + resType: FuncValue, + input: map[string]int{"x": 42, "y": 99}, + segs: []*Segment{Child(Name("x"))}, + exp: []any{42}, + loc: []*LocatedNode{ + {Path: Normalized(Name("x")), Node: 42}, + }, + }, + { + test: "int_keyed_map", + resType: FuncValue, + input: map[int]string{42: "hi", 99: "y"}, + segs: []*Segment{Child(Name("42"))}, + exp: []any{}, + loc: []*LocatedNode{}, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() @@ -631,6 +659,22 @@ func TestQueryArray(t *testing.T) { {Path: Normalized(Index(0), Name("x"), Index(1)), Node: 2}, }, }, + { + test: "string_slice_index", + resType: FuncValue, + segs: []*Segment{Child(Index(0))}, + input: []string{"x", "y"}, + exp: []any{"x"}, + loc: []*LocatedNode{{Path: Normalized(Index(0)), Node: "x"}}, + }, + { + test: "int_slice_index", + resType: FuncValue, + segs: []*Segment{Child(Index(1))}, + input: []int{0, 42}, + exp: []any{42}, + loc: []*LocatedNode{{Path: Normalized(Index(1)), Node: 42}}, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() @@ -1101,6 +1145,26 @@ func TestQuerySlice(t *testing.T) { {Path: Normalized(Index(0), Index(1)), Node: 42}, }, }, + { + test: "string_slice", + segs: []*Segment{Child(Slice())}, + input: []string{"x", "y"}, + exp: []any{"x", "y"}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: "x"}, + {Path: Normalized(Index(1)), Node: "y"}, + }, + }, + { + test: "int_slice", + segs: []*Segment{Child(Slice())}, + input: []int{42, 99}, + exp: []any{42, 99}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: 42}, + {Path: Normalized(Index(1)), Node: 99}, + }, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() diff --git a/spec/segment.go b/spec/segment.go index a13f70b..4f5d1e2 100644 --- a/spec/segment.go +++ b/spec/segment.go @@ -1,6 +1,8 @@ package spec import ( + "reflect" + "slices" "strings" ) @@ -57,62 +59,136 @@ func (s *Segment) String() string { // Select selects and returns values from current or root, for each of s's // selectors. Defined by the [Selector] interface. func (s *Segment) Select(current, root any) []any { - ret := []any{} + ret := make([]any, 0, len(s.selectors)) for _, sel := range s.selectors { ret = append(ret, sel.Select(current, root)...) } if s.descendant { ret = append(ret, s.descend(current, root)...) } - return ret + return slices.Clip(ret) } // SelectLocated selects and returns values as [LocatedNode] values from // current or root for each of seg's selectors. Defined by the [Selector] // interface. func (s *Segment) SelectLocated(current, root any, parent NormalizedPath) []*LocatedNode { - ret := []*LocatedNode{} + ret := make([]*LocatedNode, 0, len(s.selectors)) for _, sel := range s.selectors { ret = append(ret, sel.SelectLocated(current, root, parent)...) } if s.descendant { ret = append(ret, s.descendLocated(current, root, parent)...) } - return ret + return slices.Clip(ret) } // descend recursively executes [Segment.Select] for each value in current // and/or root and its descendants and returns the results. func (s *Segment) descend(current, root any) []any { - ret := []any{} switch val := current.(type) { case []any: + ret := make([]any, 0, len(val)) for _, v := range val { ret = append(ret, s.Select(v, root)...) } + return slices.Clip(ret) case map[string]any: + ret := make([]any, 0, len(val)) for _, v := range val { ret = append(ret, s.Select(v, root)...) } + return slices.Clip(ret) + default: + value := reflect.ValueOf(current) + switch value.Kind() { + case reflect.Slice: + // Descend into any other slice that contains slices or maps. + switch value.Type().Elem().Kind() { + case reflect.Slice, reflect.Map: + ret := make([]any, 0, value.Len()) + for i := range value.Len() { + ret = append(ret, s.Select(value.Index(i).Interface(), root)...) + } + return slices.Clip(ret) + default: + return make([]any, 0) + } + case reflect.Map: + // Descend into any map[string]* that contains slices or maps. + if value.Type().Key().Kind() != reflect.String { + return make([]any, 0) + } + switch value.Type().Elem().Kind() { + case reflect.Slice, reflect.Map: + ret := make([]any, 0, value.Len()) + for _, k := range value.MapKeys() { + ret = append(ret, s.Select(value.MapIndex(k).Interface(), root)...) + } + return slices.Clip(ret) + default: + return make([]any, 0) + } + default: + return make([]any, 0) + } } - return ret } // descend recursively executes [q] for each value in current and/or root and // its descendants and returns the results. func (s *Segment) descendLocated(current, root any, parent NormalizedPath) []*LocatedNode { - ret := []*LocatedNode{} switch val := current.(type) { case []any: + ret := make([]*LocatedNode, 0, len(val)) for i, v := range val { ret = append(ret, s.SelectLocated(v, root, append(parent, Index(i)))...) } + return slices.Clip(ret) case map[string]any: + ret := make([]*LocatedNode, 0, len(val)) for k, v := range val { ret = append(ret, s.SelectLocated(v, root, append(parent, Name(k)))...) } + return slices.Clip(ret) + default: + value := reflect.ValueOf(current) + switch value.Kind() { + case reflect.Slice: + // Descend into any other slice that contains slices or maps. + switch value.Type().Elem().Kind() { + case reflect.Slice, reflect.Map: + ret := make([]*LocatedNode, 0, value.Len()) + for i := range value.Len() { + ret = append(ret, s.SelectLocated( + value.Index(i).Interface(), root, append(parent, Index(i)), + )...) + } + return slices.Clip(ret) + default: + return make([]*LocatedNode, 0) + } + case reflect.Map: + // Descend into any map[string]* that contains slices or maps. + if value.Type().Key().Kind() != reflect.String { + return make([]*LocatedNode, 0) + } + switch value.Type().Elem().Kind() { + case reflect.Slice, reflect.Map: + ret := make([]*LocatedNode, 0, value.Len()) + for _, k := range value.MapKeys() { + ret = append(ret, s.SelectLocated( + value.MapIndex(k).Interface(), root, append(parent, Name(k.String())), + )...) + } + return slices.Clip(ret) + default: + return make([]*LocatedNode, 0) + } + default: + return make([]*LocatedNode, 0) + } } - return ret } // isSingular returns true if the segment selects at most one node. Defined by diff --git a/spec/segment_test.go b/spec/segment_test.go index 3566510..2dcd5c6 100644 --- a/spec/segment_test.go +++ b/spec/segment_test.go @@ -658,6 +658,80 @@ func TestDescendantSegmentSelect(t *testing.T) { }, rand: true, }, + { + test: "slice_ints", + seg: Descendant(Slice(1, 3)), + src: []int{1, 3, 4, 5}, + exp: []any{3, 4}, + loc: []*LocatedNode{ + {Path: Normalized(Index(1)), Node: 3}, + {Path: Normalized(Index(2)), Node: 4}, + }, + }, + { + test: "slice_slice_ints", + seg: Descendant(Slice(1, 3)), + src: [][]int{{1, 2, 4}, {5, 6, 7}, {8, 9}}, + exp: []any{[]int{5, 6, 7}, []int{8, 9}, 2, 4, 6, 7, 9}, + loc: []*LocatedNode{ + {Path: Normalized(Index(1)), Node: []int{5, 6, 7}}, + {Path: Normalized(Index(2)), Node: []int{8, 9}}, + {Path: Normalized(Index(0), Index(1)), Node: 2}, + {Path: Normalized(Index(0), Index(2)), Node: 4}, + {Path: Normalized(Index(1), Index(1)), Node: 6}, + {Path: Normalized(Index(1), Index(2)), Node: 7}, + {Path: Normalized(Index(2), Index(1)), Node: 9}, + }, + }, + { + test: "name_string_map", + seg: Descendant(Name("hi")), + src: map[string]string{"hi": "you", "go": "xyz"}, + exp: []any{"you"}, + loc: []*LocatedNode{ + {Path: Normalized(Name("hi")), Node: "you"}, + }, + rand: true, + }, + { + test: "name_string_obj_map", + seg: Descendant(Name("hi")), + src: map[string]map[string]string{ + "hi": {"x": "y", "hi": "you"}, + "go": {"hi": "how"}, + }, + exp: []any{map[string]string{"x": "y", "hi": "you"}, "you", "how"}, + loc: []*LocatedNode{ + {Path: Normalized(Name("hi")), Node: map[string]string{"x": "y", "hi": "you"}}, + {Path: Normalized(Name("hi"), Name("hi")), Node: "you"}, + {Path: Normalized(Name("go"), Name("hi")), Node: "how"}, + }, + rand: true, + }, + { + test: "name_int_map", + seg: Descendant(Name("hi")), + src: map[string]map[int]string{ + "hi": {1: "y", 2: "you"}, + "go": {42: "how"}, + }, + exp: []any{map[int]string{1: "y", 2: "you"}}, + loc: []*LocatedNode{ + {Path: Normalized(Name("hi")), Node: map[int]string{1: "y", 2: "you"}}, + }, + rand: true, + }, + { + test: "name_int_obj_map", + seg: Descendant(Name("hi")), + src: map[int]map[string]int{ + 42: {"hi": 99, "x": 4}, + 23: {"hi": 34, "x": 4}, + }, + exp: []any{}, + loc: []*LocatedNode{}, + rand: true, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() diff --git a/spec/selector.go b/spec/selector.go index 97d2526..91b9577 100644 --- a/spec/selector.go +++ b/spec/selector.go @@ -2,7 +2,10 @@ package spec import ( "fmt" + "maps" "math" + "reflect" + "slices" "strconv" "strings" ) @@ -56,26 +59,45 @@ func (n Name) writeTo(buf *strings.Builder) { } // Select selects n from input and returns it as a single value in a slice. -// Returns an empty slice if input is not a map[string]any or if it does not +// Returns an empty slice if input is not a string-keyed map or if it does not // contain n. Defined by the [Selector] interface. func (n Name) Select(input, _ any) []any { if obj, ok := input.(map[string]any); ok { if val, ok := obj[string(n)]; ok { return []any{val} } + return make([]any, 0) } + + // Select from any map[string]*. + obj := reflect.ValueOf(input) + if obj.Kind() == reflect.Map && obj.Type().Key().Kind() == reflect.String { + if v := obj.MapIndex(reflect.ValueOf(string(n))); v.Kind() != reflect.Invalid { + return []any{v.Interface()} + } + } + return make([]any, 0) } // SelectLocated selects n from input and returns it with its normalized path // as a single [LocatedNode] in a slice. Returns an empty slice if input is -// not a map[string]any or if it does not contain n. Defined by the [Selector] -// interface. +// not a string-keyed map or if it does not contain n. Defined by the +// [Selector] interface. func (n Name) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode { if obj, ok := input.(map[string]any); ok { if val, ok := obj[string(n)]; ok { return []*LocatedNode{newLocatedNode(append(parent, n), val)} } + return make([]*LocatedNode, 0) + } + + // Select from any map[string]*. + obj := reflect.ValueOf(input) + if obj.Kind() == reflect.Map && obj.Type().Key().Kind() == reflect.String { + if v := obj.MapIndex(reflect.ValueOf(string(n))); v.Kind() != reflect.Invalid { + return []*LocatedNode{newLocatedNode(append(parent, n), v.Interface())} + } } return make([]*LocatedNode, 0) } @@ -149,42 +171,84 @@ func (WildcardSelector) String() string { return "*" } 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 +// an empty slice if input is not a slice or string-keyed map. Defined by the // [Selector] interface. func (WildcardSelector) Select(input, _ any) []any { switch val := input.(type) { case []any: return val case map[string]any: - vals := make([]any, 0, len(val)) - for _, v := range val { - vals = append(vals, v) + return slices.Collect(maps.Values(val)) + default: + // Look for other slice and map types. + value := reflect.ValueOf(val) + switch value.Kind() { + case reflect.Slice: + // Copy the slice values. + ret := make([]any, value.Len()) + for i := range value.Len() { + ret[i] = value.Index(i).Interface() + } + return ret + case reflect.Map: + // Copy from any map[string]*. + if value.Type().Key().Kind() == reflect.String { + ret := make([]any, value.Len()) + for i, k := range value.MapKeys() { + ret[i] = value.MapIndex(k).Interface() + } + return ret + } + return make([]any, 0) + default: + return make([]any, 0) } - return vals } - return make([]any, 0) } // SelectLocated selects the values from input and returns them with their -// normalized paths in a slice of [LocatedNode] values. Returns an empty -// slice if input is not []any map[string]any. Defined by the [Selector] +// normalized paths in a slice of [LocatedNode] values. Returns an empty slice +// if input is not a slice or string-keyed map. Defined by the [Selector] // interface. func (WildcardSelector) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode { switch val := input.(type) { case []any: - vals := make([]*LocatedNode, len(val)) + ret := make([]*LocatedNode, len(val)) for i, v := range val { - vals[i] = newLocatedNode(append(parent, Index(i)), v) + ret[i] = newLocatedNode(append(parent, Index(i)), v) } - return vals + return ret case map[string]any: - vals := make([]*LocatedNode, 0, len(val)) + ret := make([]*LocatedNode, 0, len(val)) for k, v := range val { - vals = append(vals, newLocatedNode(append(parent, Name(k)), v)) + ret = append(ret, newLocatedNode(append(parent, Name(k)), v)) + } + return slices.Clip(ret) + default: + // Look for other slice and map types. + value := reflect.ValueOf(val) + switch value.Kind() { + case reflect.Slice: + // Copy the slice values. + ret := make([]*LocatedNode, value.Len()) + for i := range value.Len() { + ret[i] = newLocatedNode(append(parent, Index(i)), value.Index(i).Interface()) + } + return ret + case reflect.Map: + // Copy from any map[string]*. + if value.Type().Key().Kind() == reflect.String { + ret := make([]*LocatedNode, value.Len()) + for i, k := range value.MapKeys() { + ret[i] = newLocatedNode(append(parent, Name(k.String())), value.MapIndex(k).Interface()) + } + return ret + } + return make([]*LocatedNode, 0) + default: + return make([]*LocatedNode, 0) } - return vals } - return make([]*LocatedNode, 0) } // Index is an array index selector, e.g., [3], as defined by [RFC @@ -223,7 +287,22 @@ func (i Index) Select(input, _ any) []any { } else if idx < len(val) { return []any{val[idx]} } + return make([]any, 0) } + + // Select from any kind of slice. + val := reflect.ValueOf(input) + if val.Kind() == reflect.Slice { + idx := int(i) + if idx < 0 { + if idx = val.Len() + idx; idx >= 0 { + return []any{val.Index(idx).Interface()} + } + } else if idx < val.Len() { + return []any{val.Index(idx).Interface()} + } + } + return make([]any, 0) } @@ -241,7 +320,26 @@ func (i Index) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode } else if idx < len(val) { return []*LocatedNode{newLocatedNode(append(parent, Index(idx)), val[idx])} } + return make([]*LocatedNode, 0) } + + // Select from any kind of slice. + val := reflect.ValueOf(input) + if val.Kind() == reflect.Slice { + idx := int(i) + if idx < 0 { + if idx = val.Len() + idx; idx >= 0 { + return []*LocatedNode{newLocatedNode( + append(parent, Index(idx)), val.Index(idx).Interface(), + )} + } + } else if idx < val.Len() { + return []*LocatedNode{newLocatedNode( + append(parent, Index(idx)), val.Index(idx).Interface(), + )} + } + } + return make([]*LocatedNode, 0) } @@ -375,13 +473,32 @@ func (s SliceSelector) Select(input, _ any) []any { res = append(res, val[i]) } } - return res + return slices.Clip(res) } + + // Select from any kind of slice. + val := reflect.ValueOf(input) + if val.Kind() == reflect.Slice { + lower, upper := s.Bounds(val.Len()) + res := make([]any, 0, val.Len()) + switch { + case s.step > 0: + for i := lower; i < upper; i += s.step { + res = append(res, val.Index(i).Interface()) + } + case s.step < 0: + for i := upper; lower < i; i += s.step { + res = append(res, val.Index(i).Interface()) + } + } + return slices.Clip(res) + } + return make([]any, 0) } // SelectLocated selects values from input for the indexes specified by s and -// returns thm with their normalized paths as [LocatedNode] values. Returns +// returns them with their normalized paths as [LocatedNode] values. Returns // an empty slice if input is not a slice. Indexes outside the bounds of input // will not be included in the return value. Defined by the [Selector] // interface. @@ -399,7 +516,29 @@ func (s SliceSelector) SelectLocated(input, _ any, parent NormalizedPath) []*Loc res = append(res, newLocatedNode(append(parent, Index(i)), val[i])) } } - return res + return slices.Clip(res) + } + + // Select from any kind of slice. + val := reflect.ValueOf(input) + if val.Kind() == reflect.Slice { + lower, upper := s.Bounds(val.Len()) + res := make([]*LocatedNode, 0, val.Len()) + switch { + case s.step > 0: + for i := lower; i < upper; i += s.step { + res = append(res, newLocatedNode( + append(parent, Index(i)), val.Index(i).Interface(), + )) + } + case s.step < 0: + for i := upper; lower < i; i += s.step { + res = append(res, newLocatedNode( + append(parent, Index(i)), val.Index(i).Interface(), + )) + } + } + return slices.Clip(res) } return make([]*LocatedNode, 0) } @@ -476,23 +615,53 @@ func (f *FilterSelector) writeTo(buf *strings.Builder) { // expressions may evaluate the current value (@), the root value ($), or any // path expression. Defined by the [Selector] interface. func (f *FilterSelector) Select(current, root any) []any { - ret := []any{} switch current := current.(type) { case []any: + ret := make([]any, 0, len(current)) for _, v := range current { if f.Eval(v, root) { ret = append(ret, v) } } + return slices.Clip(ret) case map[string]any: + ret := make([]any, 0, len(current)) for _, v := range current { if f.Eval(v, root) { ret = append(ret, v) } } + return slices.Clip(ret) + default: + val := reflect.ValueOf(current) + switch val.Kind() { + case reflect.Slice: + // Select from any type of slice. + ret := make([]any, 0, val.Len()) + for i := range val.Len() { + v := val.Index(i).Interface() + if f.Eval(v, root) { + ret = append(ret, v) + } + } + return slices.Clip(ret) + case reflect.Map: + // Select from any map[string]*. + if val.Type().Key().Kind() != reflect.String { + return make([]any, 0) + } + ret := make([]any, 0, val.Len()) + for _, k := range val.MapKeys() { + v := val.MapIndex(k).Interface() + if f.Eval(v, root) { + ret = append(ret, v) + } + } + return slices.Clip(ret) + default: + return make([]any, 0) + } } - - return ret } // SelectLocated selects and returns [LocatedNode] values with values that f @@ -500,23 +669,53 @@ func (f *FilterSelector) Select(current, root any) []any { // (@), the root value ($), or any path expression. Defined by the [Selector] // interface. func (f *FilterSelector) SelectLocated(current, root any, parent NormalizedPath) []*LocatedNode { - ret := []*LocatedNode{} switch current := current.(type) { case []any: + ret := make([]*LocatedNode, 0, len(current)) for i, v := range current { if f.Eval(v, root) { ret = append(ret, newLocatedNode(append(parent, Index(i)), v)) } } + return slices.Clip(ret) case map[string]any: + ret := make([]*LocatedNode, 0, len(current)) for k, v := range current { if f.Eval(v, root) { ret = append(ret, newLocatedNode(append(parent, Name(k)), v)) } } + return slices.Clip(ret) + default: + val := reflect.ValueOf(current) + switch val.Kind() { + case reflect.Slice: + // Select from any type of slice. + ret := make([]*LocatedNode, 0, val.Len()) + for i := range val.Len() { + v := val.Index(i).Interface() + if f.Eval(v, root) { + ret = append(ret, newLocatedNode(append(parent, Index(i)), v)) + } + } + return slices.Clip(ret) + case reflect.Map: + // Select from any map[string]*. + if val.Type().Key().Kind() != reflect.String { + return make([]*LocatedNode, 0) + } + ret := make([]*LocatedNode, 0, val.Len()) + for _, k := range val.MapKeys() { + v := val.MapIndex(k).Interface() + if f.Eval(v, root) { + ret = append(ret, newLocatedNode(append(parent, Name(k.String())), v)) + } + } + return slices.Clip(ret) + default: + return make([]*LocatedNode, 0) + } } - - return ret } // Eval evaluates the f's [LogicalOr] expression against node and root. Uses diff --git a/spec/selector_test.go b/spec/selector_test.go index 1eed61d..fd082d7 100644 --- a/spec/selector_test.go +++ b/spec/selector_test.go @@ -3,6 +3,7 @@ package spec import ( "fmt" "math" + "reflect" "strings" "testing" @@ -388,6 +389,34 @@ func TestNameSelect(t *testing.T) { exp: []any{}, loc: []*LocatedNode{}, }, + { + test: "got_name_string_obj", + sel: Name("hi"), + src: map[string]any{"hi": "xyz"}, + exp: []any{"xyz"}, + loc: []*LocatedNode{{Path: Normalized(Name("hi")), Node: "xyz"}}, + }, + { + test: "got_name_int_obj", + sel: Name("hi"), + src: map[string]int{"hi": 42}, + exp: []any{42}, + loc: []*LocatedNode{{Path: Normalized(Name("hi")), Node: 42}}, + }, + { + test: "got_name_slice_obj", + sel: Name("hi"), + src: map[string][]int{"hi": {42, 99}}, + exp: []any{[]int{42, 99}}, + loc: []*LocatedNode{{Path: Normalized(Name("hi")), Node: []int{42, 99}}}, + }, + { + test: "got_name_int_keyed_slice", + sel: Name("hi"), + src: map[int]any{42: "xyz"}, + exp: []any{}, + loc: []*LocatedNode{}, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() @@ -458,6 +487,27 @@ func TestIndexSelect(t *testing.T) { exp: []any{}, loc: []*LocatedNode{}, }, + { + test: "index_zero_string", + sel: Index(0), + src: []string{"hi", "x", "y"}, + exp: []any{"hi"}, + loc: []*LocatedNode{{Path: Normalized(Index(0)), Node: "hi"}}, + }, + { + test: "index_neg_one_string", + sel: Index(-1), + src: []string{"hi", "x", "y"}, + exp: []any{"y"}, + loc: []*LocatedNode{{Path: Normalized(Index(2)), Node: "y"}}, + }, + { + test: "neg_out_of_range_string", + sel: Index(-4), + src: []string{"x", "y", "hi"}, + exp: []any{}, + loc: []*LocatedNode{}, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() @@ -487,6 +537,21 @@ func TestWildcardSelect(t *testing.T) { {Path: Normalized(Name("y")), Node: []any{true}}, }, }, + { + test: "int_object", + src: map[string]int{"x": 42, "y": 33}, + exp: []any{42, 33}, + loc: []*LocatedNode{ + {Path: Normalized(Name("x")), Node: 42}, + {Path: Normalized(Name("y")), Node: 33}, + }, + }, + { + test: "int_keyed_object", + src: map[int]string{42: "x", 33: "y"}, + exp: []any{}, + loc: []*LocatedNode{}, + }, { test: "array", src: []any{true, 42, map[string]any{"x": 6}}, @@ -498,7 +563,17 @@ func TestWildcardSelect(t *testing.T) { }, }, { - test: "something_else", + test: "int_array", + src: []int{1, 42, 5}, + exp: []any{1, 42, 5}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: 1}, + {Path: Normalized(Index(1)), Node: 42}, + {Path: Normalized(Index(2)), Node: 5}, + }, + }, + { + test: "integer", src: 42, exp: []any{}, loc: []*LocatedNode{}, @@ -508,7 +583,7 @@ func TestWildcardSelect(t *testing.T) { t.Parallel() a := assert.New(t) - if _, ok := tc.src.(map[string]any); ok { + if reflect.ValueOf(tc.src).Kind() == reflect.Map { a.ElementsMatch(tc.exp, Wildcard().Select(tc.src, nil)) a.ElementsMatch(tc.loc, Wildcard().SelectLocated(tc.src, nil, Normalized())) } else { @@ -655,6 +730,49 @@ func TestSliceSelect(t *testing.T) { exp: []any{}, loc: []*LocatedNode{}, }, + { + test: "src_string_slice", + sel: Slice(0, 2), + src: []string{"hi", "bye", "x"}, + exp: []any{"hi", "bye"}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: "hi"}, + {Path: Normalized(Index(1)), Node: "bye"}, + }, + }, + { + test: "src_int_slice", + sel: Slice(), + src: []int{42, 1024}, + exp: []any{42, 1024}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: 42}, + {Path: Normalized(Index(1)), Node: 1024}, + }, + }, + { + test: "src_object_slice", + sel: Slice(), + src: []map[string]any{{"x": 1}, {"y": true}}, + exp: []any{map[string]any{"x": 1}, map[string]any{"y": true}}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: map[string]any{"x": 1}}, + {Path: Normalized(Index(1)), Node: map[string]any{"y": true}}, + }, + }, + { + test: "negative_step_strings", + sel: Slice(nil, nil, -1), + src: []string{"x", "y", "z", "a", "b"}, + exp: []any{"b", "a", "z", "y", "x"}, + loc: []*LocatedNode{ + {Path: Normalized(Index(4)), Node: "b"}, + {Path: Normalized(Index(3)), Node: "a"}, + {Path: Normalized(Index(2)), Node: "z"}, + {Path: Normalized(Index(1)), Node: "y"}, + {Path: Normalized(Index(0)), Node: "x"}, + }, + }, } { t.Run(tc.test, func(t *testing.T) { t.Parallel() @@ -719,6 +837,29 @@ func TestFilterSelector(t *testing.T) { str: `?$["y"]`, rand: true, }, + { + test: "string_object_root", + filter: Filter(And(Existence(Query(true, Child(Name("y")))))), + root: map[string]string{"x": "how", "y": "hi"}, + current: map[string]string{"a": "grit", "b": "kick"}, + exp: []any{"grit", "kick"}, + loc: []*LocatedNode{ + {Path: Normalized(Name("a")), Node: "grit"}, + {Path: Normalized(Name("b")), Node: "kick"}, + }, + str: `?$["y"]`, + rand: true, + }, + { + test: "int_keyed_object_root", + filter: Filter(And(Existence(Query(true, Child(Name("y")))))), + root: map[int]string{1: "how", 2: "hi"}, + current: map[int]string{3: "grit", 4: "kick"}, + exp: []any{}, + loc: []*LocatedNode{}, + str: `?$["y"]`, + rand: true, + }, { test: "object_root_false", filter: Filter(And(Existence(Query(true, Child(Name("z")))))), @@ -739,6 +880,16 @@ func TestFilterSelector(t *testing.T) { }, str: `?@[0]`, }, + { + test: "int_array_current", + filter: Filter(And(Existence(Query(false, Child(Index(0)))))), + current: [][]int{{42}}, + exp: []any{[]int{42}}, + loc: []*LocatedNode{ + {Path: Normalized(Index(0)), Node: []int{42}}, + }, + str: `?@[0]`, + }, { test: "array_current_false", filter: Filter(And(Existence(Query(false, Child(Index(1)))))),