From 67748a2132ede5cbfd1b4f8a63a3d7e57285d7a9 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Sun, 14 Feb 2021 23:33:01 +0300 Subject: [PATCH] util/reflect: import own path based interface lookup Signed-off-by: Vasiliy Tolstov --- util/reflect/path.go | 257 ++++++++++++++++++++++++++++++++++++++ util/reflect/path_test.go | 34 +++++ 2 files changed, 291 insertions(+) create mode 100644 util/reflect/path.go create mode 100644 util/reflect/path_test.go diff --git a/util/reflect/path.go b/util/reflect/path.go new file mode 100644 index 00000000..902add63 --- /dev/null +++ b/util/reflect/path.go @@ -0,0 +1,257 @@ +package reflect + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" +) + +const ( + SplitToken = "." + IndexCloseChar = "]" + IndexOpenChar = "[" +) + +var ( + ErrMalformedIndex = errors.New("Malformed index key") + ErrInvalidIndexUsage = errors.New("Invalid index key usage") + ErrKeyNotFound = errors.New("Unable to find the key") + ErrBadJSONPath = errors.New("Bad path: must start with $ and have more then 2 chars") +) + +func Lookup(i interface{}, path string) (reflect.Value, error) { + if path == "" || path[0:1] != "$" { + return reflect.Value{}, ErrBadJSONPath + } + + if path == "$" { + return reflect.ValueOf(i), nil + } + + if len(path) < 2 { + return reflect.Value{}, ErrBadJSONPath + } + + return lookup(i, strings.Split(path[2:], SplitToken)...) +} + +// Lookup performs a lookup into a value, using a path of keys. The key should +// match with a Field or a MapIndex. For slice you can use the syntax key[index] +// to access a specific index. If one key owns to a slice and an index is not +// specificied the rest of the path will be apllied to evaley value of the +// slice, and the value will be merged into a slice. +func lookup(i interface{}, path ...string) (reflect.Value, error) { + value := reflect.ValueOf(i) + var parent reflect.Value + var err error + + for i, part := range path { + parent = value + + value, err = getValueByName(value, part) + if err == nil { + continue + } + + if !isAggregable(parent) { + break + } + + value, err = aggreateAggregableValue(parent, path[i:]) + + break + } + + return value, err +} + +func getValueByName(v reflect.Value, key string) (reflect.Value, error) { + var value reflect.Value + var index int + var err error + + key, index, err = parseIndex(key) + if err != nil { + return value, err + } + switch v.Kind() { + case reflect.Ptr, reflect.Interface: + return getValueByName(v.Elem(), key) + case reflect.Struct: + value = v.FieldByName(key) + case reflect.Map: + kValue := reflect.Indirect(reflect.New(v.Type().Key())) + kValue.SetString(key) + value = v.MapIndex(kValue) + } + + if !value.IsValid() { + return reflect.Value{}, ErrKeyNotFound + } + + if index != -1 { + if value.Type().Kind() != reflect.Slice { + return reflect.Value{}, ErrInvalidIndexUsage + } + + value = value.Index(index) + } + + if value.Kind() == reflect.Ptr || value.Kind() == reflect.Interface { + value = value.Elem() + } + + return value, nil +} + +func aggreateAggregableValue(v reflect.Value, path []string) (reflect.Value, error) { + values := make([]reflect.Value, 0) + + l := v.Len() + if l == 0 { + ty, ok := lookupType(v.Type(), path...) + if !ok { + return reflect.Value{}, ErrKeyNotFound + } + return reflect.MakeSlice(reflect.SliceOf(ty), 0, 0), nil + } + + switch v.Kind() { + case reflect.Slice, reflect.Map: + break + default: + return reflect.Value{}, fmt.Errorf("unsuported kind for index") + } + + index := indexFunction(v) + for i := 0; i < l; i++ { + value, err := lookup(index(i).Interface(), path...) + if err != nil { + return reflect.Value{}, err + } + + values = append(values, value) + } + + return mergeValue(values), nil +} + +func indexFunction(v reflect.Value) func(i int) reflect.Value { + switch v.Kind() { + case reflect.Slice: + return v.Index + case reflect.Map: + keys := v.MapKeys() + return func(i int) reflect.Value { + return v.MapIndex(keys[i]) + } + } + return func(i int) reflect.Value { return reflect.Value{} } +} + +func mergeValue(values []reflect.Value) reflect.Value { + values = removeZeroValues(values) + l := len(values) + if l == 0 { + return reflect.Value{} + } + + sample := values[0] + mergeable := isMergeable(sample) + + t := sample.Type() + if mergeable { + t = t.Elem() + } + + value := reflect.MakeSlice(reflect.SliceOf(t), 0, 0) + for i := 0; i < l; i++ { + if !values[i].IsValid() { + continue + } + + if mergeable { + value = reflect.AppendSlice(value, values[i]) + } else { + value = reflect.Append(value, values[i]) + } + } + + return value +} + +func removeZeroValues(values []reflect.Value) []reflect.Value { + l := len(values) + + var v []reflect.Value + for i := 0; i < l; i++ { + if values[i].IsValid() { + v = append(v, values[i]) + } + } + + return v +} + +func isAggregable(v reflect.Value) bool { + k := v.Kind() + + return k == reflect.Map || k == reflect.Slice +} + +func isMergeable(v reflect.Value) bool { + k := v.Kind() + return k == reflect.Map || k == reflect.Slice +} + +func hasIndex(s string) bool { + return strings.Contains(s, IndexOpenChar) +} + +func parseIndex(s string) (string, int, error) { + start := strings.Index(s, IndexOpenChar) + end := strings.Index(s, IndexCloseChar) + + if start == -1 && end == -1 { + return s, -1, nil + } + + if (start != -1 && end == -1) || (start == -1 && end != -1) { + return "", -1, ErrMalformedIndex + } + + index, err := strconv.Atoi(s[start+1 : end]) + if err != nil { + return "", -1, ErrMalformedIndex + } + + return s[:start], index, nil +} + +func lookupType(ty reflect.Type, path ...string) (reflect.Type, bool) { + if len(path) == 0 { + return ty, true + } + + switch ty.Kind() { + case reflect.Slice, reflect.Array, reflect.Map: + if hasIndex(path[0]) { + return lookupType(ty.Elem(), path[1:]...) + } + // Aggregate. + return lookupType(ty.Elem(), path...) + case reflect.Ptr: + return lookupType(ty.Elem(), path...) + case reflect.Interface: + // We can't know from here without a value. Let's just return this type. + return ty, true + case reflect.Struct: + f, ok := ty.FieldByName(path[0]) + if ok { + return lookupType(f.Type, path[1:]...) + } + } + return nil, false +} diff --git a/util/reflect/path_test.go b/util/reflect/path_test.go new file mode 100644 index 00000000..4954a022 --- /dev/null +++ b/util/reflect/path_test.go @@ -0,0 +1,34 @@ +package reflect + +import ( + "testing" +) + +func TestPath(t *testing.T) { + + type Nested2 struct { + Name string + } + type Nested1 struct { + Nested2 Nested2 + } + type Config struct { + Nested1 Nested1 + } + + cfg := &Config{ + Nested1: Nested1{ + Nested2: Nested2{ + Name: "NAME", + }, + }, + } + + v, err := Lookup(cfg, "$.Nested1.Nested2.Name") + if err != nil { + t.Fatal(err) + } + if v.String() != "NAME" { + t.Fatalf("lookup returns invalid value: %v", v) + } +}