Skip to content

Commit 91bfd93

Browse files
authored
Merge pull request #42 from segmentio/merge
Add merge function to templates
2 parents 696f462 + 7f878ad commit 91bfd93

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed

pkg/util/template.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var (
2222
"pathLookup": pathLookup,
2323
"toYaml": toYaml,
2424
"urlEncode": url.QueryEscape,
25+
"merge": merge,
2526
}
2627
)
2728

@@ -271,3 +272,70 @@ func toYaml(input interface{}) (string, error) {
271272
}
272273
return string(bytes), nil
273274
}
275+
276+
// merge recursively merges one or more string-keyed maps into one map. It
277+
// always returns a map[string]interface{} or an error.
278+
func merge(values ...interface{}) (interface{}, error) {
279+
merged := map[string]interface{}{}
280+
var err error
281+
282+
for i, val := range values {
283+
switch v := val.(type) {
284+
case map[string]interface{}:
285+
merged, err = mergeMap("", merged, v)
286+
case nil:
287+
continue
288+
default:
289+
err = fmt.Errorf("Argument %d: Expected map[string]interface{} or nil, got %s", i, typeLabel(val))
290+
}
291+
292+
if err != nil {
293+
return nil, err
294+
}
295+
}
296+
297+
return merged, nil
298+
}
299+
300+
func mergeMap(path string, l, r map[string]interface{}) (map[string]interface{}, error) {
301+
var err error
302+
303+
for k, val := range r {
304+
if l[k] == nil {
305+
l[k] = val
306+
continue
307+
}
308+
309+
switch v := val.(type) {
310+
case map[string]interface{}:
311+
lMap, ok := l[k].(map[string]interface{})
312+
if !ok {
313+
return nil, fmt.Errorf("%s: Expected map[string]interface{}, got %s", joinPath(path, k), typeLabel(l[k]))
314+
}
315+
316+
l[k], err = mergeMap(joinPath(path, k), lMap, v)
317+
if err != nil {
318+
return nil, err
319+
}
320+
default:
321+
l[k] = val
322+
}
323+
}
324+
325+
return l, nil
326+
}
327+
328+
func typeLabel(v interface{}) string {
329+
typ := reflect.TypeOf(v)
330+
if typ == nil {
331+
return "nil"
332+
}
333+
return typ.String()
334+
}
335+
336+
func joinPath(l, r string) string {
337+
if l == "" {
338+
return r
339+
}
340+
return l + "." + r
341+
}

pkg/util/template_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,156 @@ func TestLookup(t *testing.T) {
221221
}
222222
}
223223
}
224+
225+
func TestMerge(t *testing.T) {
226+
tests := []struct {
227+
desc string
228+
values []interface{}
229+
expect interface{}
230+
expectError string
231+
}{
232+
{
233+
desc: "empty",
234+
values: []interface{}{},
235+
expect: map[string]interface{}{},
236+
},
237+
{
238+
desc: "nil",
239+
values: []interface{}{nil, nil},
240+
expect: map[string]interface{}{},
241+
},
242+
{
243+
desc: "single",
244+
values: []interface{}{map[string]interface{}{"a": 1}},
245+
expect: map[string]interface{}{"a": 1},
246+
},
247+
{
248+
desc: "invalid type",
249+
values: []interface{}{1},
250+
expectError: "Argument 0: Expected map[string]interface{} or nil, got int",
251+
},
252+
{
253+
desc: "simple",
254+
values: []interface{}{
255+
map[string]interface{}{"a": 1, "c": 3},
256+
map[string]interface{}{"b": 2},
257+
},
258+
expect: map[string]interface{}{
259+
"a": 1,
260+
"b": 2,
261+
"c": 3,
262+
},
263+
},
264+
{
265+
desc: "overwrite",
266+
values: []interface{}{
267+
map[string]interface{}{"a": 1, "c": 3},
268+
map[string]interface{}{"a": 4, "b": 2},
269+
},
270+
expect: map[string]interface{}{
271+
"a": 4,
272+
"b": 2,
273+
"c": 3,
274+
},
275+
},
276+
{
277+
desc: "nested",
278+
values: []interface{}{
279+
map[string]interface{}{
280+
"a": map[string]interface{}{
281+
"b": map[string]interface{}{
282+
"c": 1,
283+
},
284+
"d": 2,
285+
},
286+
},
287+
map[string]interface{}{
288+
"a": map[string]interface{}{
289+
"b": map[string]interface{}{
290+
"c": 3,
291+
},
292+
},
293+
"e": 4,
294+
},
295+
},
296+
expect: map[string]interface{}{
297+
"a": map[string]interface{}{
298+
"b": map[string]interface{}{
299+
"c": 3,
300+
},
301+
"d": 2,
302+
},
303+
"e": 4,
304+
},
305+
},
306+
{
307+
desc: "nested replace",
308+
values: []interface{}{
309+
map[string]interface{}{
310+
"a": map[string]interface{}{
311+
"b": map[string]interface{}{
312+
"c": 1,
313+
},
314+
"d": 2,
315+
},
316+
},
317+
map[string]interface{}{
318+
"a": map[string]interface{}{
319+
"b": 1,
320+
},
321+
},
322+
},
323+
expect: map[string]interface{}{
324+
"a": map[string]interface{}{
325+
"b": 1,
326+
"d": 2,
327+
},
328+
},
329+
},
330+
{
331+
desc: "type error",
332+
values: []interface{}{
333+
map[string]interface{}{
334+
"a": 1,
335+
},
336+
map[string]interface{}{
337+
"a": map[string]interface{}{
338+
"b": 2,
339+
},
340+
},
341+
},
342+
expectError: "a: Expected map[string]interface{}, got int",
343+
},
344+
{
345+
desc: "nested type error",
346+
values: []interface{}{
347+
map[string]interface{}{
348+
"a": map[string]interface{}{
349+
"b": 1,
350+
},
351+
},
352+
map[string]interface{}{
353+
"a": map[string]interface{}{
354+
"b": map[string]interface{}{
355+
"c": 2,
356+
},
357+
},
358+
},
359+
},
360+
expectError: "a.b: Expected map[string]interface{}, got int",
361+
},
362+
}
363+
364+
for _, test := range tests {
365+
t.Run(test.desc, func(t *testing.T) {
366+
got, gotErr := merge(test.values...)
367+
if test.expectError != "" {
368+
require.Error(t, gotErr)
369+
assert.Equal(t, test.expectError, gotErr.Error())
370+
} else {
371+
require.NoError(t, gotErr)
372+
assert.Equal(t, test.expect, got)
373+
}
374+
})
375+
}
376+
}

0 commit comments

Comments
 (0)