util/reflect: import own path based interface lookup

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
Василий Толстов 2021-02-14 23:33:01 +03:00
parent c2333a9f35
commit 67748a2132
2 changed files with 291 additions and 0 deletions

257
util/reflect/path.go Normal file
View File

@ -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
}

34
util/reflect/path_test.go Normal file
View File

@ -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)
}
}