Skip to content

Commit bde53b4

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

4 files changed

Lines changed: 151 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, not just
15+
`[]any`. Internally it still prefers `[]any`, in the expectation that it's
16+
optimized for a data structure decoded by `json.Unmarshal`, but it now
17+
falls back on reflection to detect any other kind of slice. Thanks to
18+
@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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,22 @@ func TestQueryArray(t *testing.T) {
631631
{Path: Normalized(Index(0), Name("x"), Index(1)), Node: 2},
632632
},
633633
},
634+
{
635+
test: "string_slice_index",
636+
resType: FuncValue,
637+
segs: []*Segment{Child(Index(0))},
638+
input: []string{"x", "y"},
639+
exp: []any{"x"},
640+
loc: []*LocatedNode{{Path: Normalized(Index(0)), Node: "x"}},
641+
},
642+
{
643+
test: "int_slice_index",
644+
resType: FuncValue,
645+
segs: []*Segment{Child(Index(1))},
646+
input: []int{0, 42},
647+
exp: []any{42},
648+
loc: []*LocatedNode{{Path: Normalized(Index(1)), Node: 42}},
649+
},
634650
} {
635651
t.Run(tc.test, func(t *testing.T) {
636652
t.Parallel()
@@ -1101,6 +1117,26 @@ func TestQuerySlice(t *testing.T) {
11011117
{Path: Normalized(Index(0), Index(1)), Node: 42},
11021118
},
11031119
},
1120+
{
1121+
test: "string_slice",
1122+
segs: []*Segment{Child(Slice())},
1123+
input: []string{"x", "y"},
1124+
exp: []any{"x", "y"},
1125+
loc: []*LocatedNode{
1126+
{Path: Normalized(Index(0)), Node: "x"},
1127+
{Path: Normalized(Index(1)), Node: "y"},
1128+
},
1129+
},
1130+
{
1131+
test: "int_slice",
1132+
segs: []*Segment{Child(Slice())},
1133+
input: []int{42, 99},
1134+
exp: []any{42, 99},
1135+
loc: []*LocatedNode{
1136+
{Path: Normalized(Index(0)), Node: 42},
1137+
{Path: Normalized(Index(1)), Node: 99},
1138+
},
1139+
},
11041140
} {
11051141
t.Run(tc.test, func(t *testing.T) {
11061142
t.Parallel()

spec/selector.go

Lines changed: 77 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
)
@@ -223,7 +224,23 @@ func (i Index) Select(input, _ any) []any {
223224
} else if idx < len(val) {
224225
return []any{val[idx]}
225226
}
227+
return make([]any, 0)
226228
}
229+
230+
// Select from any kind of slice.
231+
val := reflect.ValueOf(input)
232+
if val.Kind() == reflect.Slice {
233+
idx := int(i)
234+
if idx < 0 {
235+
if idx = val.Len() + idx; idx >= 0 {
236+
return []any{val.Index(idx).Interface()}
237+
}
238+
} else if idx < val.Len() {
239+
return []any{val.Index(idx).Interface()}
240+
}
241+
return make([]any, 0)
242+
}
243+
227244
return make([]any, 0)
228245
}
229246

@@ -241,7 +258,26 @@ func (i Index) SelectLocated(input, _ any, parent NormalizedPath) []*LocatedNode
241258
} else if idx < len(val) {
242259
return []*LocatedNode{newLocatedNode(append(parent, Index(idx)), val[idx])}
243260
}
261+
return make([]*LocatedNode, 0)
244262
}
263+
264+
// Select from any kind of slice.
265+
val := reflect.ValueOf(input)
266+
if val.Kind() == reflect.Slice {
267+
idx := int(i)
268+
if idx < 0 {
269+
if idx = val.Len() + idx; idx >= 0 {
270+
return []*LocatedNode{newLocatedNode(
271+
append(parent, Index(idx)), val.Index(idx).Interface(),
272+
)}
273+
}
274+
} else if idx < val.Len() {
275+
return []*LocatedNode{newLocatedNode(
276+
append(parent, Index(idx)), val.Index(idx).Interface(),
277+
)}
278+
}
279+
}
280+
245281
return make([]*LocatedNode, 0)
246282
}
247283

@@ -377,6 +413,25 @@ func (s SliceSelector) Select(input, _ any) []any {
377413
}
378414
return res
379415
}
416+
417+
// Select from any kind of slice.
418+
val := reflect.ValueOf(input)
419+
if val.Kind() == reflect.Slice {
420+
lower, upper := s.Bounds(val.Len())
421+
res := make([]any, 0, val.Len())
422+
switch {
423+
case s.step > 0:
424+
for i := lower; i < upper; i += s.step {
425+
res = append(res, val.Index(i).Interface())
426+
}
427+
case s.step < 0:
428+
for i := upper; lower < i; i += s.step {
429+
res = append(res, val.Index(i).Interface())
430+
}
431+
}
432+
return res
433+
}
434+
380435
return make([]any, 0)
381436
}
382437

@@ -401,6 +456,28 @@ func (s SliceSelector) SelectLocated(input, _ any, parent NormalizedPath) []*Loc
401456
}
402457
return res
403458
}
459+
460+
// Select from any kind of slice.
461+
val := reflect.ValueOf(input)
462+
if val.Kind() == reflect.Slice {
463+
lower, upper := s.Bounds(val.Len())
464+
res := make([]*LocatedNode, 0, val.Len())
465+
switch {
466+
case s.step > 0:
467+
for i := lower; i < upper; i += s.step {
468+
res = append(res, newLocatedNode(
469+
append(parent, Index(i)), val.Index(i).Interface(),
470+
))
471+
}
472+
case s.step < 0:
473+
for i := upper; lower < i; i += s.step {
474+
res = append(res, newLocatedNode(
475+
append(parent, Index(i)), val.Index(i).Interface(),
476+
))
477+
}
478+
}
479+
return res
480+
}
404481
return make([]*LocatedNode, 0)
405482
}
406483

spec/selector_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,36 @@ func TestSliceSelect(t *testing.T) {
655655
exp: []any{},
656656
loc: []*LocatedNode{},
657657
},
658+
{
659+
test: "src_string_slice",
660+
sel: Slice(0, 2),
661+
src: []string{"hi", "bye", "x"},
662+
exp: []any{"hi", "bye"},
663+
loc: []*LocatedNode{
664+
{Path: Normalized(Index(0)), Node: "hi"},
665+
{Path: Normalized(Index(1)), Node: "bye"},
666+
},
667+
},
668+
{
669+
test: "src_int_slice",
670+
sel: Slice(),
671+
src: []int{42, 1024},
672+
exp: []any{42, 1024},
673+
loc: []*LocatedNode{
674+
{Path: Normalized(Index(0)), Node: 42},
675+
{Path: Normalized(Index(1)), Node: 1024},
676+
},
677+
},
678+
{
679+
test: "src_object_slice",
680+
sel: Slice(),
681+
src: []map[string]any{{"x": 1}, {"y": true}},
682+
exp: []any{map[string]any{"x": 1}, map[string]any{"y": true}},
683+
loc: []*LocatedNode{
684+
{Path: Normalized(Index(0)), Node: map[string]any{"x": 1}},
685+
{Path: Normalized(Index(1)), Node: map[string]any{"y": true}},
686+
},
687+
},
658688
} {
659689
t.Run(tc.test, func(t *testing.T) {
660690
t.Parallel()

0 commit comments

Comments
 (0)