Skip to content

Commit da3d445

Browse files
authored
Update value.Field implementation (#30)
- support '.' in map keys - support pointer receiver for method invocation - rewrite internals to operate on `reflect.Value` rather than `interface{}`
1 parent 2fcf77b commit da3d445

File tree

2 files changed

+105
-53
lines changed

2 files changed

+105
-53
lines changed

pkg/utils/value/field.go

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,84 +5,119 @@ import (
55
"strings"
66
)
77

8-
// Field takes a root value or an array of root values, navigates through the
9-
// data tree according to a keypath, and returns the targeted values. `keypath`
10-
// is a dot-separated list of keys, each used as either field name in a struct,
11-
// a key in a map, or a niladic method name. Any error during key evaluation
12-
// results in a nil value. If a method invocation yields multiple return values,
13-
// only the first one is captured.
8+
// Field takes a value or an array of values, navigates through the data tree
9+
// according to a keypath, and returns the targeted values. `keypath` is a
10+
// dot-separated list of keys, each used as either field name in a struct, a key
11+
// in a map, or a niladic method name. Once a lookup is successful on the first
12+
// fragment of the keypath, the evaluation continue recursively with the lookup
13+
// result and the remainder of the keypath. If a key lookup fails, a nil value
14+
// is returned. If a key lookup results in a method invocation that yields
15+
// multiple values, only the first one is captured.
1416
//
15-
// When the root is a single object and all the fields along the keypath are
16-
// scalar types, the result is a scalar value. For each array or slice type
17-
// along the path, the result become a slice collecting the result of
17+
// In cases where the value is a map[string]..., and the first keypath fragment
18+
// is not a valid key, all partial keypaths are considered as potential keys.
19+
// For example, if the keypath is "foo.bar.baz", "foo" is considered first, then
20+
// "foo.bar", then "foo.bar.baz". However, if the map contains both "foo" and
21+
// "foo.bar" keys, only "foo" will be accessible with this method.
22+
//
23+
// When the current value is a single object and all the fields along the
24+
// keypath are scalar types, the result is a scalar value. For each array or
25+
// slice type along the path, the result become a slice collecting the result of
1826
// evaluating the sub-path on each individual element. The shape of the result
1927
// is then a N-dimensional array, where N is the number of arrays traversed
20-
// along the path.
21-
//
22-
// Because the implementation is using the reflect package and is mostly type
23-
// agnostic, the resulting arrays are always of type []interface{}, even if the
24-
// field types are consistent across values.
28+
// along the path. Arrays are always returns as a type agnostic array
29+
// (`[]interface{}`), even if all the values have a consistent type.
2530
func Field(value interface{}, keypath string) interface{} {
2631
var keys = strings.Split(keypath, ".")
27-
return field(value, keys)
32+
var v = reflect.ValueOf(value)
33+
var rv = field(v, keys)
34+
if rv.IsValid() && rv.CanInterface() {
35+
return rv.Interface()
36+
}
37+
return nil
2838
}
2939

30-
func field(value interface{}, keypath []string) interface{} {
31-
if len(keypath) == 0 || value == nil {
32-
return value
40+
func isNil(v reflect.Value) bool {
41+
switch v.Kind() {
42+
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr,
43+
reflect.UnsafePointer, reflect.Interface, reflect.Slice:
44+
45+
return v.IsNil()
46+
default:
47+
return false
48+
}
49+
}
50+
51+
func field(v reflect.Value, keypath []string) reflect.Value {
52+
if len(keypath) == 0 || isNil(v) || !v.IsValid() {
53+
return v
3354
}
3455

35-
v := reflect.ValueOf(value)
3656
switch v.Type().Kind() {
37-
case reflect.Ptr:
38-
return field(v.Elem().Interface(), keypath)
57+
case reflect.Ptr, reflect.Interface:
58+
return field(v.Elem(), keypath)
3959

4060
case reflect.Array, reflect.Slice:
41-
r := make([]interface{}, v.Len())
61+
var r = make([]interface{}, v.Len())
4262
for i := 0; i < v.Len(); i++ {
43-
vv := v.Index(i)
44-
r[i] = field(vv.Interface(), keypath)
63+
var vv = v.Index(i)
64+
var rv = field(vv, keypath)
65+
if rv.IsValid() && rv.CanInterface() {
66+
r[i] = rv.Interface()
67+
}
4568
}
46-
return r
69+
return reflect.ValueOf(r)
4770

4871
case reflect.Struct:
49-
r := extractStructField(v, keypath[0])
72+
var r = extractStructField(v, keypath[0])
5073
return field(r, keypath[1:])
5174

5275
case reflect.Map:
53-
r := extractMapField(v, keypath[0])
54-
return field(r, keypath[1:])
76+
var r, remainingKeypath = extractMapField(v, keypath)
77+
return field(r, remainingKeypath)
5578

5679
}
57-
return nil
80+
return reflect.Value{}
5881
}
5982

60-
func extractStructField(v reflect.Value, key string) (r interface{}) {
83+
func extractStructField(v reflect.Value, key string) reflect.Value {
6184
vt := v.Type()
6285
if field, ok := vt.FieldByName(key); ok {
6386
rv := v.FieldByIndex(field.Index)
6487
if rv.IsValid() && rv.CanInterface() {
65-
return rv.Interface()
88+
return rv
6689
}
6790
}
6891
if m, ok := vt.MethodByName(key); ok {
6992
return invokeMethod(v, m)
7093
}
71-
return nil
94+
95+
if v.CanAddr() {
96+
var pv = v.Addr()
97+
var pvt = pv.Type()
98+
if m, ok := pvt.MethodByName(key); ok {
99+
return invokeMethod(pv, m)
100+
}
101+
}
102+
103+
return reflect.Value{}
72104
}
73105

74-
func extractMapField(v reflect.Value, key string) (r interface{}) {
75-
rv := v.MapIndex(reflect.ValueOf(key))
76-
if rv.IsValid() && rv.CanInterface() {
77-
return rv.Interface()
106+
func extractMapField(v reflect.Value, keypath []string) (r reflect.Value, remainingKeypath []string) {
107+
for i := range keypath {
108+
var key = strings.Join(keypath[:i+1], ".")
109+
var rv = v.MapIndex(reflect.ValueOf(key))
110+
if rv.IsValid() && rv.CanInterface() {
111+
return rv, keypath[i+1:]
112+
}
78113
}
79-
return nil
114+
return reflect.Value{}, nil
80115
}
81116

82-
func invokeMethod(v reflect.Value, m reflect.Method) (r interface{}) {
117+
func invokeMethod(v reflect.Value, m reflect.Method) reflect.Value {
83118
if m.Type.NumIn() == 1 && m.Type.NumOut() >= 1 {
84119
rvs := m.Func.Call([]reflect.Value{v})
85-
return rvs[0].Interface()
120+
return rvs[0]
86121
}
87-
return nil
122+
return reflect.Value{}
88123
}

pkg/utils/value/field_test.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package value_test
22

33
import (
4+
"errors"
45
"fmt"
56
"reflect"
67
"testing"
@@ -19,7 +20,7 @@ func verifyFieldTestCase(t *testing.T, tc TestCase) {
1920
t.Helper()
2021
t.Run(fmt.Sprintf("Given %v", tc.name), func(t *testing.T) {
2122
t.Helper()
22-
t.Run("when calling extractValue", func(t *testing.T) {
23+
t.Run(fmt.Sprintf("when calling Field(\"%v\")", tc.keypath), func(t *testing.T) {
2324
t.Helper()
2425
r := value.Field(tc.value, tc.keypath)
2526
t.Run("then expected value is returned", func(t *testing.T) {
@@ -34,11 +35,16 @@ func verifyFieldTestCase(t *testing.T, tc TestCase) {
3435
})
3536
}
3637

37-
func TestField(t *testing.T) {
38-
39-
// -----------------------------------------------------------------------
40-
// Struct-based cases
38+
func TestFieldOnNonFieldTypes(t *testing.T) {
39+
verifyFieldTestCase(t, TestCase{
40+
name: "an integer value",
41+
value: 123,
42+
keypath: "Name",
43+
expected: nil,
44+
})
45+
}
4146

47+
func TestFieldOnStructTypes(t *testing.T) {
4248
var v = []TestStruct{
4349
{Name: "aaa"},
4450
{Name: "bbb"},
@@ -100,9 +106,9 @@ func TestField(t *testing.T) {
100106
keypath: "FuncArgs",
101107
expected: []interface{}{nil, nil, nil},
102108
})
109+
}
103110

104-
// -----------------------------------------------------------------------
105-
// Struct special cases
111+
func TestFieldOnArrayOfStructTypes(t *testing.T) {
106112
verifyFieldTestCase(t, TestCase{
107113
name: "an array of pointers to struct",
108114
value: []*TestStruct{
@@ -113,10 +119,9 @@ func TestField(t *testing.T) {
113119
keypath: "Name",
114120
expected: []interface{}{"aaa", "bbb", "ccc"},
115121
})
122+
}
116123

117-
// -----------------------------------------------------------------------
118-
// map-based cases
119-
124+
func TestFieldOnMapTypes(t *testing.T) {
120125
verifyFieldTestCase(t, TestCase{
121126
name: "an map with matching keys",
122127
value: []obj{
@@ -145,10 +150,9 @@ func TestField(t *testing.T) {
145150
keypath: "Name",
146151
expected: []interface{}{nil, nil, nil},
147152
})
153+
}
148154

149-
// -----------------------------------------------------------------------
150-
// map-based special cases
151-
155+
func TestFieldOnArrayOfMapTypes(t *testing.T) {
152156
verifyFieldTestCase(t, TestCase{
153157
name: "an map with non-matching keys",
154158
value: []interface{}{
@@ -183,6 +187,19 @@ func TestField(t *testing.T) {
183187
keypath: "Name.aaa",
184188
expected: []interface{}{nil, []interface{}{"bbb", "ccc"}, nil},
185189
})
190+
191+
}
192+
193+
func TestFieldWithNiladicFunctionTarget(t *testing.T) {
194+
var sentinel = errors.New("sentinel")
195+
var err = fmt.Errorf("error: %w", sentinel)
196+
197+
verifyFieldTestCase(t, TestCase{
198+
name: "Error() value on an error",
199+
value: err,
200+
keypath: "Error",
201+
expected: "error: sentinel",
202+
})
186203
}
187204

188205
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)