Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 96 additions & 11 deletions lang/funcs/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
},
})

Expand Down
127 changes: 123 additions & 4 deletions lang/funcs/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}),
Expand All @@ -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,
},
Expand All @@ -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"),
}),
Expand Down Expand Up @@ -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"),
}),
Expand All @@ -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"),
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ I think we should add this (type and value being overridden) as an example to the documentation, what do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it looks like this was never documented to work on anything by maps! Hard to roll that back now since users very quickly discovered that arbitrary combinations of objects and maps could be merged.

I guess we need to update the whole doc page on this

[]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{
Expand Down
29 changes: 22 additions & 7 deletions website/docs/configuration/functions/merge.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Howdy, I was just passing by. Sorry if this is a duplicate:

Watch out pr object I think you meant or object

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, totally missed that


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
}
```