Merge pull request #24032 from hashicorp/jbardin/map-funcs
make the merge function more precise
This commit is contained in:
commit
a765d69fb0
|
@ -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))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
Loading…
Reference in New Issue