Skip to content

Commit ab286b7

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 ab286b7

4 files changed

Lines changed: 226 additions & 0 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

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/selector.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package spec
33
import (
44
"fmt"
55
"math"
6+
"reflect"
67
"strconv"
78
"strings"
89
)
@@ -63,7 +64,17 @@ func (n Name) Select(input, _ any) []any {
6364
if val, ok := obj[string(n)]; ok {
6465
return []any{val}
6566
}
67+
return make([]any, 0)
6668
}
69+
70+
// Select from any type of map.
71+
obj := reflect.ValueOf(input)
72+
if obj.Kind() == reflect.Map && reflect.TypeOf(input).Key().Kind() == reflect.String {
73+
if v := obj.MapIndex(reflect.ValueOf(string(n))); v.Kind() != reflect.Invalid {
74+
return []any{v.Interface()}
75+
}
76+
}
77+
6778
return make([]any, 0)
6879
}
6980

@@ -76,6 +87,15 @@ func (n Name) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode
7687
if val, ok := obj[string(n)]; ok {
7788
return []*LocatedNode{newLocatedNode(append(parent, n), val)}
7889
}
90+
return make([]*LocatedNode, 0)
91+
}
92+
93+
// Select from any type of map.
94+
obj := reflect.ValueOf(input)
95+
if obj.Kind() == reflect.Map && reflect.TypeOf(input).Key().Kind() == reflect.String {
96+
if v := obj.MapIndex(reflect.ValueOf(string(n))); v.Kind() != reflect.Invalid {
97+
return []*LocatedNode{newLocatedNode(append(parent, n), v.Interface())}
98+
}
7999
}
80100
return make([]*LocatedNode, 0)
81101
}
@@ -223,7 +243,23 @@ func (i Index) Select(input, _ any) []any {
223243
} else if idx < len(val) {
224244
return []any{val[idx]}
225245
}
246+
return make([]any, 0)
247+
}
248+
249+
// Select from any kind of slice.
250+
val := reflect.ValueOf(input)
251+
if val.Kind() == reflect.Slice {
252+
idx := int(i)
253+
if idx < 0 {
254+
if idx = val.Len() + idx; idx >= 0 {
255+
return []any{val.Index(idx).Interface()}
256+
}
257+
} else if idx < val.Len() {
258+
return []any{val.Index(idx).Interface()}
259+
}
260+
return make([]any, 0)
226261
}
262+
227263
return make([]any, 0)
228264
}
229265

@@ -241,7 +277,26 @@ func (i Index) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode
241277
} else if idx < len(val) {
242278
return []*LocatedNode{newLocatedNode(append(parent, Index(idx)), val[idx])}
243279
}
280+
return make([]*LocatedNode, 0)
244281
}
282+
283+
// Select from any kind of slice.
284+
val := reflect.ValueOf(input)
285+
if val.Kind() == reflect.Slice {
286+
idx := int(i)
287+
if idx < 0 {
288+
if idx = val.Len() + idx; idx >= 0 {
289+
return []*LocatedNode{newLocatedNode(
290+
append(parent, Index(idx)), val.Index(idx).Interface(),
291+
)}
292+
}
293+
} else if idx < val.Len() {
294+
return []*LocatedNode{newLocatedNode(
295+
append(parent, Index(idx)), val.Index(idx).Interface(),
296+
)}
297+
}
298+
}
299+
245300
return make([]*LocatedNode, 0)
246301
}
247302

@@ -377,6 +432,25 @@ func (s SliceSelector) Select(input, _ any) []any {
377432
}
378433
return res
379434
}
435+
436+
// Select from any kind of slice.
437+
val := reflect.ValueOf(input)
438+
if val.Kind() == reflect.Slice {
439+
lower, upper := s.Bounds(val.Len())
440+
res := make([]any, 0, val.Len())
441+
switch {
442+
case s.step > 0:
443+
for i := lower; i < upper; i += s.step {
444+
res = append(res, val.Index(i).Interface())
445+
}
446+
case s.step < 0:
447+
for i := upper; lower < i; i += s.step {
448+
res = append(res, val.Index(i).Interface())
449+
}
450+
}
451+
return res
452+
}
453+
380454
return make([]any, 0)
381455
}
382456

