Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 75 additions & 40 deletions pkg/utils/value/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,84 +5,119 @@ import (
"strings"
)

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

func field(value interface{}, keypath []string) interface{} {
if len(keypath) == 0 || value == nil {
return value
func isNil(v reflect.Value) bool {
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr,
reflect.UnsafePointer, reflect.Interface, reflect.Slice:

return v.IsNil()
default:
return false
}
}

func field(v reflect.Value, keypath []string) reflect.Value {
if len(keypath) == 0 || isNil(v) || !v.IsValid() {
return v
}

v := reflect.ValueOf(value)
switch v.Type().Kind() {
case reflect.Ptr:
return field(v.Elem().Interface(), keypath)
case reflect.Ptr, reflect.Interface:
return field(v.Elem(), keypath)

case reflect.Array, reflect.Slice:
r := make([]interface{}, v.Len())
var r = make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
vv := v.Index(i)
r[i] = field(vv.Interface(), keypath)
var vv = v.Index(i)
var rv = field(vv, keypath)
if rv.IsValid() && rv.CanInterface() {
r[i] = rv.Interface()
}
}
return r
return reflect.ValueOf(r)

case reflect.Struct:
r := extractStructField(v, keypath[0])
var r = extractStructField(v, keypath[0])
return field(r, keypath[1:])

case reflect.Map:
r := extractMapField(v, keypath[0])
return field(r, keypath[1:])
var r, remainingKeypath = extractMapField(v, keypath)
return field(r, remainingKeypath)

}
return nil
return reflect.Value{}
}

func extractStructField(v reflect.Value, key string) (r interface{}) {
func extractStructField(v reflect.Value, key string) reflect.Value {
vt := v.Type()
if field, ok := vt.FieldByName(key); ok {
rv := v.FieldByIndex(field.Index)
if rv.IsValid() && rv.CanInterface() {
return rv.Interface()
return rv
}
}
if m, ok := vt.MethodByName(key); ok {
return invokeMethod(v, m)
}
return nil

if v.CanAddr() {
var pv = v.Addr()
var pvt = pv.Type()
if m, ok := pvt.MethodByName(key); ok {
return invokeMethod(pv, m)
}
}

return reflect.Value{}
}

func extractMapField(v reflect.Value, key string) (r interface{}) {
rv := v.MapIndex(reflect.ValueOf(key))
if rv.IsValid() && rv.CanInterface() {
return rv.Interface()
func extractMapField(v reflect.Value, keypath []string) (r reflect.Value, remainingKeypath []string) {
for i := range keypath {
var key = strings.Join(keypath[:i+1], ".")
var rv = v.MapIndex(reflect.ValueOf(key))
if rv.IsValid() && rv.CanInterface() {
return rv, keypath[i+1:]
}
}
return nil
return reflect.Value{}, nil
}

func invokeMethod(v reflect.Value, m reflect.Method) (r interface{}) {
func invokeMethod(v reflect.Value, m reflect.Method) reflect.Value {
if m.Type.NumIn() == 1 && m.Type.NumOut() >= 1 {
rvs := m.Func.Call([]reflect.Value{v})
return rvs[0].Interface()
return rvs[0]
}
return nil
return reflect.Value{}
}
43 changes: 30 additions & 13 deletions pkg/utils/value/field_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package value_test

import (
"errors"
"fmt"
"reflect"
"testing"
Expand All @@ -19,7 +20,7 @@ func verifyFieldTestCase(t *testing.T, tc TestCase) {
t.Helper()
t.Run(fmt.Sprintf("Given %v", tc.name), func(t *testing.T) {
t.Helper()
t.Run("when calling extractValue", func(t *testing.T) {
t.Run(fmt.Sprintf("when calling Field(\"%v\")", tc.keypath), func(t *testing.T) {
t.Helper()
r := value.Field(tc.value, tc.keypath)
t.Run("then expected value is returned", func(t *testing.T) {
Expand All @@ -34,11 +35,16 @@ func verifyFieldTestCase(t *testing.T, tc TestCase) {
})
}

func TestField(t *testing.T) {

// -----------------------------------------------------------------------
// Struct-based cases
func TestFieldOnNonFieldTypes(t *testing.T) {
verifyFieldTestCase(t, TestCase{
name: "an integer value",
value: 123,
keypath: "Name",
expected: nil,
})
}

func TestFieldOnStructTypes(t *testing.T) {
var v = []TestStruct{
{Name: "aaa"},
{Name: "bbb"},
Expand Down Expand Up @@ -100,9 +106,9 @@ func TestField(t *testing.T) {
keypath: "FuncArgs",
expected: []interface{}{nil, nil, nil},
})
}

// -----------------------------------------------------------------------
// Struct special cases
func TestFieldOnArrayOfStructTypes(t *testing.T) {
verifyFieldTestCase(t, TestCase{
name: "an array of pointers to struct",
value: []*TestStruct{
Expand All @@ -113,10 +119,9 @@ func TestField(t *testing.T) {
keypath: "Name",
expected: []interface{}{"aaa", "bbb", "ccc"},
})
}

// -----------------------------------------------------------------------
// map-based cases

func TestFieldOnMapTypes(t *testing.T) {
verifyFieldTestCase(t, TestCase{
name: "an map with matching keys",
value: []obj{
Expand Down Expand Up @@ -145,10 +150,9 @@ func TestField(t *testing.T) {
keypath: "Name",
expected: []interface{}{nil, nil, nil},
})
}

// -----------------------------------------------------------------------
// map-based special cases

func TestFieldOnArrayOfMapTypes(t *testing.T) {
verifyFieldTestCase(t, TestCase{
name: "an map with non-matching keys",
value: []interface{}{
Expand Down Expand Up @@ -183,6 +187,19 @@ func TestField(t *testing.T) {
keypath: "Name.aaa",
expected: []interface{}{nil, []interface{}{"bbb", "ccc"}, nil},
})

}

func TestFieldWithNiladicFunctionTarget(t *testing.T) {
var sentinel = errors.New("sentinel")
var err = fmt.Errorf("error: %w", sentinel)

verifyFieldTestCase(t, TestCase{
name: "Error() value on an error",
value: err,
keypath: "Error",
expected: "error: sentinel",
})
}

// ---------------------------------------------------------------------------
Expand Down