functions: MergeFunc

This commit is contained in:
Kristin Laemmert 2018-05-31 11:47:50 -07:00 committed by Martin Atkins
parent aecd7b2e62
commit 30671d85ad
3 changed files with 206 additions and 1 deletions

View File

@ -634,6 +634,38 @@ var MatchkeysFunc = function.New(&function.Spec{
}, },
}) })
// MergeFunc contructs 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.
//
// If more than one given map 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,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: function.StaticReturnType(cty.DynamicPseudoType),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
outputMap := make(map[string]cty.Value)
for _, arg := range args {
if !arg.Type().IsObjectType() && !arg.Type().IsMapType() {
return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type())
}
for it := arg.ElementIterator(); it.Next(); {
k, v := it.Element()
outputMap[k.AsString()] = v
}
}
return cty.ObjectVal(outputMap), nil
},
})
// helper function to add an element to a list, if it does not already exist // helper function to add an element to a list, if it does not already exist
func appendIfMissing(slice []cty.Value, element cty.Value) ([]cty.Value, error) { func appendIfMissing(slice []cty.Value, element cty.Value) ([]cty.Value, error) {
for _, ele := range slice { for _, ele := range slice {
@ -728,3 +760,12 @@ func Map(args ...cty.Value) (cty.Value, error) {
func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) { func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
return MatchkeysFunc.Call([]cty.Value{values, keys, searchset}) return MatchkeysFunc.Call([]cty.Value{values, keys, searchset})
} }
// Merge takes an arbitrary number of maps and returns a single map that contains
// a merged set of elements from all of the maps.
//
// If more than one given map defines the same key then the one that is later in
// the argument sequence takes precedence.
func Merge(maps ...cty.Value) (cty.Value, error) {
return MergeFunc.Call(maps)
}

View File

@ -1365,3 +1365,167 @@ func TestMatchkeys(t *testing.T) {
}) })
} }
} }
func TestMerge(t *testing.T) {
tests := []struct {
Values []cty.Value
Want cty.Value
Err bool
}{
{
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("b"),
}),
cty.MapVal(map[string]cty.Value{
"c": cty.StringVal("d"),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("b"),
"c": cty.StringVal("d"),
}),
false,
},
{ // merge with conflicts is ok, last in wins
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("b"),
"c": cty.StringVal("d"),
}),
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("x"),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("x"),
"c": cty.StringVal("d"),
}),
false,
},
{ // only accept maps
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("b"),
"c": cty.StringVal("d"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("x"),
}),
},
cty.NilVal,
true,
},
{ // merge maps of maps
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"b": cty.StringVal("c"),
}),
}),
cty.MapVal(map[string]cty.Value{
"d": cty.MapVal(map[string]cty.Value{
"e": cty.StringVal("f"),
}),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{
"b": cty.StringVal("c"),
}),
"d": cty.MapVal(map[string]cty.Value{
"e": cty.StringVal("f"),
}),
}),
false,
},
{ // map of lists
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("b"),
cty.StringVal("c"),
}),
}),
cty.MapVal(map[string]cty.Value{
"d": cty.ListVal([]cty.Value{
cty.StringVal("e"),
cty.StringVal("f"),
}),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("b"),
cty.StringVal("c"),
}),
"d": cty.ListVal([]cty.Value{
cty.StringVal("e"),
cty.StringVal("f"),
}),
}),
false,
},
{ // merge map of various kinds
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("b"),
cty.StringVal("c"),
}),
}),
cty.MapVal(map[string]cty.Value{
"d": cty.MapVal(map[string]cty.Value{
"e": cty.StringVal("f"),
}),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("b"),
cty.StringVal("c"),
}),
"d": cty.MapVal(map[string]cty.Value{
"e": cty.StringVal("f"),
}),
}),
false,
},
{ // argument error: non map type
[]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("b"),
cty.StringVal("c"),
}),
}),
cty.ListVal([]cty.Value{
cty.StringVal("d"),
cty.StringVal("e"),
}),
},
cty.NilVal,
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("merge(%#v)", test.Values), func(t *testing.T) {
got, err := Merge(test.Values...)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

View File

@ -73,7 +73,7 @@ func (s *Scope) Functions() map[string]function.Function {
"matchkeys": funcs.MatchkeysFunc, "matchkeys": funcs.MatchkeysFunc,
"max": stdlib.MaxFunc, "max": stdlib.MaxFunc,
"md5": funcs.Md5Func, "md5": funcs.Md5Func,
"merge": unimplFunc, // TODO "merge": funcs.MergeFunc,
"min": stdlib.MinFunc, "min": stdlib.MinFunc,
"pathexpand": funcs.PathExpandFunc, "pathexpand": funcs.PathExpandFunc,
"pow": funcs.PowFunc, "pow": funcs.PowFunc,