Skip to content

Recursive DeepMerge function #25032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.dll
*.exe
.DS_Store
.vscode
example.tf
terraform.tfplan
terraform.tfstate
Expand Down
142 changes: 142 additions & 0 deletions lang/funcs/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,134 @@ var CoalesceFunc = function.New(&function.Spec{
},
})

// DeepMergeFunc 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 or object defines the same key then the one that
// is later in the argument sequence takes precedence.
//
// Nested object will be recursed to the bottom, and partial maps will be combined to
// create the final map. Types can be changed [array->string] without issue, so no
// type checking occurs to prevent this.
var DeepMergeFunc = 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
}

// check for invalid arguments
for _, arg := range args {
ty := arg.Type()

// we can't work with dynamic types, so move along
if ty.Equals(cty.DynamicPseudoType) {
return cty.DynamicPseudoType, nil
}

if !ty.IsMapType() && !ty.IsObjectType() {
return cty.NilType, fmt.Errorf("arguments must be maps or objects, got %#v", ty.FriendlyName())
}
}

//premerge objects so we can determine final type
outputMap := cty.NullVal(cty.NilType)

for _, arg := range args {
outputMap = recursiveMerge(arg, outputMap)
}

if outputMap.Type().HasDynamicTypes() {
return cty.DynamicPseudoType, nil
}

return outputMap.Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
outputMap := cty.NullVal(cty.NilType)

for _, arg := range args {
outputMap = recursiveMerge(arg, outputMap)
}

return outputMap, nil
},
})

func recursiveMerge(newValue cty.Value, existingValue cty.Value) cty.Value {

// unknown types trump known ones, don't merge
if !existingValue.IsKnown() {
return existingValue
}

// New value isn't mergeable, so just replace.
newValueMergeable := newValue.Type().IsMapType() || newValue.Type().IsObjectType()
existingValueMergeable := existingValue.Type().IsMapType() || existingValue.Type().IsObjectType()
if !newValueMergeable || !existingValueMergeable || existingValue.IsNull() {
return newValue
}

if newValue.IsNull() {
return existingValue
}

mergedMap := existingValue.AsValueMap()
for it := newValue.ElementIterator(); it.Next(); {
k, newValue := it.Element()
key := k.AsString()
if newValue.IsNull() {
delete(mergedMap, key)
continue
}

mergedMap[key] = recursiveMerge(newValue, mergedMap[key])
}
// strip out any null properties
for key, value := range mergedMap {
if value.IsNull() {
delete(mergedMap, key)
}
}

return wrapAsObjectOrMap(mergedMap)
}

func wrapAsObjectOrMap(input map[string]cty.Value) cty.Value {
if len(input) == 0 {
return cty.EmptyObjectVal
}

typesMatch := true
firstType := cty.NilType

for _, value := range input {
ty := value.Type()
if firstType == cty.NilType {
firstType = ty
}

if !ty.Equals(firstType) {
typesMatch = false
}
}

if typesMatch {
return cty.MapVal(input)
}

return cty.ObjectVal(input)
}

// IndexFunc constructs a function that finds the element index for a given value in a list.
var IndexFunc = function.New(&function.Spec{
Params: []function.Parameter{
Expand Down Expand Up @@ -587,6 +715,20 @@ func Coalesce(args ...cty.Value) (cty.Value, error) {
return CoalesceFunc.Call(args)
}

// DeepMergeFunc 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 or object defines the same key then the one that
// is later in the argument sequence takes precedence.
//
// Nested object will be recursed to the bottom, and partial maps will be combined to
// create the final map. Types can be changed [array->string] without issue, so no
// type checking occurs to prevent this.
func DeepMerge(maps ...cty.Value) (cty.Value, error) {
return DeepMergeFunc.Call(maps)
}

// Index finds the element index for a given value in a list.
func Index(list, value cty.Value) (cty.Value, error) {
return IndexFunc.Call([]cty.Value{list, value})
Expand Down
Loading