diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index 2ea4168757..21ba0abb2c 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -865,35 +865,120 @@ var MatchkeysFunc = function.New(&function.Spec{ }, }) -// MergeFunc constructs 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. +// MergeFunc constructs a function that takes an arbitrary number of maps or objects, and +// returns a single value that contains a merged set of keys and values from +// all of the inputs. // -// If more than one given map defines the same key then the one that is later in -// the argument sequence takes precedence. +// If more than one given map or object 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, AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (cty.Type, error) { + // empty args is accepted, so assume an empty object since we have no + // key-value types. + if len(args) == 0 { + return cty.EmptyObject, nil + } + + // collect the possible object attrs + attrs := map[string]cty.Type{} + + first := cty.NilType + matching := true + attrsKnown := true + for i, arg := range args { + ty := arg.Type() + // any dynamic args mean we can't compute a type + if ty.Equals(cty.DynamicPseudoType) { + return cty.DynamicPseudoType, nil + } + + // check for invalid arguments + if !ty.IsMapType() && !ty.IsObjectType() { + return cty.NilType, fmt.Errorf("arguments must be maps or objects, got %#v", ty.FriendlyName()) + } + + switch { + case ty.IsObjectType() && !arg.IsNull(): + for attr, aty := range ty.AttributeTypes() { + attrs[attr] = aty + } + case ty.IsMapType(): + switch { + case arg.IsNull(): + // pass, nothing to add + case arg.IsKnown(): + ety := arg.Type().ElementType() + for it := arg.ElementIterator(); it.Next(); { + attr, _ := it.Element() + attrs[attr.AsString()] = ety + } + default: + // any unknown maps means we don't know all possible attrs + // for the return type + attrsKnown = false + } + } + + // record the first argument type for comparison + if i == 0 { + first = arg.Type() + continue + } + + if !ty.Equals(first) && matching { + matching = false + } + } + + // the types all match, so use the first argument type + if matching { + return first, nil + } + + // We had a mix of unknown maps and objects, so we can't predict the + // attributes + if !attrsKnown { + return cty.DynamicPseudoType, nil + } + + return cty.Object(attrs), nil }, - Type: function.StaticReturnType(cty.DynamicPseudoType), Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { outputMap := make(map[string]cty.Value) + // if all inputs are null, return a null value rather than an object + // with null attributes + allNull := true for _, arg := range args { - if !arg.IsWhollyKnown() { - return cty.UnknownVal(retType), nil - } - if !arg.Type().IsObjectType() && !arg.Type().IsMapType() { - return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type().FriendlyName()) + if arg.IsNull() { + continue + } else { + allNull = false } + for it := arg.ElementIterator(); it.Next(); { k, v := it.Element() outputMap[k.AsString()] = v } } - return cty.ObjectVal(outputMap), nil + + switch { + case allNull: + return cty.NullVal(retType), nil + case retType.IsMapType(): + return cty.MapVal(outputMap), nil + case retType.IsObjectType(), retType.Equals(cty.DynamicPseudoType): + return cty.ObjectVal(outputMap), nil + default: + panic(fmt.Sprintf("unexpected return type: %#v", retType)) + } }, }) diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 58fa4beffc..552cccfb7d 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -2079,7 +2079,7 @@ func TestMerge(t *testing.T) { "c": cty.StringVal("d"), }), }, - cty.ObjectVal(map[string]cty.Value{ + cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("b"), "c": cty.StringVal("d"), }), @@ -2094,6 +2094,67 @@ func TestMerge(t *testing.T) { "c": cty.StringVal("d"), }), }, + cty.MapVal(map[string]cty.Value{ + "a": cty.UnknownVal(cty.String), + "c": cty.StringVal("d"), + }), + false, + }, + { // handle null map + []cty.Value{ + cty.NullVal(cty.Map(cty.String)), + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + }, + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + false, + }, + { // handle null map + []cty.Value{ + cty.NullVal(cty.Map(cty.String)), + cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.List(cty.String), + })), + }, + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + false, + }, + { // handle null object + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.List(cty.String), + })), + }, + cty.ObjectVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + false, + }, + { // handle unknowns + []cty.Value{ + cty.UnknownVal(cty.Map(cty.String)), + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + }, + cty.UnknownVal(cty.Map(cty.String)), + false, + }, + { // handle dynamic unknown + []cty.Value{ + cty.UnknownVal(cty.DynamicPseudoType), + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + }, cty.DynamicVal, false, }, @@ -2107,7 +2168,7 @@ func TestMerge(t *testing.T) { "a": cty.StringVal("x"), }), }, - cty.ObjectVal(map[string]cty.Value{ + cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("x"), "c": cty.StringVal("d"), }), @@ -2151,7 +2212,7 @@ func TestMerge(t *testing.T) { }), }), }, - cty.ObjectVal(map[string]cty.Value{ + cty.MapVal(map[string]cty.Value{ "a": cty.MapVal(map[string]cty.Value{ "b": cty.StringVal("c"), }), @@ -2176,7 +2237,7 @@ func TestMerge(t *testing.T) { }), }), }, - cty.ObjectVal(map[string]cty.Value{ + cty.MapVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.StringVal("b"), cty.StringVal("c"), @@ -2213,6 +2274,66 @@ func TestMerge(t *testing.T) { }), false, }, + { // merge objects of various shapes + []cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "d": cty.DynamicVal, + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + }), + "d": cty.DynamicVal, + }), + false, + }, + { // merge maps and objects + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "d": cty.NumberIntVal(2), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + }), + "d": cty.NumberIntVal(2), + }), + false, + }, + { // attr a type and value is overridden + []cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.StringVal("b"), + }), + "b": cty.StringVal("b"), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "e": cty.StringVal("f"), + }), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "e": cty.StringVal("f"), + }), + "b": cty.StringVal("b"), + }), + false, + }, { // argument error: non map type []cty.Value{ cty.MapVal(map[string]cty.Value{