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
// 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,65 @@ 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.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,
false,
},
@ -2107,7 +2166,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 +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{
"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{
cty.StringVal("b"),
cty.StringVal("c"),
@ -2213,6 +2272,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{

View File

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