functions: ZipmapFunc

This commit is contained in:
Kristin Laemmert 2018-06-01 09:23:06 -07:00 committed by Martin Atkins
parent 2ef56b3e05
commit 4f5c03339a
3 changed files with 270 additions and 2 deletions

View File

@ -775,6 +775,106 @@ var TransposeFunc = function.New(&function.Spec{
},
})
// ValuesFunc contructs a function that returns a list of the map values,
// in the order of the sorted keys. This function only works on flat maps.
var ValuesFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "values",
Type: cty.Map(cty.DynamicPseudoType),
},
},
Type: function.StaticReturnType(cty.List(cty.DynamicPseudoType)),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
mapVar := args[0]
keys, err := Keys(mapVar)
if err != nil {
return cty.NilVal, err
}
var values []cty.Value
for it := keys.ElementIterator(); it.Next(); {
_, v := it.Element()
value := mapVar.Index(cty.StringVal(v.AsString()))
if !value.Type().Equals(cty.String) && !value.Type().Equals(cty.Number) {
return cty.NilVal, fmt.Errorf("values only works on flat maps")
}
values = append(values, value)
}
if len(values) == 0 {
return cty.ListValEmpty(cty.DynamicPseudoType), nil
}
return cty.ListVal(values), nil
},
})
// ZipmapFunc contructs a function that constructs a map from a list of keys
// and a corresponding list of values.
var ZipmapFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "keys",
Type: cty.List(cty.String),
},
{
Name: "values",
Type: cty.List(cty.DynamicPseudoType),
},
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
keys := args[0]
values := args[1]
if keys.LengthInt() == 0 {
return cty.Map(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())
}
argTypes := make([]cty.Type, values.LengthInt())
index := 0
for it := values.ElementIterator(); it.Next(); {
_, v := it.Element()
argTypes[index] = v.Type()
index++
}
valType, _ := convert.UnifyUnsafe(argTypes)
if valType == cty.NilType {
return cty.NilType, fmt.Errorf("list elements must have the same type")
}
return cty.Map(valType), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
keys := args[0]
values := args[1]
if keys.LengthInt() == 0 {
return cty.MapValEmpty(cty.DynamicPseudoType), nil
}
output := make(map[string]cty.Value)
i := 0
for it := keys.ElementIterator(); it.Next(); {
_, v := it.Element()
val := values.Index(cty.NumberIntVal(int64(i)))
output[v.AsString()] = val
i++
}
return cty.MapVal(output), nil
},
})
// 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) {
for _, ele := range slice {
@ -889,3 +989,14 @@ func Slice(list, start, end cty.Value) (cty.Value, error) {
func Transpose(values cty.Value) (cty.Value, error) {
return TransposeFunc.Call([]cty.Value{values})
}
// Values returns a list of the map values, in the order of the sorted keys.
// This function only works on flat maps.
func Values(values cty.Value) (cty.Value, error) {
return ValuesFunc.Call([]cty.Value{values})
}
// Zipmap constructs a map from a list of keys and a corresponding list of values.
func Zipmap(keys, values cty.Value) (cty.Value, error) {
return ZipmapFunc.Call([]cty.Value{keys, values})
}

View File

@ -1687,3 +1687,160 @@ func TestTranspose(t *testing.T) {
})
}
}
func TestValues(t *testing.T) {
tests := []struct {
Values cty.Value
Want cty.Value
Err bool
}{
{
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
"what's": cty.StringVal("up"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("world"),
cty.StringVal("up"),
}),
false,
},
{ // note ordering: keys are sorted first
cty.MapVal(map[string]cty.Value{
"hello": cty.NumberIntVal(1),
"goodbye": cty.NumberIntVal(42),
}),
cty.ListVal([]cty.Value{
cty.NumberIntVal(42),
cty.NumberIntVal(1),
}),
false,
},
{ // error: map of lists
cty.MapVal(map[string]cty.Value{
"hello": cty.ListVal([]cty.Value{cty.StringVal("world")}),
"what's": cty.ListVal([]cty.Value{cty.StringVal("up")}),
}),
cty.NilVal,
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("values(%#v)", test.Values), func(t *testing.T) {
got, err := Values(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 TestZipmap(t *testing.T) {
list1 := cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("world"),
})
list2 := cty.ListVal([]cty.Value{
cty.StringVal("bar"),
cty.StringVal("baz"),
})
list3 := cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("there"),
cty.StringVal("world"),
})
list4 := cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(42),
})
list5 := cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("bar"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("baz"),
}),
})
tests := []struct {
Keys cty.Value
Values cty.Value
Want cty.Value
Err bool
}{
{
list1,
list2,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("bar"),
"world": cty.StringVal("baz"),
}),
false,
},
{
list1,
list4,
cty.MapVal(map[string]cty.Value{
"hello": cty.NumberIntVal(1),
"world": cty.NumberIntVal(42),
}),
false,
},
{ // length mismatch
list1,
list3,
cty.NilVal,
true,
},
{ // map of lists
list1,
list5,
cty.MapVal(map[string]cty.Value{
"hello": cty.ListVal([]cty.Value{cty.StringVal("bar")}),
"world": cty.ListVal([]cty.Value{cty.StringVal("baz")}),
}),
false,
},
{ // empty input returns an empty map
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String),
cty.MapValEmpty(cty.DynamicPseudoType),
false,
},
{ // keys cannot be a list
list5,
list1,
cty.NilVal,
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("zipmap(%#v, %#v)", test.Keys, test.Values), func(t *testing.T) {
got, err := Zipmap(test.Keys, 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

@ -95,8 +95,8 @@ func (s *Scope) Functions() map[string]function.Function {
"upper": stdlib.UpperFunc,
"urlencode": funcs.URLEncodeFunc,
"uuid": funcs.UUIDFunc,
"values": unimplFunc, // TODO
"zipmap": unimplFunc, // TODO
"values": funcs.ValuesFunc,
"zipmap": funcs.ZipmapFunc,
}
if s.PureOnly {