Skip to content

Commit f65332d

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 f65332d

8 files changed

Lines changed: 412 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ 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 map, not just
15+
`[]any` or `map[string]any`. Internally it still prefers `[]any` and
16+
`map[string]any`, to optimize for values decoded by encoding/json, but it
17+
now falls back on reflection to detect any other kind of slice or
18+
string-keyed map. Thanks to @ndsboy for the prompt (#26).
19+
1220
### ⬆️ Dependency Updates
1321

1422
* Upgraded to `golangci-lint` v2.11.1 and made suggested slice allocation

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: 8 additions & 1 deletion
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"
@@ -50,7 +51,13 @@ 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, reflect.Map:
57+
return spec.Value(val.Len())
58+
default:
59+
return nil
60+
}
5461
}
5562
}
5663

spec/query.go

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

3-
import "strings"
3+
import (
4+
"strings"
5+
)
46

57
// PathQuery represents a JSONPath query. Interfaces implemented:
68
// - [Selector]

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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package spec
22

33
import (
4+
"reflect"
45
"strings"
56
)
67

@@ -94,6 +95,20 @@ func (s *Segment) descend(current, root any) []any {
9495
for _, v := range val {
9596
ret = append(ret, s.Select(v, root)...)
9697
}
98+
default:
99+
value := reflect.ValueOf(current)
100+
switch value.Kind() {
101+
case reflect.Slice:
102+
for i := range value.Len() {
103+
ret = append(ret, s.Select(value.Index(i).Interface(), root)...)
104+
}
105+
case reflect.Map:
106+
for _, k := range value.MapKeys() {
107+
ret = append(ret, s.Select(value.MapIndex(k).Interface(), root)...)
108+
}
109+
default:
110+
return nil
111+
}
97112
}
98113
return ret
99114
}
@@ -111,6 +126,24 @@ func (s *Segment) descendLocated(current, root any, parent NormalizedPath) []*Lo
111126
for k, v := range val {
112127
ret = append(ret, s.SelectLocated(v, root, append(parent, Name(k)))...)
113128
}
129+
default:
130+
value := reflect.ValueOf(current)
131+
switch value.Kind() {
132+
case reflect.Slice:
133+
for i := range value.Len() {
134+
ret = append(ret, s.SelectLocated(
135+
value.Index(i).Interface(), root, append(parent, Index(i)),
136+
)...)
137+
}
138+
case reflect.Map:
139+
for _, k := range value.MapKeys() {
140+
ret = append(ret, s.SelectLocated(
141+
value.MapIndex(k).Interface(), root, append(parent, Name(k.String())),
142+
)...)
143+
}
144+
default:
145+
return nil
146+
}
114147
}
115148
return ret
116149
}

0 commit comments

Comments
 (0)