From 5fc0d642ac38742f5eaddd3a4c6916fe23dfb563 Mon Sep 17 00:00:00 2001 From: Naveen <172697+naveensrinivasan@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:11:13 -0500 Subject: [PATCH] feat: add MergePathAndValueIntoMap (#49) ## Description - included a helper for generating a nested map. ## Related Issue https://github.com/defenseunicorns/zarf/pull/2403#discussion_r1543576325 --------- Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: Lucas Rodriguez Co-authored-by: Lucas Rodriguez --- helpers/misc.go | 26 +++++++++++++ helpers/misc_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/helpers/misc.go b/helpers/misc.go index 16a02e91..446f1d90 100644 --- a/helpers/misc.go +++ b/helpers/misc.go @@ -8,6 +8,7 @@ import ( "math" "reflect" "regexp" + "strings" "time" ) @@ -146,3 +147,28 @@ func MergeNonZero[T any](original T, overrides T) T { } return originalValue.Elem().Interface().(T) } + +// MergePathAndValueIntoMap takes a path in dot notation as a string and a value (also as a string for simplicity), +// then merges this into the provided map. The value can be any type. +func MergePathAndValueIntoMap(m map[string]any, path string, value any) error { + pathParts := strings.Split(path, ".") + current := m + for i, part := range pathParts { + if i == len(pathParts)-1 { + // Set the value at the last key in the path. + current[part] = value + } else { + if _, exists := current[part]; !exists { + // If the part does not exist, create a new map for it. + current[part] = make(map[string]any) + } + + nextMap, ok := current[part].(map[string]any) + if !ok { + return fmt.Errorf("conflict at %q, expected map but got %T", strings.Join(pathParts[:i+1], "."), current[part]) + } + current = nextMap + } + } + return nil +} diff --git a/helpers/misc_test.go b/helpers/misc_test.go index 44019ae8..0be62db8 100644 --- a/helpers/misc_test.go +++ b/helpers/misc_test.go @@ -5,6 +5,7 @@ package helpers import ( "errors" + "reflect" "strings" "testing" @@ -199,3 +200,89 @@ func (suite *TestMiscSuite) TestBoolPtr() { func TestMisc(t *testing.T) { suite.Run(t, new(TestMiscSuite)) } + +func (suite *TestMiscSuite) TestMergePathAndValueIntoMap() { + type args struct { + m map[string]interface{} + path string + value interface{} + } + tests := []struct { + name string + args args + wantErr bool + want map[string]any + }{ + { + name: "nested map creation", + args: args{m: make(map[string]interface{}), path: "a.b.c", value: "hello"}, + wantErr: false, + want: map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c": "hello", + }, + }, + }, + }, + { + name: "overwrite existing value", + args: args{m: map[string]interface{}{"a": map[string]any{"b": map[string]any{"c": "initial"}}}, + path: "a.b.c", value: "updated"}, + wantErr: false, + want: map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c": "updated", + }, + }, + }, + }, + { + name: "deeply nested map creation", + args: args{m: make(map[string]interface{}), path: "a.b.c.d.e.f", value: 42}, + wantErr: false, + want: map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c": map[string]any{ + "d": map[string]any{ + "e": map[string]any{ + "f": 42, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty path", + args: args{m: make(map[string]interface{}), path: "", value: "root level"}, + wantErr: false, + want: map[string]any{ + "": "root level", + }, + }, + { + name: "root level value", + args: args{m: make(map[string]interface{}), path: "root", value: "root value"}, + wantErr: false, + want: map[string]any{ + "root": "root value", + }, + }, + } + for _, tt := range tests { + suite.Run(tt.name, func() { + err := MergePathAndValueIntoMap(tt.args.m, tt.args.path, tt.args.value) + if tt.wantErr { + suite.Error(err, "Expected an error") + } else { + suite.NoError(err, "Expected no error") + } + + suite.True(reflect.DeepEqual(tt.args.m, tt.want), "Map structure mismatch: got %v, want %v", tt.args.m, tt.want) + }) + } +}