From 85df72d128d4a1eb5a1b22ebb6ff809cdc1bfb21 Mon Sep 17 00:00:00 2001 From: Goreorto Date: Thu, 7 May 2026 10:10:17 -0600 Subject: [PATCH 1/2] MapUtil: GetValue Add simple way to get a value from a map[string]any using key path --- maputil/maputil.go | 45 +++++++++++++++++++++++ maputil/maputil_test.go | 81 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/maputil/maputil.go b/maputil/maputil.go index 6221ce4..927c485 100644 --- a/maputil/maputil.go +++ b/maputil/maputil.go @@ -1,6 +1,11 @@ // Package maputil provides a set if functions for working with maps. package maputil // import "github.com/teamwork/utils/v2/maputil" +import ( + "errors" + "fmt" +) + // Swap the keys and values of a map. func Swap[T comparable, V comparable](m map[T]V) map[V]T { n := make(map[V]T) @@ -10,3 +15,43 @@ func Swap[T comparable, V comparable](m map[T]V) map[V]T { return n } + +var ( + ErrWrongType = errors.New("wrong type") + ErrNotFound = errors.New("key not found") +) + +// GetValue returns the value defined as a key path +func GetValue[T any](m map[string]any, keys ...string) (T, error) { + var out T + + v, err := getValue(m, keys) + if err != nil { + return out, err + } + + if v == nil { + return out, ErrNotFound + } + + vv, ok := v.(T) + if !ok { + return out, ErrWrongType + } + + return vv, nil +} + +func getValue(m map[string]any, keys []string) (any, error) { + if len(keys) == 1 { + return m[keys[0]], nil + } + + a := m[keys[0]] + + if m, ok := a.(map[string]any); ok { + return getValue(m, keys[1:]) + } + + return nil, fmt.Errorf("%w key `%s`", ErrNotFound, keys[1]) +} diff --git a/maputil/maputil_test.go b/maputil/maputil_test.go index d6a2a47..58279c4 100644 --- a/maputil/maputil_test.go +++ b/maputil/maputil_test.go @@ -1,6 +1,7 @@ package maputil import ( + "errors" "fmt" "reflect" "testing" @@ -26,3 +27,83 @@ func TestSwap(t *testing.T) { }) } } + +func TestGetValue(t *testing.T) { + m := map[string]any{ + "a": int64(1), + "b": "2", + "c": float64(3.1), + "d": map[string]any{ + "a": int64(4), + "b": "5", + "c": float64(6.1), + }, + } + + outInt64, err := GetValue[int64](m, "a") + if err != nil { + t.Fatal(err) + } + if outInt64 != int64(1) { + t.Fatalf("expected 1, got %v", outInt64) + } + + outStr, err := GetValue[string](m, "b") + if err != nil { + t.Fatal(err) + } + if outStr != "2" { + t.Fatalf("expected \"2\", got %v", outStr) + } + + outFloat, err := GetValue[float64](m, "c") + if err != nil { + t.Fatal(err) + } + if outFloat != 3.1 { + t.Fatalf("expected 3.1, got %v", outFloat) + } + + outInt64, err = GetValue[int64](m, "d", "a") + if err != nil { + t.Fatal(err) + } + if outInt64 != int64(4) { + t.Fatalf("expected 4 got %v", outInt64) + } + + _, err = GetValue[string](m, "a") + if !errors.Is(err, ErrWrongType) { + t.Fatalf("error wrong type expected, got %v", err) + } + + _, err = GetValue[string](m, "d", "a") + if !errors.Is(err, ErrWrongType) { + t.Fatalf("error wrong type expected, got %v", err) + } + + _, err = GetValue[string](m, "e") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("error not found expected, got %v", err) + } + + _, err = GetValue[string](m, "d", "e") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("error not found expected, got %v", err) + } + + _, err = GetValue[string](m, "e", "d") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("error not found expected, got %v", err) + } + + _, err = GetValue[string](m, "d", "c", "a") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("error not found expected, got %v", err) + } + + _, err = GetValue[string](m, "a", "") + if !errors.Is(err, ErrNotFound) { + t.Fatalf("error not found expected, got %v", err) + } +} From 32a43fa8c76abbfbde4c63b1d6705500e97db438 Mon Sep 17 00:00:00 2001 From: Goreorto Date: Fri, 8 May 2026 07:29:26 -0600 Subject: [PATCH 2/2] SetValue --- maputil/maputil.go | 23 ++++++++++++ maputil/maputil_test.go | 77 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/maputil/maputil.go b/maputil/maputil.go index 927c485..3db0e6d 100644 --- a/maputil/maputil.go +++ b/maputil/maputil.go @@ -21,6 +21,29 @@ var ( ErrNotFound = errors.New("key not found") ) +// SetValue set value using key path +func SetValue[T any](m map[string]any, v T, keys ...string) error { + if len(keys) == 1 { + m[keys[0]] = v + return nil + } + + next := m[keys[0]] + var nextMap map[string]any + if next == nil { + nextMap = map[string]any{} + m[keys[0]] = nextMap + } else { + var ok bool + nextMap, ok = next.(map[string]any) + if !ok { + return ErrWrongType + } + } + + return SetValue(nextMap, v, keys[1:]...) +} + // GetValue returns the value defined as a key path func GetValue[T any](m map[string]any, keys ...string) (T, error) { var out T diff --git a/maputil/maputil_test.go b/maputil/maputil_test.go index 58279c4..69ee991 100644 --- a/maputil/maputil_test.go +++ b/maputil/maputil_test.go @@ -107,3 +107,80 @@ func TestGetValue(t *testing.T) { t.Fatalf("error not found expected, got %v", err) } } + +func TestSetValue(t *testing.T) { + m := map[string]any{} + + // simple int set + err := SetValue(m, 10, "a") + if err != nil { + t.Fatal(err) + } + if m["a"] == nil { + t.Fatal("expected key 'a'") + } + if v, ok := m["a"].(int); !ok || v != 10 { + t.Fatalf("expected 10, got %v", v) + } + + // simple string set + str := "mystring" + err = SetValue(m, str, "b") + if err != nil { + t.Fatal(err) + } + if m["b"] == nil { + t.Fatal("expected key 'b'") + } + if v, ok := m["b"].(string); !ok || v != str { + t.Fatalf("expected '%v', got %v", str, v) + } + + // nested set + err = SetValue(m, 2, "c", "d") + if err != nil { + t.Fatal(err) + } + if m["c"] == nil { + t.Fatal("expected key 'b'") + } + nested, ok := m["c"].(map[string]any) + if !ok { + t.Fatalf("expected map, got %T", nested) + } + if nested["d"] == nil { + t.Fatal("expected key 'd'") + } + if v, ok := nested["d"].(int); !ok || v != 2 { + t.Fatalf("expected 2, got %v", v) + } + + // replace + err = SetValue(map[string]any{"a": 10}, 10, "a") + if err != nil { + t.Fatal(err) + } + if m["a"] == nil { + t.Fatal("expected key 'a'") + } + if v, ok := m["a"].(int); !ok || v != 10 { + t.Fatalf("expected 10, got %v", v) + } + + // replace tip + m = map[string]any{"a": map[string]any{"b": 1}} + err = SetValue(m, 10, "a", "b") + if err != nil { + t.Fatal(err) + } + if v, _ := m["a"].(map[string]any)["b"].(int); v != 10 { + t.Fatalf("expected 10, got %v", v) + } + + // crash when mid key is not a map + m = map[string]any{"a": map[string]any{"b": 1}} + err = SetValue(m, 10, "a", "b", "c") + if err != ErrWrongType { + t.Fatal(err) + } +}