mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return cty.ObjectVal(outputMap), nil
|
||||
|
||||
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,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{
|
||||
|
@ -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
|
||||
}
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user