diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index 79e11d708..d3343415a 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -986,29 +986,57 @@ var ZipmapFunc = function.New(&function.Spec{ }, { Name: "values", - Type: cty.List(cty.DynamicPseudoType), + Type: cty.DynamicPseudoType, }, }, Type: func(args []cty.Value) (ret cty.Type, err error) { keys := args[0] values := args[1] + valuesTy := values.Type() - if !keys.IsKnown() || !values.IsKnown() || keys.LengthInt() == 0 { - return cty.Map(cty.DynamicPseudoType), nil - } + switch { + case valuesTy.IsListType(): + return cty.Map(values.Type().ElementType()), nil + case valuesTy.IsTupleType(): + if !keys.IsWhollyKnown() { + // Since zipmap with a tuple produces an object, we need to know + // all of the key names before we can predict our result type. + return cty.DynamicPseudoType, nil + } - if keys.LengthInt() != values.LengthInt() { - return cty.NilType, fmt.Errorf("count of keys (%d) does not match count of values (%d)", - keys.LengthInt(), values.LengthInt()) + keysRaw := keys.AsValueSlice() + valueTypesRaw := valuesTy.TupleElementTypes() + if len(keysRaw) != len(valueTypesRaw) { + return cty.NilType, fmt.Errorf("number of keys (%d) does not match number of values (%d)", len(keysRaw), len(valueTypesRaw)) + } + atys := make(map[string]cty.Type, len(valueTypesRaw)) + for i, keyVal := range keysRaw { + if keyVal.IsNull() { + return cty.NilType, fmt.Errorf("keys list has null value at index %d", i) + } + key := keyVal.AsString() + atys[key] = valueTypesRaw[i] + } + return cty.Object(atys), nil + + default: + return cty.NilType, fmt.Errorf("values argument must be a list or tuple value") } - return cty.Map(values.Type().ElementType()), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { keys := args[0] values := args[1] - if !keys.IsKnown() || !values.IsKnown() || keys.LengthInt() == 0 { - return cty.MapValEmpty(cty.DynamicPseudoType), nil + if !keys.IsWhollyKnown() { + // Unknown map keys and object attributes are not supported, so + // our entire result must be unknown in this case. + return cty.UnknownVal(retType), nil + } + + // both keys and values are guaranteed to be shallowly-known here, + // because our declared params above don't allow unknown or null values. + if keys.LengthInt() != values.LengthInt() { + return cty.NilVal, fmt.Errorf("number of keys (%d) does not match number of values (%d)", keys.LengthInt(), values.LengthInt()) } output := make(map[string]cty.Value) @@ -1021,7 +1049,19 @@ var ZipmapFunc = function.New(&function.Spec{ i++ } - return cty.MapVal(output), nil + switch { + case retType.IsMapType(): + if len(output) == 0 { + return cty.MapValEmpty(retType.ElementType()), nil + } + return cty.MapVal(output), nil + case retType.IsObjectType(): + return cty.ObjectVal(output), nil + default: + // Should never happen because the type-check function should've + // caught any other case. + return cty.NilVal, fmt.Errorf("internally selected incorrect result type %s (this is a bug)", retType.FriendlyName()) + } }, }) diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 83b8a3cbf..7e1ed6714 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -2204,13 +2204,88 @@ func TestZipmap(t *testing.T) { }), false, }, + { // tuple values produce object + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }), + cty.TupleVal([]cty.Value{ + cty.StringVal("bar"), + cty.UnknownVal(cty.Bool), + }), + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("bar"), + "world": cty.UnknownVal(cty.Bool), + }), + false, + }, + { // empty tuple produces empty object + cty.ListValEmpty(cty.String), + cty.EmptyTupleVal, + cty.EmptyObjectVal, + false, + }, + { // tuple with any unknown keys produces DynamicVal + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.UnknownVal(cty.String), + }), + cty.TupleVal([]cty.Value{ + cty.StringVal("bar"), + cty.True, + }), + cty.DynamicVal, + false, + }, + { // tuple with all keys unknown produces DynamicVal + cty.UnknownVal(cty.List(cty.String)), + cty.TupleVal([]cty.Value{ + cty.StringVal("bar"), + cty.True, + }), + cty.DynamicVal, + false, + }, + { // list with all keys unknown produces correctly-typed unknown map + cty.UnknownVal(cty.List(cty.String)), + cty.ListVal([]cty.Value{ + cty.StringVal("bar"), + cty.StringVal("baz"), + }), + cty.UnknownVal(cty.Map(cty.String)), + false, + }, + { // unknown tuple as values produces correctly-typed unknown object + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }), + cty.UnknownVal(cty.Tuple([]cty.Type{ + cty.String, + cty.Bool, + })), + cty.UnknownVal(cty.Object(map[string]cty.Type{ + "hello": cty.String, + "world": cty.Bool, + })), + false, + }, + { // unknown list as values produces correctly-typed unknown map + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }), + cty.UnknownVal(cty.List(cty.String)), + cty.UnknownVal(cty.Map(cty.String)), + false, + }, { // empty input returns an empty map cty.ListValEmpty(cty.String), cty.ListValEmpty(cty.String), - cty.MapValEmpty(cty.DynamicPseudoType), + cty.MapValEmpty(cty.String), false, }, - { // keys cannot be a list + { // keys cannot be a list of lists list5, list1, cty.NilVal, @@ -2232,7 +2307,7 @@ func TestZipmap(t *testing.T) { } if !got.RawEquals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + t.Errorf("wrong result\n\nkeys: %#v\nvalues: %#v\ngot: %#v\nwant: %#v", test.Keys, test.Values, got, test.Want) } }) }