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:
parent
c121d1a927
commit
f5bf9aa55d
|
@ -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))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue