make the merge function more precise

This PR implements 2 changes to the merge function.
 - Rather than always defining the merge return type as dynamic, return
 a precise type when all argument types match, or all possible object
 attributes are known.
 - Always return a value containing all keys when the keys are known.
 This allows the use of merge output in for_each, even when keys are yet
 to be determined.
This commit is contained in:
James Bardin 2020-02-04 14:40:00 -05:00
parent c121d1a927
commit f5bf9aa55d
2 changed files with 221 additions and 15 deletions

View File

@ -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
}
}
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))
}
},
})

View File

@ -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{