Merge pull request #24032 from hashicorp/jbardin/map-funcs

make the merge function more precise
This commit is contained in:
James Bardin 2020-02-12 10:51:05 -05:00 committed by GitHub
commit a765d69fb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 241 additions and 22 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 // MergeFunc constructs a function that takes an arbitrary number of maps or objects, and
// returns a single map that contains a merged set of elements from all of the maps. // 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 // If more than one given map or object defines the same key then the one that
// the argument sequence takes precedence. // is later in the argument sequence takes precedence.
var MergeFunc = function.New(&function.Spec{ var MergeFunc = function.New(&function.Spec{
Params: []function.Parameter{}, Params: []function.Parameter{},
VarParam: &function.Parameter{ VarParam: &function.Parameter{
Name: "maps", Name: "maps",
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
AllowDynamicType: true, 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) { Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
outputMap := make(map[string]cty.Value) 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 { for _, arg := range args {
if !arg.IsWhollyKnown() { if arg.IsNull() {
return cty.UnknownVal(retType), nil continue
} } else {
if !arg.Type().IsObjectType() && !arg.Type().IsMapType() { allNull = false
return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type().FriendlyName())
} }
for it := arg.ElementIterator(); it.Next(); { for it := arg.ElementIterator(); it.Next(); {
k, v := it.Element() k, v := it.Element()
outputMap[k.AsString()] = v 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 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"), "c": cty.StringVal("d"),
}), }),
}, },
cty.ObjectVal(map[string]cty.Value{ cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("b"), "a": cty.StringVal("b"),
"c": cty.StringVal("d"), "c": cty.StringVal("d"),
}), }),
@ -2094,6 +2094,65 @@ func TestMerge(t *testing.T) {
"c": cty.StringVal("d"), "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.NullVal(cty.EmptyObject),
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, cty.DynamicVal,
false, false,
}, },
@ -2107,7 +2166,7 @@ func TestMerge(t *testing.T) {
"a": cty.StringVal("x"), "a": cty.StringVal("x"),
}), }),
}, },
cty.ObjectVal(map[string]cty.Value{ cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("x"), "a": cty.StringVal("x"),
"c": cty.StringVal("d"), "c": cty.StringVal("d"),
}), }),
@ -2151,7 +2210,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{ "a": cty.MapVal(map[string]cty.Value{
"b": cty.StringVal("c"), "b": cty.StringVal("c"),
}), }),
@ -2176,7 +2235,7 @@ func TestMerge(t *testing.T) {
}), }),
}), }),
}, },
cty.ObjectVal(map[string]cty.Value{ cty.MapVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{ "a": cty.ListVal([]cty.Value{
cty.StringVal("b"), cty.StringVal("b"),
cty.StringVal("c"), cty.StringVal("c"),
@ -2213,6 +2272,66 @@ func TestMerge(t *testing.T) {
}), }),
false, 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 { // argument error: non map type
[]cty.Value{ []cty.Value{
cty.MapVal(map[string]cty.Value{ cty.MapVal(map[string]cty.Value{

View File

@ -3,8 +3,9 @@ layout: "functions"
page_title: "merge - Functions - Configuration Language" page_title: "merge - Functions - Configuration Language"
sidebar_current: "docs-funcs-collection-merge" sidebar_current: "docs-funcs-collection-merge"
description: |- description: |-
The merge function takes an arbitrary number of maps and returns a single The merge function takes an arbitrary number maps or objects, and returns a
map after merging the keys from each argument. single map or object that contains a merged set of elements from all
arguments.
--- ---
# `merge` Function # `merge` Function
@ -13,19 +14,33 @@ description: |-
earlier, see earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). [0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
`merge` takes an arbitrary number of maps and returns a single map that `merge` takes an arbitrary number of maps or objects, and returns a single map
contains a merged set of elements from all of the maps. pr object that contains a merged set of elements from all arguments.
If more than one given map defines the same key then the one that is later If more than one given map or object defines the same key or attribute, then
in the argument sequence takes precedence. the one that is later in the argument sequence takes precedence. If the
argument types do not match, the resulting type will be an object matching the
type structure of the attributes after the merging rules have been applied.
## Examples ## Examples
``` ```
> merge({"a"="b", "c"="d"}, {"e"="f", "c"="z"}) > merge({a="b", c="d"}, {e="f", c="z"})
{ {
"a" = "b" "a" = "b"
"c" = "z" "c" = "z"
"e" = "f" "e" = "f"
} }
``` ```
```
> merge({a="b"}, {a=[1,2], c="z"}, {d=3})
{
"a" = [
1,
2,
]
"c" = "z"
"d" = 3
}
```