Skip to content

Commit e49a20b

Browse files
committed
Fallback on reflection to select from slice & map
Turns out to be more straightforward than I expected to support selecting values not just from `[]any` and `map[string]any` but from any type of slice or string-keyed map, thanks to the `reflect` package. Leave the branches for `[]any` and `map[string]any` in place to avoid the overhead of reflection, but then use reflection if the value is any other type of slice or map. Resolves #26.
1 parent 1bef171 commit e49a20b

10 files changed

Lines changed: 657 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,25 @@ All notable changes to this project will be documented in this file. It uses the
99

1010
## [v0.11.1] — Unreleased
1111

12+
### ⚡ Improvements
13+
14+
* Added support for selecting values from any type of slice or string-keyed
15+
map, not just `[]any` or `map[string]any`. Internally it still prefers
16+
`[]any` and `map[string]any`, to optimize for values decoded by
17+
encoding/json, but it now falls back on reflection to detect any other
18+
kind of slice or string-keyed map. Thanks to @ndsboy for the prompt (#26).
19+
* Updated result set creation to allocate more slots for results when the
20+
number of results are unknown, based on the number of selectors or items
21+
to select from, to improve memory efficiency. Encouraged by the recent
22+
[Go blog post] describing the advantages of this pattern.
23+
1224
### ⬆️ Dependency Updates
1325

1426
* Upgraded to `golangci-lint` v2.11.1 and made suggested slice allocation
1527
optimization
1628

1729
[v0.11.1]: https://github.com/theory/jsonpath/compare/v0.11.0...v0.11.1
30+
[Go blog post]: https://go.dev/blog/allocation-optimizations
1831

1932
## [v0.11.0] — 2026-03-02
2033

path_example_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func ExampleLocatedNodeList() {
115115

116116
func ExampleLocatedNodeList_Deduplicate() {
117117
// Load some JSON.
118-
pallet := map[string]any{"colors": []any{"red", "blue"}}
118+
pallet := map[string]any{"colors": []string{"red", "blue"}}
119119

120120
// Parse a JSONPath and select from the input.
121121
p := jsonpath.MustParse("$.colors[0, 1, 1, 0]")
@@ -133,7 +133,7 @@ func ExampleLocatedNodeList_Deduplicate() {
133133

134134
func ExampleLocatedNodeList_Sort() {
135135
// Load some JSON.
136-
pallet := map[string]any{"colors": []any{"red", "blue", "green"}}
136+
pallet := map[string]any{"colors": []string{"red", "blue", "green"}}
137137

138138
// Parse a JSONPath and select from the input.
139139
p := jsonpath.MustParse("$.colors[2, 0, 1]")
@@ -166,7 +166,7 @@ func ExampleLocatedNodeList_Sort() {
166166

167167
func ExampleLocatedNodeList_Clone() {
168168
// Load some JSON.
169-
items := []any{1, 2, 3, 4, 5}
169+
items := []int{1, 2, 3, 4, 5}
170170

171171
// Parse a JSONPath and select from the input.
172172
p := jsonpath.MustParse("$[2, 0, 1, 0, 1]")
@@ -259,9 +259,9 @@ func ExampleWithRegistry() {
259259

260260
// Do any of these arrays start with 6?
261261
input := []any{
262-
[]any{1, 2, 3, 4, 5},
263-
[]any{6, 7, 8, 9},
264-
[]any{4, 8, 12},
262+
[]int{1, 2, 3, 4, 5},
263+
[]int{6, 7, 8, 9},
264+
[]int{4, 8, 12},
265265
}
266266
nodes := path.Select(input)
267267
fmt.Printf("%v\n", nodes)

registry/funcs.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package registry
33
import (
44
"errors"
55
"fmt"
6+
"reflect"
67
"regexp"
78
"regexp/syntax"
89
"unicode/utf8"
@@ -32,8 +33,8 @@ func checkLengthArgs(args []spec.FuncExprArg) error {
3233
// - if jv[0] is nil, the result is nil
3334
// - If jv[0] is a string, the result is the number of Unicode scalar values
3435
// in the string.
35-
// - If jv[0] is a []any, the result is the number of elements in the slice.
36-
// - If jv[0] is an map[string]any, the result is the number of members in
36+
// - If jv[0] is a slice, the result is the number of elements in the slice.
37+
// - If jv[0] is a string-keyed map, the result is the number of members in
3738
// the map.
3839
// - For any other value, the result is nil.
3940
func lengthFunc(jv []spec.PathValue) spec.PathValue {
@@ -50,7 +51,18 @@ func lengthFunc(jv []spec.PathValue) spec.PathValue {
5051
case map[string]any:
5152
return spec.Value(len(v))
5253
default:
53-
return nil
54+
val := reflect.ValueOf(v)
55+
switch val.Kind() {
56+
case reflect.Slice:
57+
return spec.Value(val.Len())
58+
case reflect.Map:
59+
if val.Type().Key().Kind() == reflect.String {
60+
return spec.Value(val.Len())
61+
}
62+
return nil
63+
default:
64+
return nil
65+
}
5466
}
5567
}
5668

registry/funcs_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,31 @@ func TestLengthFunc(t *testing.T) {
9898
vals: []spec.PathValue{spec.LogicalFalse},
9999
err: "cannot convert LogicalType to ValueType",
100100
},
101+
{
102+
test: "int_array",
103+
vals: []spec.PathValue{spec.Value([]int{1, 2, 3, 4, 5})},
104+
exp: 5,
105+
},
106+
{
107+
test: "string_array",
108+
vals: []spec.PathValue{spec.Value([]string{"x", "y", "z"})},
109+
exp: 3,
110+
},
111+
{
112+
test: "int_object",
113+
vals: []spec.PathValue{spec.Value(map[string]int{"x": 1, "y": 0, "z": 2})},
114+
exp: 3,
115+
},
116+
{
117+
test: "string_object",
118+
vals: []spec.PathValue{spec.Value(map[string]string{"x": "x", "y": "y"})},
119+
exp: 2,
120+
},
121+
{
122+
test: "int_indexed_object",
123+
vals: []spec.PathValue{spec.Value(map[int]string{1: "x", 2: "c"})},
124+
exp: -1,
125+
},
101126
} {
102127
t.Run(tc.test, func(t *testing.T) {
103128
t.Parallel()

spec/function.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func NodesFrom(value PathValue) NodesType {
8383
case *ValueType:
8484
return NodesType([]any{v.any})
8585
case nil:
86-
return NodesType([]any{})
86+
return NodesType(make([]any, 0))
8787
case LogicalType:
8888
panic("cannot convert LogicalType to NodesType")
8989
default:

spec/query_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,34 @@ func TestQueryObject(t *testing.T) {
288288
exp: []any{},
289289
loc: []*LocatedNode{},
290290
},
291+
{
292+
test: "string_map",
293+
resType: FuncValue,
294+
input: map[string]string{"x": "hi", "y": "y"},
295+
segs: []*Segment{Child(Name("x"))},
296+
exp: []any{"hi"},
297+
loc: []*LocatedNode{
298+
{Path: Normalized(Name("x")), Node: "hi"},
299+
},
300+
},
301+
{
302+
test: "int_map",
303+
resType: FuncValue,
304+
input: map[string]int{"x": 42, "y": 99},
305+
segs: []*Segment{Child(Name("x"))},
306+
exp: []any{42},
307+
loc: []*LocatedNode{
308+
{Path: Normalized(Name("x")), Node: 42},
309+
},
310+
},
311+
{
312+
test: "int_keyed_map",
313+
resType: FuncValue,
314+
input: map[int]string{42: "hi", 99: "y"},
315+
segs: []*Segment{Child(Name("42"))},
316+
exp: []any{},
317+
loc: []*LocatedNode{},
318+
},
291319
} {
292320
t.Run(tc.test, func(t *testing.T) {
293321
t.Parallel()
@@ -631,6 +659,22 @@ func TestQueryArray(t *testing.T) {
631659
{Path: Normalized(Index(0), Name("x"), Index(1)), Node: 2},
632660
},
633661
},
662+
{
663+
test: "string_slice_index",
664+
resType: FuncValue,
665+
segs: []*Segment{Child(Index(0))},
666+
input: []string{"x", "y"},
667+
exp: []any{"x"},
668+
loc: []*LocatedNode{{Path: Normalized(Index(0)), Node: "x"}},
669+
},
670+
{
671+
test: "int_slice_index",
672+
resType: FuncValue,
673+
segs: []*Segment{Child(Index(1))},
674+
input: []int{0, 42},
675+
exp: []any{42},
676+
loc: []*LocatedNode{{Path: Normalized(Index(1)), Node: 42}},
677+
},
634678
} {
635679
t.Run(tc.test, func(t *testing.T) {
636680
t.Parallel()
@@ -1101,6 +1145,26 @@ func TestQuerySlice(t *testing.T) {
11011145
{Path: Normalized(Index(0), Index(1)), Node: 42},
11021146
},
11031147
},
1148+
{
1149+
test: "string_slice",
1150+
segs: []*Segment{Child(Slice())},
1151+
input: []string{"x", "y"},
1152+
exp: []any{"x", "y"},
1153+
loc: []*LocatedNode{
1154+
{Path: Normalized(Index(0)), Node: "x"},
1155+
{Path: Normalized(Index(1)), Node: "y"},
1156+
},
1157+
},
1158+
{
1159+
test: "int_slice",
1160+
segs: []*Segment{Child(Slice())},
1161+
input: []int{42, 99},
1162+
exp: []any{42, 99},
1163+
loc: []*LocatedNode{
1164+
{Path: Normalized(Index(0)), Node: 42},
1165+
{Path: Normalized(Index(1)), Node: 99},
1166+
},
1167+
},
11041168
} {
11051169
t.Run(tc.test, func(t *testing.T) {
11061170
t.Parallel()

spec/segment.go

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package spec
22

33
import (
4+
"reflect"
5+
"slices"
46
"strings"
57
)
68

@@ -57,62 +59,136 @@ func (s *Segment) String() string {
5759
// Select selects and returns values from current or root, for each of s's
5860
// selectors. Defined by the [Selector] interface.
5961
func (s *Segment) Select(current, root any) []any {
60-
ret := []any{}
62+
ret := make([]any, 0, len(s.selectors))
6163
for _, sel := range s.selectors {
6264
ret = append(ret, sel.Select(current, root)...)
6365
}
6466
if s.descendant {
6567
ret = append(ret, s.descend(current, root)...)
6668
}
67-
return ret
69+
return slices.Clip(ret)
6870
}
6971

7072
// SelectLocated selects and returns values as [LocatedNode] values from
7173
// current or root for each of seg's selectors. Defined by the [Selector]
7274
// interface.
7375
func (s *Segment) SelectLocated(current, root any, parent NormalizedPath) []*LocatedNode {
74-
ret := []*LocatedNode{}
76+
ret := make([]*LocatedNode, 0, len(s.selectors))
7577
for _, sel := range s.selectors {
7678
ret = append(ret, sel.SelectLocated(current, root, parent)...)
7779
}
7880
if s.descendant {
7981
ret = append(ret, s.descendLocated(current, root, parent)...)
8082
}
81-
return ret
83+
return slices.Clip(ret)
8284
}
8385

8486
// descend recursively executes [Segment.Select] for each value in current
8587
// and/or root and its descendants and returns the results.
8688
func (s *Segment) descend(current, root any) []any {
87-
ret := []any{}
8889
switch val := current.(type) {
8990
case []any:
91+
ret := make([]any, 0, len(val))
9092
for _, v := range val {
9193
ret = append(ret, s.Select(v, root)...)
9294
}
95+
return slices.Clip(ret)
9396
case map[string]any:
97+
ret := make([]any, 0, len(val))
9498
for _, v := range val {
9599
ret = append(ret, s.Select(v, root)...)
96100
}
101+
return slices.Clip(ret)
102+
default:
103+
value := reflect.ValueOf(current)
104+
switch value.Kind() {
105+
case reflect.Slice:
106+
// Descend into any other slice that contains slices or maps.
107+
switch value.Type().Elem().Kind() {
108+
case reflect.Slice, reflect.Map:
109+
ret := make([]any, 0, value.Len())
110+
for i := range value.Len() {
111+
ret = append(ret, s.Select(value.Index(i).Interface(), root)...)
112+
}
113+
return slices.Clip(ret)
114+
default:
115+
return make([]any, 0)
116+
}
117+
case reflect.Map:
118+
// Descend into any map[string]* that contains slices or maps.
119+
if value.Type().Key().Kind() != reflect.String {
120+
return make([]any, 0)
121+
}
122+
switch value.Type().Elem().Kind() {
123+
case reflect.Slice, reflect.Map:
124+
ret := make([]any, 0, value.Len())
125+
for _, k := range value.MapKeys() {
126+
ret = append(ret, s.Select(value.MapIndex(k).Interface(), root)...)
127+
}
128+
return slices.Clip(ret)
129+
default:
130+
return make([]any, 0)
131+
}
132+
default:
133+
return make([]any, 0)
134+
}
97135
}
98-
return ret
99136
}
100137

101138
// descend recursively executes [q] for each value in current and/or root and
102139
// its descendants and returns the results.
103140
func (s *Segment) descendLocated(current, root any, parent NormalizedPath) []*LocatedNode {
104-
ret := []*LocatedNode{}
105141
switch val := current.(type) {
106142
case []any:
143+
ret := make([]*LocatedNode, 0, len(val))
107144
for i, v := range val {
108145
ret = append(ret, s.SelectLocated(v, root, append(parent, Index(i)))...)
109146
}
147+
return slices.Clip(ret)
110148
case map[string]any:
149+
ret := make([]*LocatedNode, 0, len(val))
111150
for k, v := range val {
112151
ret = append(ret, s.SelectLocated(v, root, append(parent, Name(k)))...)
113152
}
153+
return slices.Clip(ret)
154+
default:
155+
value := reflect.ValueOf(current)
156+
switch value.Kind() {
157+
case reflect.Slice:
158+
// Descend into any other slice that contains slices or maps.
159+
switch value.Type().Elem().Kind() {
160+
case reflect.Slice, reflect.Map:
161+
ret := make([]*LocatedNode, 0, value.Len())
162+
for i := range value.Len() {
163+
ret = append(ret, s.SelectLocated(
164+
value.Index(i).Interface(), root, append(parent, Index(i)),
165+
)...)
166+
}
167+
return slices.Clip(ret)
168+
default:
169+
return make([]*LocatedNode, 0)
170+
}
171+
case reflect.Map:
172+
// Descend into any map[string]* that contains slices or maps.
173+
if value.Type().Key().Kind() != reflect.String {
174+
return make([]*LocatedNode, 0)
175+
}
176+
switch value.Type().Elem().Kind() {
177+
case reflect.Slice, reflect.Map:
178+
ret := make([]*LocatedNode, 0, value.Len())
179+
for _, k := range value.MapKeys() {
180+
ret = append(ret, s.SelectLocated(
181+
value.MapIndex(k).Interface(), root, append(parent, Name(k.String())),
182+
)...)
183+
}
184+
return slices.Clip(ret)
185+
default:
186+
return make([]*LocatedNode, 0)
187+
}
188+
default:
189+
return make([]*LocatedNode, 0)
190+
}
114191
}
115-
return ret
116192
}
117193

118194
// isSingular returns true if the segment selects at most one node. Defined by

0 commit comments

Comments
 (0)