lang/funcs: zipmap accepts tuple of values and produces object

Now that our language supports tuple/object types in addition to list/map
types, it's convenient for zipmap to be able to produce an object type
given a tuple, since this makes it symmetrical with "keys" and "values"
such the the following identity holds for any map or object value "a"

    a == zipmap(keys(a), values(a))

When the values sequence is a tuple, the result has an object type whose
attribute types correspond to the given tuple types.

Since an object type has attribute names as part of its definition, there
is the additional constraint here that the result has an unknown type
(represented by the dynamic pseudo-type) if the given values is a tuple
and the given keys contains any unknown values. This isn't true for values
as a list because we can predict the resulting map element type using
just the list element type.
This commit is contained in:
Martin Atkins 2018-11-27 17:22:27 -08:00
parent 093cfacbcf
commit 30497bbfb7
2 changed files with 129 additions and 14 deletions

View File

@ -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())
}
},
})

View File

@ -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)
}
})
}