@@ -401,6 +475,28 @@ func (s SliceSelector) SelectLocated(input, _ any, parent NormalizedPath) []*Loc
401475
}
402476
return res
403477
}
478+
479+
// Select from any kind of slice.
480+
val := reflect.ValueOf(input)
481+
if val.Kind() == reflect.Slice {
482+
lower, upper := s.Bounds(val.Len())
483+
res := make([]*LocatedNode, 0, val.Len())
484+
switch {
485+
case s.step > 0:
486+
for i := lower; i < upper; i += s.step {
487+
res = append(res, newLocatedNode(
488+
append(parent, Index(i)), val.Index(i).Interface(),
489+
))
490+
}
491+
case s.step < 0:
492+
for i := upper; lower < i; i += s.step {
493+
res = append(res, newLocatedNode(
494+
append(parent, Index(i)), val.Index(i).Interface(),
495+
))
496+
}
497+
}
498+
return res
499+
}
404500
return make([]*LocatedNode, 0)
405501
}
406502

spec/selector_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,34 @@ func TestNameSelect(t *testing.T) {
388388
exp: []any{},
389389
loc: []*LocatedNode{},
390390
},
391+
{
392+
test: "got_name_string_obj",
393+
sel: Name("hi"),
394+
src: map[string]any{"hi": "xyz"},
395+
exp: []any{"xyz"},
396+
loc: []*LocatedNode{{Path: Normalized(Name("hi")), Node: "xyz"}},
397+
},
398+
{
399+
test: "got_name_int_obj",
400+
sel: Name("hi"),
401+
src: map[string]int{"hi": 42},
402+
exp: []any{42},
403+
loc: []*LocatedNode{{Path: Normalized(Name("hi")), Node: 42}},
404+
},
405+
{
406+
test: "got_name_slice_obj",
407+
sel: Name("hi"),
408+
src: map[string][]int{"hi": {42, 99}},
409+
exp: []any{[]int{42, 99}},
410+
loc: []*LocatedNode{{Path: Normalized(Name("hi")), Node: []int{42, 99}}},
411+
},
412+
{
413+
test: "got_name_int_keyed_slice",
414+
sel: Name("hi"),
415+
src: map[int]any{42: "xyz"},
416+
exp: []any{},
417+
loc: []*LocatedNode{},
418+
},
391419
} {
392420
t.Run(tc.test, func(t *testing.T) {
393421
t.Parallel()
@@ -655,6 +683,36 @@ func TestSliceSelect(t *testing.T) {
655683
exp: []any{},
656684
loc: []*LocatedNode{},
657685
},
686+
{
687+
test: "src_string_slice",
688+
sel: Slice(0, 2),
689+
src: []string{"hi", "bye", "x"},
690+
exp: []any{"hi", "bye"},
691+
loc: []*LocatedNode{
692+
{Path: Normalized(Index(0)), Node: "hi"},
693+
{Path: Normalized(Index(1)), Node: "bye"},
694+
},
695+
},
696+
{
697+
test: "src_int_slice",
698+
sel: Slice(),
699+
src: []int{42, 1024},
700+
exp: []any{42, 1024},
701+
loc: []*LocatedNode{
702+
{Path: Normalized(Index(0)), Node: 42},
703+
{Path: Normalized(Index(1)), Node: 1024},
704+
},
705+
},
706+
{
707+
test: "src_object_slice",
708+
sel: Slice(),
709+
src: []map[string]any{{"x": 1}, {"y": true}},
710+
exp: []any{map[string]any{"x": 1}, map[string]any{"y": true}},
711+
loc: []*LocatedNode{
712+
{Path: Normalized(Index(0)), Node: map[string]any{"x": 1}},
713+
{Path: Normalized(Index(1)), Node: map[string]any{"y": true}},
714+
},
715+
},
658716
} {
659717
t.Run(tc.test, func(t *testing.T) {
660718
t.Parallel()

0 commit comments

Comments
 (0)