Skip to content

Commit d6460e1

Browse files
committed
Adding ExecutePath utility
1 parent 7c3de94 commit d6460e1

File tree

2 files changed

+178
-1
lines changed

2 files changed

+178
-1
lines changed

jpath/jpath.go

+108
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
package jpath
22

33
import (
4+
"errors"
45
"fmt"
6+
"reflect"
7+
"strconv"
58
"strings"
69
)
710

11+
var (
12+
ErrInvalidPath = errors.New("invalid path")
13+
ErrNotSlice = errors.New("not a slice")
14+
ErrNotMap = errors.New("not a map")
15+
ErrOutOfBounds = errors.New("index out of bounds")
16+
ErrInvalidInterface = errors.New("cannot get interface of value")
17+
ErrNil = errors.New("cannot get index or key of nil")
18+
ErrKeyType = errors.New("cannot handle this type of key")
19+
)
20+
821
// StripIndices removes the characters in between brackets in a json path
922
func StripIndices(path string) string {
1023
r := make([]byte, 0, len(path))
@@ -61,3 +74,98 @@ func EscapeKey(v interface{}) string {
6174
}
6275
return fmt.Sprintf("%q", s)
6376
}
77+
78+
func Split(path string) (head, tail string) {
79+
if path == "" {
80+
return "", ""
81+
}
82+
83+
if path[0] == '[' {
84+
for i := 1; i < len(path); i++ {
85+
if path[i] == ']' {
86+
return path[0 : i+1], path[i+1 : len(path)]
87+
}
88+
}
89+
}
90+
// Skipping first character as we espect the path to start with a dot.
91+
for i := 1; i < len(path); i++ {
92+
if path[i] == '.' || path[i] == '[' {
93+
return path[0:i], path[i:len(path)]
94+
}
95+
}
96+
97+
// tail not found, returning the path as head
98+
return path, ""
99+
}
100+
101+
func getKey(s string, kind reflect.Kind) (reflect.Value, error) {
102+
switch kind {
103+
default:
104+
return reflect.Value{}, ErrKeyType
105+
case reflect.Int:
106+
i, err := strconv.Atoi(s)
107+
if err != nil {
108+
return reflect.Value{}, err
109+
}
110+
return reflect.ValueOf(i), nil
111+
case reflect.String:
112+
return reflect.ValueOf(s), nil
113+
}
114+
}
115+
116+
func ExecutePath(path string, i interface{}) (interface{}, error) {
117+
// TODO(yazgazan): better errors
118+
head, tail := Split(path)
119+
if head == "" {
120+
return i, nil
121+
}
122+
123+
v := reflect.ValueOf(i)
124+
125+
switch head[0] {
126+
default:
127+
return nil, ErrInvalidPath
128+
case '[':
129+
if v.Kind() != reflect.Slice {
130+
return nil, ErrNotSlice
131+
}
132+
if v.IsNil() {
133+
return nil, ErrNil
134+
}
135+
if head[len(head)-1] != ']' {
136+
return nil, ErrInvalidPath
137+
}
138+
index, err := strconv.Atoi(head[1 : len(head)-1])
139+
if err != nil {
140+
return nil, err
141+
}
142+
if index >= v.Len() {
143+
return nil, ErrOutOfBounds
144+
}
145+
val := v.Index(index)
146+
if !val.CanInterface() {
147+
return nil, ErrInvalidInterface
148+
}
149+
return ExecutePath(tail, val.Interface())
150+
case '.':
151+
if v.Kind() != reflect.Map {
152+
return nil, ErrNotMap
153+
}
154+
keyStr := head[1:len(head)]
155+
if keyStr == "" {
156+
return nil, ErrInvalidPath
157+
}
158+
key, err := getKey(keyStr, v.Type().Key().Kind())
159+
if err != nil {
160+
return nil, err
161+
}
162+
if v.IsNil() {
163+
return nil, ErrNil
164+
}
165+
val := v.MapIndex(key)
166+
if !val.CanInterface() {
167+
return nil, ErrInvalidInterface
168+
}
169+
return ExecutePath(tail, val.Interface())
170+
}
171+
}

jpath/jpath_test.go

+70-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package jpath
22

3-
import "testing"
3+
import (
4+
"reflect"
5+
"testing"
6+
)
47

58
func TestStripIndices(t *testing.T) {
69
for _, test := range []struct {
@@ -65,3 +68,69 @@ func TestHasSuffix(t *testing.T) {
6568
}
6669
}
6770
}
71+
72+
func TestSplit(t *testing.T) {
73+
for _, test := range []struct {
74+
In string
75+
Head string
76+
Tail string
77+
}{
78+
{"", "", ""},
79+
{".", ".", ""},
80+
{"foo", "foo", ""},
81+
{".foo.", ".foo", "."},
82+
{".foo", ".foo", ""},
83+
{".foo.bar", ".foo", ".bar"},
84+
{".foo[2].bar.fizz", ".foo", "[2].bar.fizz"},
85+
{"[2].bar.fizz", "[2]", ".bar.fizz"},
86+
{"[2]", "[2]", ""},
87+
} {
88+
head, tail := Split(test.In)
89+
if head != test.Head || tail != test.Tail {
90+
t.Errorf("Split(%q) = (%q, %q), expected (%q, %q)", test.In, head, tail, test.Head, test.Tail)
91+
}
92+
}
93+
}
94+
95+
func TestExecutePath(t *testing.T) {
96+
for _, test := range []struct {
97+
I interface{}
98+
Path string
99+
Expected interface{}
100+
}{
101+
{
102+
map[string]int{"foo": 42},
103+
".foo",
104+
42,
105+
},
106+
{
107+
map[string][]int{
108+
"foo": []int{1, 2, 3},
109+
"bar": []int{4, 5, 6},
110+
},
111+
".foo[1]",
112+
2,
113+
},
114+
{
115+
map[string][]map[int]string{
116+
"foo": []map[int]string{
117+
map[int]string{
118+
23: "ha",
119+
44: "bar",
120+
},
121+
},
122+
},
123+
".foo[0].23",
124+
"ha",
125+
},
126+
} {
127+
got, err := ExecutePath(test.Path, test.I)
128+
if err != nil {
129+
t.Errorf("ExecutePath(%q, %+v): unexpected error: %s", test.Path, test.I, err)
130+
continue
131+
}
132+
if !reflect.DeepEqual(got, test.Expected) {
133+
t.Errorf("ExecutePath(%q, %+v) = %+v, expected %+v", test.Path, test.I, got, test.Expected)
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)