Skip to content

Commit a765d69

Browse files
authored
Merge pull request #24032 from hashicorp/jbardin/map-funcs
make the merge function more precise
2 parents 378b651 + 529271e commit a765d69

File tree

3 files changed

+241
-22
lines changed

3 files changed

+241
-22
lines changed

lang/funcs/collection.go

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -865,35 +865,120 @@ var MatchkeysFunc = function.New(&function.Spec{
865865
},
866866
})
867867

868-
// MergeFunc constructs a function that takes an arbitrary number of maps and
869-
// returns a single map that contains a merged set of elements from all of the maps.
868+
// MergeFunc constructs a function that takes an arbitrary number of maps or objects, and
869+
// returns a single value that contains a merged set of keys and values from
870+
// all of the inputs.
870871
//
871-
// If more than one given map defines the same key then the one that is later in
872-
// the argument sequence takes precedence.
872+
// If more than one given map or object defines the same key then the one that
873+
// is later in the argument sequence takes precedence.
873874
var MergeFunc = function.New(&function.Spec{
874875
Params: []function.Parameter{},
875876
VarParam: &function.Parameter{
876877
Name: "maps",
877878
Type: cty.DynamicPseudoType,
878879
AllowDynamicType: true,
880+
AllowNull: true,
881+
},
882+
Type: func(args []cty.Value) (cty.Type, error) {
883+
// empty args is accepted, so assume an empty object since we have no
884+
// key-value types.
885+
if len(args) == 0 {
886+
return cty.EmptyObject, nil
887+
}
888+
889+
// collect the possible object attrs
890+
attrs := map[string]cty.Type{}
891+
892+
first := cty.NilType
893+
matching := true
894+
attrsKnown := true
895+
for i, arg := range args {
896+
ty := arg.Type()
897+
// any dynamic args mean we can't compute a type
898+
if ty.Equals(cty.DynamicPseudoType) {
899+
return cty.DynamicPseudoType, nil
900+
}
901+
902+
// check for invalid arguments
903+
if !ty.IsMapType() && !ty.IsObjectType() {
904+
return cty.NilType, fmt.Errorf("arguments must be maps or objects, got %#v", ty.FriendlyName())
905+
}
906+
907+
switch {
908+
case ty.IsObjectType() && !arg.IsNull():
909+
for attr, aty := range ty.AttributeTypes() {
910+
attrs[attr] = aty
911+
}
912+
case ty.IsMapType():
913+
switch {
914+
case arg.IsNull():
915+
// pass, nothing to add
916+
case arg.IsKnown():
917+
ety := arg.Type().ElementType()
918+
for it := arg.ElementIterator(); it.Next(); {
919+
attr, _ := it.Element()
920+
attrs[attr.AsString()] = ety
921+
}
922+
default:
923+
// any unknown maps means we don't know all possible attrs
924+
// for the return type
925+
attrsKnown = false
926+
}
927+
}
928+
929+
// record the first argument type for comparison
930+
if i == 0 {
931+
first = arg.Type()
932+
continue
933+
}
934+
935+
if !ty.Equals(first) && matching {
936+
matching = false
937+
}
938+
}
939+
940+
// the types all match, so use the first argument type
941+
if matching {
942+
return first, nil
943+
}
944+
945+
// We had a mix of unknown maps and objects, so we can't predict the
946+
// attributes
947+
if !attrsKnown {
948+
return cty.DynamicPseudoType, nil
949+
}
950+
951+
return cty.Object(attrs), nil
879952
},
880-
Type: function.StaticReturnType(cty.DynamicPseudoType),
881953
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
882954
outputMap := make(map[string]cty.Value)
883955

956+
// if all inputs are null, return a null value rather than an object
957+
// with null attributes
958+
allNull := true
884959
for _, arg := range args {
885-
if !arg.IsWhollyKnown() {
886-
return cty.UnknownVal(retType), nil
887-
}
888-
if !arg.Type().IsObjectType() && !arg.Type().IsMapType() {
889-
return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type().FriendlyName())
960+
if arg.IsNull() {
961+
continue
962+
} else {
963+
allNull = false
890964
}
965+
891966
for it := arg.ElementIterator(); it.Next(); {
892967
k, v := it.Element()
893968
outputMap[k.AsString()] = v
894969
}
895970
}
896-
return cty.ObjectVal(outputMap), nil
971+
972+
switch {
973+
case allNull:
974+
return cty.NullVal(retType), nil
975+
case retType.IsMapType():
976+
return cty.MapVal(outputMap), nil
977+
case retType.IsObjectType(), retType.Equals(cty.DynamicPseudoType):
978+
return cty.ObjectVal(outputMap), nil
979+
default:
980+
panic(fmt.Sprintf("unexpected return type: %#v", retType))
981+
}
897982
},
898983
})
899984

