diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index 48410e0ee..2af8d1acb 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -634,6 +634,38 @@ var MatchkeysFunc = function.New(&function.Spec{ }, }) +// MergeFunc contructs a function that takes an arbitrary number of maps and +// returns a single map that contains a merged set of elements from all of the maps. +// +// If more than one given map defines the same key then the one that is later in +// the argument sequence takes precedence. +var MergeFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "maps", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: function.StaticReturnType(cty.DynamicPseudoType), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + outputMap := make(map[string]cty.Value) + + for _, arg := range args { + + if !arg.Type().IsObjectType() && !arg.Type().IsMapType() { + return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type()) + } + for it := arg.ElementIterator(); it.Next(); { + k, v := it.Element() + outputMap[k.AsString()] = v + } + } + return cty.ObjectVal(outputMap), nil + }, +}) + // helper function to add an element to a list, if it does not already exist func appendIfMissing(slice []cty.Value, element cty.Value) ([]cty.Value, error) { for _, ele := range slice { @@ -728,3 +760,12 @@ func Map(args ...cty.Value) (cty.Value, error) { func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) { return MatchkeysFunc.Call([]cty.Value{values, keys, searchset}) } + +// Merge takes an arbitrary number of maps and returns a single map that contains +// a merged set of elements from all of the maps. +// +// If more than one given map defines the same key then the one that is later in +// the argument sequence takes precedence. +func Merge(maps ...cty.Value) (cty.Value, error) { + return MergeFunc.Call(maps) +} diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 46d0fd096..0c63c632d 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -1365,3 +1365,167 @@ func TestMatchkeys(t *testing.T) { }) } } + +func TestMerge(t *testing.T) { + tests := []struct { + Values []cty.Value + Want cty.Value + Err bool + }{ + { + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + }), + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "c": cty.StringVal("d"), + }), + false, + }, + { // merge with conflicts is ok, last in wins + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "c": cty.StringVal("d"), + }), + cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("x"), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("x"), + "c": cty.StringVal("d"), + }), + false, + }, + { // only accept maps + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "c": cty.StringVal("d"), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("x"), + }), + }, + cty.NilVal, + true, + }, + { // merge maps of maps + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.MapVal(map[string]cty.Value{ + "b": cty.StringVal("c"), + }), + }), + cty.MapVal(map[string]cty.Value{ + "d": cty.MapVal(map[string]cty.Value{ + "e": cty.StringVal("f"), + }), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.MapVal(map[string]cty.Value{ + "b": cty.StringVal("c"), + }), + "d": cty.MapVal(map[string]cty.Value{ + "e": cty.StringVal("f"), + }), + }), + false, + }, + { // map of lists + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + cty.StringVal("c"), + }), + }), + cty.MapVal(map[string]cty.Value{ + "d": cty.ListVal([]cty.Value{ + cty.StringVal("e"), + cty.StringVal("f"), + }), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + cty.StringVal("c"), + }), + "d": cty.ListVal([]cty.Value{ + cty.StringVal("e"), + cty.StringVal("f"), + }), + }), + false, + }, + { // merge map of various kinds + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + cty.StringVal("c"), + }), + }), + cty.MapVal(map[string]cty.Value{ + "d": cty.MapVal(map[string]cty.Value{ + "e": cty.StringVal("f"), + }), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + cty.StringVal("c"), + }), + "d": cty.MapVal(map[string]cty.Value{ + "e": cty.StringVal("f"), + }), + }), + false, + }, + { // argument error: non map type + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + cty.StringVal("c"), + }), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("d"), + cty.StringVal("e"), + }), + }, + cty.NilVal, + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("merge(%#v)", test.Values), func(t *testing.T) { + got, err := Merge(test.Values...) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index e9e2fcb0c..8ba4719e4 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -73,7 +73,7 @@ func (s *Scope) Functions() map[string]function.Function { "matchkeys": funcs.MatchkeysFunc, "max": stdlib.MaxFunc, "md5": funcs.Md5Func, - "merge": unimplFunc, // TODO + "merge": funcs.MergeFunc, "min": stdlib.MinFunc, "pathexpand": funcs.PathExpandFunc, "pow": funcs.PowFunc,