Skip to content

Commit df3cc49

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 df3cc49

4 files changed

Lines changed: 259 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: 95 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,22 @@ 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+
}
226260
}
261+
227262
return make([]any, 0)
228263
}
229264

@@ -241,7 +276,26 @@ func (i Index) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode
241276
} else if idx < len(val) {
242277
return []*LocatedNode{newLocatedNode(append(parent, Index(idx)), val[idx])}
243278
}
279+
return make([]*LocatedNode, 0)
244280
}
281+
282+
// Select from any kind of slice.
283+
val := reflect.ValueOf(input)
284+
if val.Kind() == reflect.Slice {
285+
idx := int(i)
286+
if idx < 0 {
287+
if idx = val.Len() + idx; idx >= 0 {
288+
return []*LocatedNode{newLocatedNode(
289+
append(parent, Index(idx)), val.Index(idx).Interface(),
290+
)}
291+
}
292+
} else if idx < val.Len() {
293+
return []*LocatedNode{newLocatedNode(
294+
append(parent, Index(idx)), val.Index(idx).Interface(),
295+
)}
296+
}
297+
}
298+
245299
return make([]*LocatedNode, 0)
246300
}
247301

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

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

spec/selector_test.go

Lines changed: 92 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()
@@ -458,6 +486,27 @@ func TestIndexSelect(t *testing.T) {
458486
exp: []any{},
459487
loc: []*LocatedNode{},
460488
},
489+
{
490+
test: "index_zero_string",
491+
sel: Index(0),
492+
src: []string{"hi", "x", "y"},
493+
exp: []any{"hi"},
494+
loc: []*LocatedNode{{Path: Normalized(Index(0)), Node: "hi"}},
495+
},
496+
{
497+
test: "index_neg_one_string",
498+
sel: Index(-1),
499+
src: []string{"hi", "x", "y"},
500+
exp: []any{"y"},
501+
loc: []*LocatedNode{{Path: Normalized(Index(2)), Node: "y"}},
502+
},
503+
{
504+
test: "neg_out_of_range_string",
505+
sel: Index(-4),
506+
src: []string{"x", "y", "hi"},
507+
exp: []any{},
508+
loc: []*LocatedNode{},
509+
},
461510
} {
462511
t.Run(tc.test, func(t *testing.T) {
463512
t.Parallel()
@@ -655,6 +704,49 @@ func TestSliceSelect(t *testing.T) {
655704
exp: []any{},
656705
loc: []*LocatedNode{},
657706
},
707+
{
708+
test: "src_string_slice",
709+
sel: Slice(0, 2),
710+
src: []string{"hi", "bye", "x"},
711+
exp: []any{"hi", "bye"},
712+
loc: []*LocatedNode{
713+
{Path: Normalized(Index(0)), Node: "hi"},
714+
{Path: Normalized(Index(1)), Node: "bye"},
715+
},
716+
},
717+
{
718+
test: "src_int_slice",
719+
sel: Slice(),
720+
src: []int{42, 1024},
721+
exp: []any{42, 1024},
722+
loc: []*LocatedNode{
723+
{Path: Normalized(Index(0)), Node: 42},
724+
{Path: Normalized(Index(1)), Node: 1024},
725+
},
726+
},
727+
{
728+
test: "src_object_slice",
729+
sel: Slice(),
730+
src: []map[string]any{{"x": 1}, {"y": true}},
731+
exp: []any{map[string]any{"x": 1}, map[string]any{"y": true}},
732+
loc: []*LocatedNode{
733+
{Path: Normalized(Index(0)), Node: map[string]any{"x": 1}},
734+
{Path: Normalized(Index(1)), Node: map[string]any{"y": true}},
735+
},
736+
},
737+
{
738+
test: "negative_step_strings",
739+
sel: Slice(nil, nil, -1),
740+
src: []string{"x", "y", "z", "a", "b"},
741+
exp: []any{"b", "a", "z", "y", "x"},
742+
loc: []*LocatedNode{
743+
{Path: Normalized(Index(4)), Node: "b"},
744+
{Path: Normalized(Index(3)), Node: "a"},
745+
{Path: Normalized(Index(2)), Node: "z"},
746+
{Path: Normalized(Index(1)), Node: "y"},
747+
{Path: Normalized(Index(0)), Node: "x"},
748+
},
749+
},
658750
} {
659751
t.Run(tc.test, func(t *testing.T) {
660752
t.Parallel()

0 commit comments

Comments
 (0)