Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions pkg/util/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (
"pathLookup": pathLookup,
"toYaml": toYaml,
"urlEncode": url.QueryEscape,
"merge": merge,
}
)

Expand Down Expand Up @@ -271,3 +272,70 @@ 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, val := range r {
if l[k] == nil {
l[k] = val
continue
}

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, v)
if err != nil {
return nil, err
}
default:
l[k] = val
}
}

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
}
153 changes: 153 additions & 0 deletions pkg/util/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}