Skip to content

Commit 0e9e605

Browse files
committed
Allow star filters in parse
1 parent 6ade995 commit 0e9e605

File tree

3 files changed

+144
-19
lines changed

3 files changed

+144
-19
lines changed

TODO.md

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
# ToDo
22

3-
* Allow * in path filtering, e.g.:
4-
5-
Given the data:
6-
7-
[
8-
{
9-
"id": 1
10-
},
11-
{
12-
"id": 2
13-
}
14-
]
15-
16-
`please parse *.id` would result in:
17-
18-
[1, 2]
19-
203
* Tidy up help text - separate input/output params
214

225
Use [optional] and <value goes here> conventions

parser/filter.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import (
88
)
99

1010
// Filter takes structured data and returns just the portion of it that matches the provided path using dot notation.
11-
// e.g. data.header.1
11+
// The path may contain asterisks, in which case all entries will match at that level of the path
12+
// e.g. products.*.id will return all product ids
1213
func Filter(in interface{}, path string) (interface{}, error) {
1314
if path == "" {
1415
return in, nil
@@ -27,6 +28,21 @@ func Filter(in interface{}, path string) (interface{}, error) {
2728

2829
switch val.Kind() {
2930
case reflect.Map:
31+
if thisPath == "*" {
32+
var err error
33+
out := make([]interface{}, val.Len())
34+
35+
for i, key := range val.MapKeys() {
36+
out[i], err = Filter(val.MapIndex(key).Interface(), nextPath)
37+
38+
if err != nil {
39+
return nil, err
40+
}
41+
}
42+
43+
return out, nil
44+
}
45+
3046
for _, key := range val.MapKeys() {
3147
if fmt.Sprint(key.Interface()) == thisPath {
3248
value := val.MapIndex(key).Interface()
@@ -36,6 +52,21 @@ func Filter(in interface{}, path string) (interface{}, error) {
3652

3753
break
3854
case reflect.Array, reflect.Slice:
55+
if thisPath == "*" {
56+
var err error
57+
out := make([]interface{}, val.Len())
58+
59+
for i := 0; i < val.Len(); i++ {
60+
out[i], err = Filter(val.Index(i).Interface(), nextPath)
61+
62+
if err != nil {
63+
return nil, err
64+
}
65+
}
66+
67+
return out, nil
68+
}
69+
3970
index, err := strconv.Atoi(thisPath)
4071

4172
if err != nil || index < 0 || index >= val.Len() {

parser/filter_test.go

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package parser
22

33
import (
4+
"fmt"
5+
"reflect"
6+
"sort"
47
"testing"
58
)
69

@@ -16,7 +19,7 @@ func TestFilter(t *testing.T) {
1619
},
1720
}
1821

19-
cases := map[string]string{
22+
cases := map[string]interface{}{
2023
"top": "I am the top",
2124
"bottom.16": "left",
2225
"bottom.13.1": "right 2",
@@ -35,6 +38,94 @@ func TestFilter(t *testing.T) {
3538
}
3639
}
3740

41+
func TestStarFilterSlice(t *testing.T) {
42+
input := map[string]interface{}{
43+
"top": []string{
44+
"foo",
45+
"bar",
46+
"baz",
47+
},
48+
"bottom": []interface{}{
49+
map[string]int{
50+
"id": 1,
51+
},
52+
map[string]int{
53+
"id": 2,
54+
},
55+
map[string]int{
56+
"id": 3,
57+
},
58+
},
59+
}
60+
61+
cases := map[string]interface{}{
62+
"top.*": []interface{}{"foo", "bar", "baz"},
63+
"bottom.*.id": []interface{}{1, 2, 3},
64+
}
65+
66+
for path, expected := range cases {
67+
actual, err := Filter(input, path)
68+
69+
if err != nil {
70+
t.Fail()
71+
}
72+
73+
if !reflect.DeepEqual(expected, actual) {
74+
t.Errorf("case failed: %v vs %v", expected, actual)
75+
}
76+
}
77+
}
78+
79+
func TestStarFilterMap(t *testing.T) {
80+
input := map[string]interface{}{
81+
"top": map[string]int{
82+
"one": 1,
83+
"two": 2,
84+
"three": 3,
85+
},
86+
"bottom": map[string]interface{}{
87+
"a": map[string]int{
88+
"id": 4,
89+
},
90+
"b": map[string]int{
91+
"id": 5,
92+
},
93+
"c": map[string]int{
94+
"id": 6,
95+
},
96+
},
97+
}
98+
99+
cases := map[string]interface{}{
100+
"top.*": []int{1, 2, 3},
101+
"bottom.*.id": []int{4, 5, 6},
102+
}
103+
104+
for path, expected := range cases {
105+
actual, err := Filter(input, path)
106+
107+
val := reflect.ValueOf(actual)
108+
109+
// Some "fun" type conversion here to make sure things work ok
110+
intActual := make([]int, val.Len())
111+
112+
for i := 0; i < val.Len(); i++ {
113+
fmt.Println(val.Index(i))
114+
intActual[i] = val.Index(i).Interface().(int)
115+
}
116+
117+
sort.Ints(intActual)
118+
119+
if err != nil {
120+
t.Fail()
121+
}
122+
123+
if !reflect.DeepEqual(expected, intActual) {
124+
t.Errorf("case failed: %v vs %v", expected, actual)
125+
}
126+
}
127+
}
128+
38129
func TestFilterBadKey(t *testing.T) {
39130
input := map[string]interface{}{
40131
"foo": "bar",
@@ -46,3 +137,23 @@ func TestFilterBadKey(t *testing.T) {
46137
t.Errorf("unexpected return values: %v, %v", val, err)
47138
}
48139
}
140+
141+
func TestStarFilterBadKey(t *testing.T) {
142+
input := []interface{}{
143+
map[string]int{
144+
"id": 1,
145+
},
146+
map[string]int{
147+
"id": 2,
148+
},
149+
map[string]int{
150+
"notid": 3,
151+
},
152+
}
153+
154+
val, err := Filter(input, "*.id")
155+
156+
if err == nil || err.Error() != "key does not exist: id" {
157+
t.Errorf("unexpected return values: %v, %v", val, err)
158+
}
159+
}

0 commit comments

Comments
 (0)