functions: TransposeFunc, SliceFunc

This commit is contained in:
Kristin Laemmert 2018-05-31 14:46:24 -07:00 committed by Martin Atkins
parent 30671d85ad
commit 6463dd90e9
3 changed files with 278 additions and 2 deletions

View File

@ -2,6 +2,7 @@ package funcs
import (
"fmt"
"sort"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
@ -666,6 +667,112 @@ var MergeFunc = function.New(&function.Spec{
},
})
// SliceFunc contructs a function that extracts some consecutive elements
// from within a list.
var SliceFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "startIndex",
Type: cty.Number,
},
{
Name: "endIndex",
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.List(cty.DynamicPseudoType)),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputList := args[0]
var startIndex, endIndex int
if err = gocty.FromCtyValue(args[1], &startIndex); err != nil {
return cty.NilVal, fmt.Errorf("invalid start index: %s", err)
}
if err = gocty.FromCtyValue(args[2], &endIndex); err != nil {
return cty.NilVal, fmt.Errorf("invalid start index: %s", err)
}
if startIndex < 0 {
return cty.NilVal, fmt.Errorf("from index must be >= 0")
}
if endIndex > inputList.LengthInt() {
return cty.NilVal, fmt.Errorf("to index must be <= length of the input list")
}
if startIndex > endIndex {
return cty.NilVal, fmt.Errorf("from index must be <= to index")
}
var outputList []cty.Value
i := 0
for it := inputList.ElementIterator(); it.Next(); {
_, v := it.Element()
if i >= startIndex && i < endIndex {
outputList = append(outputList, v)
}
i++
}
if len(outputList) == 0 {
return cty.ListValEmpty(cty.DynamicPseudoType), nil
}
return cty.ListVal(outputList), nil
},
})
// TransposeFunc contructs a function that takes a map of lists of strings and
// swaps the keys and values to produce a new map of lists of strings.
var TransposeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "values",
Type: cty.Map(cty.List(cty.String)),
},
},
Type: function.StaticReturnType(cty.Map(cty.List(cty.String))),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputMap := args[0]
outputMap := make(map[string]cty.Value)
tmpMap := make(map[string][]string)
for it := inputMap.ElementIterator(); it.Next(); {
inKey, inVal := it.Element()
if !inVal.Type().IsListType() {
return cty.MapValEmpty(cty.List(cty.String)), fmt.Errorf("input must be a map of lists of strings")
}
for iter := inVal.ElementIterator(); iter.Next(); {
_, val := iter.Element()
if !val.Type().Equals(cty.String) {
return cty.MapValEmpty(cty.List(cty.String)), fmt.Errorf("input must be a map of lists of strings")
}
outKey := val.AsString()
if _, ok := tmpMap[outKey]; !ok {
tmpMap[outKey] = make([]string, 0)
}
outVal := tmpMap[outKey]
outVal = append(outVal, inKey.AsString())
sort.Strings(outVal)
tmpMap[outKey] = outVal
}
}
for outKey, outVal := range tmpMap {
values := make([]cty.Value, 0)
for _, v := range outVal {
values = append(values, cty.StringVal(v))
}
outputMap[outKey] = cty.ListVal(values)
}
return cty.MapVal(outputMap), 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 {
@ -769,3 +876,14 @@ func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
func Merge(maps ...cty.Value) (cty.Value, error) {
return MergeFunc.Call(maps)
}
// Slice extracts some consecutive elements from within a list.
func Slice(list, start, end cty.Value) (cty.Value, error) {
return SliceFunc.Call([]cty.Value{list, start, end})
}
// Transpose takes a map of lists of strings and swaps the keys and values to
// produce a new map of lists of strings.
func Transpose(values cty.Value) (cty.Value, error) {
return TransposeFunc.Call([]cty.Value{values})
}

View File

@ -1529,3 +1529,161 @@ func TestMerge(t *testing.T) {
})
}
}
func TestSlice(t *testing.T) {
listOfStrings := cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
})
listOfInts := cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
})
tests := []struct {
List cty.Value
StartIndex cty.Value
EndIndex cty.Value
Want cty.Value
Err bool
}{
{ // normal usage
listOfStrings,
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.ListVal([]cty.Value{
cty.StringVal("b"),
}),
false,
},
{ // normal usage
listOfInts,
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.ListVal([]cty.Value{
cty.NumberIntVal(2),
}),
false,
},
{ // empty result
listOfStrings,
cty.NumberIntVal(1),
cty.NumberIntVal(1),
cty.ListValEmpty(cty.DynamicPseudoType),
false,
},
{ // index out of bounds
listOfStrings,
cty.NumberIntVal(1),
cty.NumberIntVal(4),
cty.NilVal,
true,
},
{ // StartIndex index > EndIndex
listOfStrings,
cty.NumberIntVal(2),
cty.NumberIntVal(1),
cty.NilVal,
true,
},
{ // negative StartIndex
listOfStrings,
cty.NumberIntVal(-1),
cty.NumberIntVal(0),
cty.NilVal,
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("slice(%#v, %#v, %#v)", test.List, test.StartIndex, test.EndIndex), func(t *testing.T) {
got, err := Slice(test.List, test.StartIndex, test.EndIndex)
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 TestTranspose(t *testing.T) {
tests := []struct {
Values cty.Value
Want cty.Value
Err bool
}{
{
cty.MapVal(map[string]cty.Value{
"key1": cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
}),
"key2": cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
"key3": cty.ListVal([]cty.Value{
cty.StringVal("c"),
}),
"key4": cty.ListValEmpty(cty.String),
}),
cty.MapVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("key1"),
cty.StringVal("key2"),
}),
"b": cty.ListVal([]cty.Value{
cty.StringVal("key1"),
cty.StringVal("key2"),
}),
"c": cty.ListVal([]cty.Value{
cty.StringVal("key2"),
cty.StringVal("key3"),
}),
}),
false,
},
{ // bad map - empty value
cty.MapVal(map[string]cty.Value{
"key1": cty.ListValEmpty(cty.String),
}),
cty.NilVal,
true,
},
{ // bad map - value not a list
cty.MapVal(map[string]cty.Value{
"key1": cty.StringVal("a"),
}),
cty.NilVal,
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("transpose(%#v)", test.Values), func(t *testing.T) {
got, err := Transpose(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

@ -83,14 +83,14 @@ func (s *Scope) Functions() map[string]function.Function {
"sha256": funcs.Sha256Func,
"sha512": funcs.Sha512Func,
"signum": funcs.SignumFunc,
"slice": unimplFunc, // TODO
"slice": funcs.SliceFunc,
"sort": funcs.SortFunc,
"split": funcs.SplitFunc,
"substr": stdlib.SubstrFunc,
"timestamp": funcs.TimestampFunc,
"timeadd": funcs.TimeAddFunc,
"title": funcs.TitleFunc,
"transpose": unimplFunc, // TODO
"transpose": funcs.TransposeFunc,
"trimspace": funcs.TrimSpaceFunc,
"upper": stdlib.UpperFunc,
"urlencode": funcs.URLEncodeFunc,