diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index 49b963520b..50e4a3ee3d 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -415,6 +415,73 @@ var ListFunc = function.New(&function.Spec{ }, }) +// MapFunc contructs a function that takes an even number of arguments and +// returns a map whose elements are constructed from consecutive pairs of arguments. +// +// This function is deprecated in Terraform v0.12 +var MapFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "vals", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + if len(args) < 2 || len(args)%2 != 0 { + return cty.NilType, fmt.Errorf("map requires an even number of two or more arguments, got %d", len(args)) + } + + argTypes := make([]cty.Type, len(args)/2) + index := 0 + + for i := 0; i < len(args); i += 2 { + argTypes[index] = args[i+1].Type() + index++ + } + + valType, _ := convert.UnifyUnsafe(argTypes) + if valType == cty.NilType { + return cty.NilType, fmt.Errorf("all arguments must have the same type") + } + + return cty.Map(valType), nil + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + outputMap := make(map[string]cty.Value) + + for i := 0; i < len(args); i += 2 { + + key := args[i].AsString() + + err := gocty.FromCtyValue(args[i], &key) + if err != nil { + return cty.NilVal, err + } + + val := args[i+1] + + var variable cty.Value + err = gocty.FromCtyValue(val, &variable) + if err != nil { + return cty.NilVal, err + } + + // We already know this will succeed because of the checks in our Type func above + variable, _ = convert.Convert(variable, retType.ElementType()) + + // Check for duplicate keys + if _, ok := outputMap[key]; ok { + return cty.NilVal, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key) + } + outputMap[key] = variable + } + + return cty.MapVal(outputMap), nil + }, +}) + // MatchkeysFunc contructs a function that constructs a new list by taking a // subset of elements from one list whose indexes match the corresponding // indexes of values in another list. @@ -557,6 +624,12 @@ func List(args ...cty.Value) (cty.Value, error) { return ListFunc.Call(args) } +// Map takes an even number of arguments and returns a map whose elements are constructed +// from consecutive pairs of arguments. +func Map(args ...cty.Value) (cty.Value, error) { + return MapFunc.Call(args) +} + // Matchkeys constructs a new list by taking a subset of elements from one list // whose indexes match the corresponding indexes of values in another list. func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) { diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 3606e25f9a..163b662157 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -905,6 +905,137 @@ func TestList(t *testing.T) { } } +func TestMap(t *testing.T) { + tests := []struct { + Values []cty.Value + Want cty.Value + Err bool + }{ + { + []cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }, + cty.MapVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + false, + }, + { + []cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + cty.StringVal("what's"), + cty.StringVal("up"), + }, + cty.MapVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + "what's": cty.StringVal("up"), + }), + false, + }, + { + []cty.Value{ + cty.StringVal("hello"), + cty.NumberIntVal(1), + cty.StringVal("goodbye"), + cty.NumberIntVal(42), + }, + cty.MapVal(map[string]cty.Value{ + "hello": cty.NumberIntVal(1), + "goodbye": cty.NumberIntVal(42), + }), + false, + }, + { // convert numbers to strings + []cty.Value{ + cty.StringVal("hello"), + cty.NumberIntVal(1), + cty.StringVal("goodbye"), + cty.StringVal("42"), + }, + cty.MapVal(map[string]cty.Value{ + "hello": cty.StringVal("1"), + "goodbye": cty.StringVal("42"), + }), + false, + }, + { // map of lists is okay + []cty.Value{ + cty.StringVal("hello"), + cty.ListVal([]cty.Value{ + cty.StringVal("world"), + }), + cty.StringVal("what's"), + cty.ListVal([]cty.Value{ + cty.StringVal("up"), + }), + }, + cty.MapVal(map[string]cty.Value{ + "hello": cty.ListVal([]cty.Value{cty.StringVal("world")}), + "what's": cty.ListVal([]cty.Value{cty.StringVal("up")}), + }), + false, + }, + { // map of maps is okay + []cty.Value{ + cty.StringVal("hello"), + cty.MapVal(map[string]cty.Value{ + "there": cty.StringVal("world"), + }), + cty.StringVal("what's"), + cty.MapVal(map[string]cty.Value{ + "really": cty.StringVal("up"), + }), + }, + cty.MapVal(map[string]cty.Value{ + "hello": cty.MapVal(map[string]cty.Value{ + "there": cty.StringVal("world"), + }), + "what's": cty.MapVal(map[string]cty.Value{ + "really": cty.StringVal("up"), + }), + }), + false, + }, + { // single argument returns an error + []cty.Value{ + cty.StringVal("hello"), + }, + cty.NilVal, + true, + }, + { // duplicate keys returns an error + []cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + cty.StringVal("hello"), + cty.StringVal("universe"), + }, + cty.NilVal, + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("map(%#v)", test.Values), func(t *testing.T) { + got, err := Map(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) + } + }) + } +} + func TestMatchkeys(t *testing.T) { tests := []struct { Keys cty.Value diff --git a/lang/functions.go b/lang/functions.go index 770ccbc714..877cb320c4 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -69,7 +69,7 @@ func (s *Scope) Functions() map[string]function.Function { "log": funcs.LogFunc, "lookup": unimplFunc, // TODO "lower": stdlib.LowerFunc, - "map": unimplFunc, // TODO + "map": funcs.MapFunc, "matchkeys": funcs.MatchkeysFunc, "max": stdlib.MaxFunc, "md5": funcs.Md5Func,