diff --git a/goextVersion.go b/goextVersion.go index bfcc20b..8419e95 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.366" +const GoextVersion = "0.0.367" -const GoextVersionTimestamp = "2024-01-12T15:10:48+0100" +const GoextVersionTimestamp = "2024-01-12T18:40:29+0100" diff --git a/reflectext/mapAccess.go b/reflectext/mapAccess.go new file mode 100644 index 0000000..f0a67c5 --- /dev/null +++ b/reflectext/mapAccess.go @@ -0,0 +1,98 @@ +package reflectext + +import ( + "reflect" + "strings" +) + +// GetMapPath returns the value deep inside a hierahically nested map structure +// eg: +// x := langext.H{"K1": langext.H{"K2": 665}} +// GetMapPath[int](x, "K1.K2") == 665 +func GetMapPath[TData any](mapval any, path string) (TData, bool) { + var ok bool + + split := strings.Split(path, ".") + + for i, key := range split { + + if i < len(split)-1 { + mapval, ok = GetMapField[any](mapval, key) + if !ok { + return *new(TData), false + } + } else { + return GetMapField[TData](mapval, key) + } + } + + return *new(TData), false +} + +// GetMapField gets the value of a map, without knowing the actual types (mapval is any) +// eg: +// x := langext.H{"K1": 665} +// GetMapPath[int](x, "K1") == 665 +// +// works with aliased types and autom. dereferences pointes +func GetMapField[TData any, TKey comparable](mapval any, key TKey) (TData, bool) { + + rval := reflect.ValueOf(mapval) + + for rval.Kind() == reflect.Ptr && !rval.IsNil() { + rval = rval.Elem() + } + + if rval.Kind() != reflect.Map { + return *new(TData), false // mapval is not a map + } + + kval := reflect.ValueOf(key) + + if !kval.Type().AssignableTo(rval.Type().Key()) { + return *new(TData), false // key cannot index mapval + } + + eval := rval.MapIndex(kval) + if !eval.IsValid() { + return *new(TData), false // key does not exist in mapval + } + + destType := reflect.TypeOf(new(TData)).Elem() + + if eval.Type() == destType { + return eval.Interface().(TData), true + } + + if eval.CanConvert(destType) && !preventConvert(eval.Type(), destType) { + return eval.Convert(destType).Interface().(TData), true + } + + if (eval.Kind() == reflect.Ptr || eval.Kind() == reflect.Interface) && eval.IsNil() && destType.Kind() == reflect.Ptr { + return *new(TData), false // special case: mapval[key] is nil + } + + for (eval.Kind() == reflect.Ptr || eval.Kind() == reflect.Interface) && !eval.IsNil() { + eval = eval.Elem() + + if eval.Type() == destType { + return eval.Interface().(TData), true + } + + if eval.CanConvert(destType) && !preventConvert(eval.Type(), destType) { + return eval.Convert(destType).Interface().(TData), true + } + } + + return *new(TData), false // mapval[key] is not of type TData +} + +func preventConvert(t1 reflect.Type, t2 reflect.Type) bool { + if t1.Kind() == reflect.String && t1.Kind() != reflect.String { + return true + } + if t2.Kind() == reflect.String && t1.Kind() != reflect.String { + return true + } + return false +} diff --git a/reflectext/mapAccess_test.go b/reflectext/mapAccess_test.go new file mode 100644 index 0000000..4c6a687 --- /dev/null +++ b/reflectext/mapAccess_test.go @@ -0,0 +1,55 @@ +package reflectext + +import ( + "fmt" + "gogs.mikescher.com/BlackForestBytes/goext/tst" + "testing" +) + +func TestGetMapPath(t *testing.T) { + type PseudoInt = int64 + + mymap2 := map[string]map[string]any{"Test": {"Second": 3}} + + var maany2 any = mymap2 + + tst.AssertEqual(t, fmt.Sprint(GetMapPath[int](maany2, "Test.Second")), "3 true") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[int](maany2, "Test2.Second")), "0 false") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[int](maany2, "Test.Second2")), "0 false") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[string](maany2, "Test.Second")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[string](maany2, "Test2.Second")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[string](maany2, "Test.Second2")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[PseudoInt](maany2, "Test.Second")), "3 true") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[PseudoInt](maany2, "Test2.Second")), "0 false") + tst.AssertEqual(t, fmt.Sprint(GetMapPath[PseudoInt](maany2, "Test.Second2")), "0 false") +} + +func TestGetMapField(t *testing.T) { + type PseudoInt = int64 + + mymap1 := map[string]any{"Test": 12} + mymap2 := map[string]int{"Test": 12} + + var maany1 any = mymap1 + var maany2 any = mymap2 + + tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany1, "Test")), "12 true") + tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany1, "Test2")), "0 false") + tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany1, "Test")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany1, "Test2")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany1, "Test")), "12 true") + tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany1, "Test2")), "0 false") + + tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany2, "Test")), "12 true") + tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany2, "Test2")), "0 false") + tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany2, "Test")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany2, "Test2")), "false") + tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany2, "Test")), "12 true") + tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany2, "Test2")), "0 false") +} + +func main2() { +} + +func main() { +}