lang/funcs/collection_test.go

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,7 +2079,7 @@ func TestMerge(t *testing.T) {
20792079
"c": cty.StringVal("d"),
20802080
}),
20812081
},
2082-
cty.ObjectVal(map[string]cty.Value{
2082+
cty.MapVal(map[string]cty.Value{
20832083
"a": cty.StringVal("b"),
20842084
"c": cty.StringVal("d"),
20852085
}),
@@ -2094,6 +2094,65 @@ func TestMerge(t *testing.T) {
20942094
"c": cty.StringVal("d"),
20952095
}),
20962096
},
2097+
cty.MapVal(map[string]cty.Value{
2098+
"a": cty.UnknownVal(cty.String),
2099+
"c": cty.StringVal("d"),
2100+
}),
2101+
false,
2102+
},
2103+
{ // handle null map
2104+
[]cty.Value{
2105+
cty.NullVal(cty.Map(cty.String)),
2106+
cty.MapVal(map[string]cty.Value{
2107+
"c": cty.StringVal("d"),
2108+
}),
2109+
},
2110+
cty.MapVal(map[string]cty.Value{
2111+
"c": cty.StringVal("d"),
2112+
}),
2113+
false,
2114+
},
2115+
{ // handle null map
2116+
[]cty.Value{
2117+
cty.NullVal(cty.Map(cty.String)),
2118+
cty.NullVal(cty.Object(map[string]cty.Type{
2119+
"a": cty.List(cty.String),
2120+
})),
2121+
},
2122+
cty.NullVal(cty.EmptyObject),
2123+
false,
2124+
},
2125+
{ // handle null object
2126+
[]cty.Value{
2127+
cty.MapVal(map[string]cty.Value{
2128+
"c": cty.StringVal("d"),
2129+
}),
2130+
cty.NullVal(cty.Object(map[string]cty.Type{
2131+
"a": cty.List(cty.String),
2132+
})),
2133+
},
2134+
cty.ObjectVal(map[string]cty.Value{
2135+
"c": cty.StringVal("d"),
2136+
}),
2137+
false,
2138+
},
2139+
{ // handle unknowns
2140+
[]cty.Value{
2141+
cty.UnknownVal(cty.Map(cty.String)),
2142+
cty.MapVal(map[string]cty.Value{
2143+
"c": cty.StringVal("d"),
2144+
}),
2145+
},
2146+
cty.UnknownVal(cty.Map(cty.String)),
2147+
false,
2148+
},
2149+
{ // handle dynamic unknown
2150+
[]cty.Value{
2151+
cty.UnknownVal(cty.DynamicPseudoType),
2152+
cty.MapVal(map[string]cty.Value{
2153+
"c": cty.StringVal("d"),
2154+
}),
2155+
},
20972156
cty.DynamicVal,
20982157
false,
20992158
},
@@ -2107,7 +2166,7 @@ func TestMerge(t *testing.T) {
21072166
"a": cty.StringVal("x"),
21082167
}),
21092168
},
2110-
cty.ObjectVal(map[string]cty.Value{
2169+
cty.MapVal(map[string]cty.Value{
21112170
"a": cty.StringVal("x"),
21122171
"c": cty.StringVal("d"),
21132172
}),
@@ -2151,7 +2210,7 @@ func TestMerge(t *testing.T) {
21512210
}),
21522211
}),
21532212
},
2154-
cty.ObjectVal(map[string]cty.Value{
2213+
cty.MapVal(map[string]cty.Value{
21552214
"a": cty.MapVal(map[string]cty.Value{
21562215
"b": cty.StringVal("c"),
21572216
}),
@@ -2176,7 +2235,7 @@ func TestMerge(t *testing.T) {
21762235
}),
21772236
}),
21782237
},
2179-
cty.ObjectVal(map[string]cty.Value{
2238+
cty.MapVal(map[string]cty.Value{
21802239
"a": cty.ListVal([]cty.Value{
21812240
cty.StringVal("b"),
21822241
cty.StringVal("c"),
@@ -2213,6 +2272,66 @@ func TestMerge(t *testing.T) {
22132272
}),
22142273
false,
22152274
},
2275+
{ // merge objects of various shapes
2276+
[]cty.Value{
2277+
cty.ObjectVal(map[string]cty.Value{
2278+
"a": cty.ListVal([]cty.Value{
2279+
cty.StringVal("b"),
2280+
}),
2281+
}),
2282+
cty.ObjectVal(map[string]cty.Value{
2283+
"d": cty.DynamicVal,
2284+
}),
2285+
},
2286+
cty.ObjectVal(map[string]cty.Value{
2287+
"a": cty.ListVal([]cty.Value{
2288+
cty.StringVal("b"),
2289+
}),
2290+
"d": cty.DynamicVal,
2291+
}),
2292+
false,
2293+
},
2294+
{ // merge maps and objects
2295+
[]cty.Value{
2296+
cty.MapVal(map[string]cty.Value{
2297+
"a": cty.ListVal([]cty.Value{
2298+
cty.StringVal("b"),
2299+
}),
2300+
}),
2301+
cty.ObjectVal(map[string]cty.Value{
2302+
"d": cty.NumberIntVal(2),
2303+
}),
2304+
},
2305+
cty.ObjectVal(map[string]cty.Value{
2306+
"a": cty.ListVal([]cty.Value{
2307+
cty.StringVal("b"),
2308+
}),
2309+
"d": cty.NumberIntVal(2),
2310+
}),
2311+
false,
2312+
},
2313+
{ // attr a type and value is overridden
2314+
[]cty.Value{
2315+
cty.ObjectVal(map[string]cty.Value{
2316+
"a": cty.ListVal([]cty.Value{
2317+
cty.StringVal("b"),
2318+
}),
2319+
"b": cty.StringVal("b"),
2320+
}),
2321+
cty.ObjectVal(map[string]cty.Value{
2322+
"a": cty.ObjectVal(map[string]cty.Value{
2323+
"e": cty.StringVal("f"),
2324+
}),
2325+
}),
2326+
},
2327+
cty.ObjectVal(map[string]cty.Value{
2328+
"a": cty.ObjectVal(map[string]cty.Value{
2329+
"e": cty.StringVal("f"),
2330+
}),
2331+
"b": cty.StringVal("b"),
2332+
}),
2333+
false,
2334+
},
22162335
{ // argument error: non map type
22172336
[]cty.Value{
22182337
cty.MapVal(map[string]cty.Value{

website/docs/configuration/functions/merge.html.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ layout: "functions"
33
page_title: "merge - Functions - Configuration Language"
44
sidebar_current: "docs-funcs-collection-merge"
55
description: |-
6-
The merge function takes an arbitrary number of maps and returns a single
7-
map after merging the keys from each argument.
6+
The merge function takes an arbitrary number maps or objects, and returns a
7+
single map or object that contains a merged set of elements from all
8+
arguments.
89
---
910

1011
# `merge` Function
@@ -13,19 +14,33 @@ description: |-
1314
earlier, see
1415
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
1516

16-
`merge` takes an arbitrary number of maps and returns a single map that
17-
contains a merged set of elements from all of the maps.
17+
`merge` takes an arbitrary number of maps or objects, and returns a single map
18+
pr object that contains a merged set of elements from all arguments.
1819

19-
If more than one given map defines the same key then the one that is later
20-
in the argument sequence takes precedence.
20+
If more than one given map or object defines the same key or attribute, then
21+
the one that is later in the argument sequence takes precedence. If the
22+
argument types do not match, the resulting type will be an object matching the
23+
type structure of the attributes after the merging rules have been applied.
2124

2225
## Examples
2326

2427
```
25-
> merge({"a"="b", "c"="d"}, {"e"="f", "c"="z"})
28+
> merge({a="b", c="d"}, {e="f", c="z"})
2629
{
2730
"a" = "b"
2831
"c" = "z"
2932
"e" = "f"
3033
}
3134
```
35+
36+
```
37+
> merge({a="b"}, {a=[1,2], c="z"}, {d=3})
38+
{
39+
"a" = [
40+
1,
41+
2,
42+
]
43+
"c" = "z"
44+
"d" = 3
45+
}
46+
```

0 commit comments

Comments
 (0)