From c5f0b5b165a21e8280aeadd0bb554f8c9b848a36 Mon Sep 17 00:00:00 2001 From: Tyson Mote Date: Mon, 21 Jun 2021 21:12:06 -0700 Subject: [PATCH 1/2] Add merge function to templates --- pkg/util/template.go | 64 ++++++++++++++++ pkg/util/template_test.go | 153 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/pkg/util/template.go b/pkg/util/template.go index e9958c5..be55691 100644 --- a/pkg/util/template.go +++ b/pkg/util/template.go @@ -22,6 +22,7 @@ var ( "pathLookup": pathLookup, "toYaml": toYaml, "urlEncode": url.QueryEscape, + "merge": merge, } ) @@ -271,3 +272,66 @@ func toYaml(input interface{}) (string, error) { } return string(bytes), nil } + +// merge recursively merges one or more string-keyed maps into one map. It +// always returns a map[string]interface{} or an error. +func merge(values ...interface{}) (interface{}, error) { + merged := map[string]interface{}{} + var err error + + for i, val := range values { + switch v := val.(type) { + case map[string]interface{}: + merged, err = mergeMap("", merged, v) + case nil: + continue + default: + err = fmt.Errorf("Argument %d: Expected map[string]interface{} or nil, got %s", i, typeLabel(val)) + } + + if err != nil { + return nil, err + } + } + + return merged, nil +} + +func mergeMap(path string, l, r map[string]interface{}) (map[string]interface{}, error) { + var err error + + for k, v := range r { + rMap, ok := v.(map[string]interface{}) + if !ok || l[k] == nil { + l[k] = v + continue + } + + lMap, ok := l[k].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s: Expected map[string]interface{}, got %s", joinPath(path, k), typeLabel(l[k])) + } + + l[k], err = mergeMap(joinPath(path, k), lMap, rMap) + if err != nil { + return nil, err + } + } + + return l, nil +} + +func typeLabel(v interface{}) string { + typ := reflect.TypeOf(v) + if typ == nil { + return "nil" + } + return typ.String() +} + +func joinPath(l, r string) string { + if l == "" { + return r + } + return l + "." + r +} diff --git a/pkg/util/template_test.go b/pkg/util/template_test.go index 302055a..c4eec32 100644 --- a/pkg/util/template_test.go +++ b/pkg/util/template_test.go @@ -221,3 +221,156 @@ func TestLookup(t *testing.T) { } } } + +func TestMerge(t *testing.T) { + tests := []struct { + desc string + values []interface{} + expect interface{} + expectError string + }{ + { + desc: "empty", + values: []interface{}{}, + expect: map[string]interface{}{}, + }, + { + desc: "nil", + values: []interface{}{nil, nil}, + expect: map[string]interface{}{}, + }, + { + desc: "single", + values: []interface{}{map[string]interface{}{"a": 1}}, + expect: map[string]interface{}{"a": 1}, + }, + { + desc: "invalid type", + values: []interface{}{1}, + expectError: "Argument 0: Expected map[string]interface{} or nil, got int", + }, + { + desc: "simple", + values: []interface{}{ + map[string]interface{}{"a": 1, "c": 3}, + map[string]interface{}{"b": 2}, + }, + expect: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + }, + }, + { + desc: "overwrite", + values: []interface{}{ + map[string]interface{}{"a": 1, "c": 3}, + map[string]interface{}{"a": 4, "b": 2}, + }, + expect: map[string]interface{}{ + "a": 4, + "b": 2, + "c": 3, + }, + }, + { + desc: "nested", + values: []interface{}{ + map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": 1, + }, + "d": 2, + }, + }, + map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": 3, + }, + }, + "e": 4, + }, + }, + expect: map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": 3, + }, + "d": 2, + }, + "e": 4, + }, + }, + { + desc: "nested replace", + values: []interface{}{ + map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": 1, + }, + "d": 2, + }, + }, + map[string]interface{}{ + "a": map[string]interface{}{ + "b": 1, + }, + }, + }, + expect: map[string]interface{}{ + "a": map[string]interface{}{ + "b": 1, + "d": 2, + }, + }, + }, + { + desc: "type error", + values: []interface{}{ + map[string]interface{}{ + "a": 1, + }, + map[string]interface{}{ + "a": map[string]interface{}{ + "b": 2, + }, + }, + }, + expectError: "a: Expected map[string]interface{}, got int", + }, + { + desc: "nested type error", + values: []interface{}{ + map[string]interface{}{ + "a": map[string]interface{}{ + "b": 1, + }, + }, + map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": 2, + }, + }, + }, + }, + expectError: "a.b: Expected map[string]interface{}, got int", + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + got, gotErr := merge(test.values...) + if test.expectError != "" { + require.Error(t, gotErr) + assert.Equal(t, test.expectError, gotErr.Error()) + } else { + require.NoError(t, gotErr) + assert.Equal(t, test.expect, got) + } + }) + } +} From 7f878adfa6c9d492c6bbab52aba0bc2b481a3772 Mon Sep 17 00:00:00 2001 From: Tyson Mote Date: Mon, 21 Jun 2021 21:53:29 -0700 Subject: [PATCH 2/2] Use type switch to make intent more clear --- pkg/util/template.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pkg/util/template.go b/pkg/util/template.go index be55691..87f0c0f 100644 --- a/pkg/util/template.go +++ b/pkg/util/template.go @@ -300,21 +300,25 @@ func merge(values ...interface{}) (interface{}, error) { func mergeMap(path string, l, r map[string]interface{}) (map[string]interface{}, error) { var err error - for k, v := range r { - rMap, ok := v.(map[string]interface{}) - if !ok || l[k] == nil { - l[k] = v + for k, val := range r { + if l[k] == nil { + l[k] = val continue } - lMap, ok := l[k].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%s: Expected map[string]interface{}, got %s", joinPath(path, k), typeLabel(l[k])) - } + switch v := val.(type) { + case map[string]interface{}: + lMap, ok := l[k].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s: Expected map[string]interface{}, got %s", joinPath(path, k), typeLabel(l[k])) + } - l[k], err = mergeMap(joinPath(path, k), lMap, rMap) - if err != nil { - return nil, err + l[k], err = mergeMap(joinPath(path, k), lMap, v) + if err != nil { + return nil, err + } + default: + l[k] = val } }