Merge pull request #7528 from hashicorp/f-list-interpolation-function

core: Add list() interpolation function
This commit is contained in:
James Bardin 2016-07-20 17:04:33 -04:00 committed by GitHub
commit d6d0c9087e
3 changed files with 254 additions and 57 deletions

View File

@ -69,6 +69,7 @@ func Funcs() map[string]ast.Function {
"join": interpolationFuncJoin(), "join": interpolationFuncJoin(),
"jsonencode": interpolationFuncJSONEncode(), "jsonencode": interpolationFuncJSONEncode(),
"length": interpolationFuncLength(), "length": interpolationFuncLength(),
"list": interpolationFuncList(),
"lower": interpolationFuncLower(), "lower": interpolationFuncLower(),
"md5": interpolationFuncMd5(), "md5": interpolationFuncMd5(),
"uuid": interpolationFuncUUID(), "uuid": interpolationFuncUUID(),
@ -83,6 +84,45 @@ func Funcs() map[string]ast.Function {
} }
} }
// interpolationFuncList creates a list from the parameters passed
// to it.
func interpolationFuncList() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{},
ReturnType: ast.TypeList,
Variadic: true,
VariadicType: ast.TypeAny,
Callback: func(args []interface{}) (interface{}, error) {
var outputList []ast.Variable
for i, val := range args {
switch v := val.(type) {
case string:
outputList = append(outputList, ast.Variable{Type: ast.TypeString, Value: v})
case []ast.Variable:
outputList = append(outputList, ast.Variable{Type: ast.TypeList, Value: v})
case map[string]ast.Variable:
outputList = append(outputList, ast.Variable{Type: ast.TypeMap, Value: v})
default:
return nil, fmt.Errorf("unexpected type %T for argument %d in list", v, i)
}
}
// we don't support heterogeneous types, so make sure all types match the first
if len(outputList) > 0 {
firstType := outputList[0].Type
for i, v := range outputList[1:] {
if v.Type != firstType {
return nil, fmt.Errorf("unexpected type %s for argument %d in list", v.Type, i+1)
}
}
}
return outputList, nil
},
}
}
// interpolationFuncCompact strips a list of multi-variable values // interpolationFuncCompact strips a list of multi-variable values
// (e.g. as returned by "split") of any empty strings. // (e.g. as returned by "split") of any empty strings.
func interpolationFuncCompact() ast.Function { func interpolationFuncCompact() ast.Function {
@ -218,10 +258,8 @@ func interpolationFuncCoalesce() ast.Function {
} }
} }
// interpolationFuncConcat implements the "concat" function that // interpolationFuncConcat implements the "concat" function that concatenates
// concatenates multiple strings. This isn't actually necessary anymore // multiple lists.
// since our language supports string concat natively, but for backwards
// compat we do this.
func interpolationFuncConcat() ast.Function { func interpolationFuncConcat() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeAny}, ArgTypes: []ast.Type{ast.TypeAny},
@ -229,33 +267,42 @@ func interpolationFuncConcat() ast.Function {
Variadic: true, Variadic: true,
VariadicType: ast.TypeAny, VariadicType: ast.TypeAny,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
var finalListElements []string var outputList []ast.Variable
for _, arg := range args { for _, arg := range args {
// Append strings for backward compatibility switch arg := arg.(type) {
if argument, ok := arg.(string); ok { case string:
finalListElements = append(finalListElements, argument) outputList = append(outputList, ast.Variable{Type: ast.TypeString, Value: arg})
continue case []ast.Variable:
} for _, v := range arg {
switch v.Type {
// Otherwise variables
if argument, ok := arg.([]ast.Variable); ok {
for _, element := range argument {
t := element.Type
switch t {
case ast.TypeString: case ast.TypeString:
finalListElements = append(finalListElements, element.Value.(string)) outputList = append(outputList, v)
case ast.TypeList:
outputList = append(outputList, v)
case ast.TypeMap:
outputList = append(outputList, v)
default: default:
return nil, fmt.Errorf("concat() does not support lists of %s", t.Printable()) return nil, fmt.Errorf("concat() does not support lists of %s", v.Type.Printable())
} }
} }
continue
}
return nil, fmt.Errorf("arguments to concat() must be a string or list of strings") default:
return nil, fmt.Errorf("concat() does not support %T", arg)
}
} }
return stringSliceToVariableValue(finalListElements), nil // we don't support heterogeneous types, so make sure all types match the first
if len(outputList) > 0 {
firstType := outputList[0].Type
for _, v := range outputList[1:] {
if v.Type != firstType {
return nil, fmt.Errorf("unexpected %s in list of %s", v.Type.Printable(), firstType.Printable())
}
}
}
return outputList, nil
}, },
} }
} }

View File

@ -5,13 +5,114 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/hashicorp/hil" "github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast" "github.com/hashicorp/hil/ast"
) )
func TestInterpolateFuncList(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{
// empty input returns empty list
{
`${list()}`,
[]interface{}{},
false,
},
// single input returns list of length 1
{
`${list("hello")}`,
[]interface{}{"hello"},
false,
},
// two inputs returns list of length 2
{
`${list("hello", "world")}`,
[]interface{}{"hello", "world"},
false,
},
// not a string input gives error
{
`${list("hello", 42)}`,
nil,
true,
},
// list of lists
{
`${list("${var.list}", "${var.list2}")}`,
[]interface{}{[]interface{}{"Hello", "World"}, []interface{}{"bar", "baz"}},
false,
},
// list of maps
{
`${list("${var.map}", "${var.map2}")}`,
[]interface{}{map[string]interface{}{"key": "bar"}, map[string]interface{}{"key2": "baz"}},
false,
},
// error on a heterogeneous list
{
`${list("first", "${var.list}")}`,
nil,
true,
},
},
Vars: map[string]ast.Variable{
"var.list": {
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeString,
Value: "Hello",
},
{
Type: ast.TypeString,
Value: "World",
},
},
},
"var.list2": {
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeString,
Value: "bar",
},
{
Type: ast.TypeString,
Value: "baz",
},
},
},
"var.map": {
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"key": {
Type: ast.TypeString,
Value: "bar",
},
},
},
"var.map2": {
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"key2": {
Type: ast.TypeString,
Value: "baz",
},
},
},
},
})
}
func TestInterpolateFuncCompact(t *testing.T) { func TestInterpolateFuncCompact(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{ Cases: []testFunctionCase{
@ -223,44 +324,88 @@ func TestInterpolateFuncConcat(t *testing.T) {
[]interface{}{"a", "b", "c", "d", "e", "f", "0", "1"}, []interface{}{"a", "b", "c", "d", "e", "f", "0", "1"},
false, false,
}, },
// list vars
{
`${concat("${var.list}", "${var.list}")}`,
[]interface{}{"a", "b", "a", "b"},
false,
},
// lists of lists
{
`${concat("${var.lists}", "${var.lists}")}`,
[]interface{}{[]interface{}{"c", "d"}, []interface{}{"c", "d"}},
false,
},
// lists of maps
{
`${concat("${var.maps}", "${var.maps}")}`,
[]interface{}{map[string]interface{}{"key1": "a", "key2": "b"}, map[string]interface{}{"key1": "a", "key2": "b"}},
false,
},
// mismatched types
{
`${concat("${var.lists}", "${var.maps}")}`,
nil,
true,
},
},
Vars: map[string]ast.Variable{
"var.list": {
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeString,
Value: "a",
},
{
Type: ast.TypeString,
Value: "b",
},
},
},
"var.lists": {
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeString,
Value: "c",
},
{
Type: ast.TypeString,
Value: "d",
},
},
},
},
},
"var.maps": {
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"key1": {
Type: ast.TypeString,
Value: "a",
},
"key2": {
Type: ast.TypeString,
Value: "b",
},
},
},
},
},
}, },
}) })
} }
// TODO: This test is split out and calls a private function
// because there's no good way to get a list of maps into the unit
// tests due to GH-7142 - once lists of maps can be expressed properly as
// literals this unit test can be wrapped back into the suite above.
//
// Reproduces crash reported in GH-7030.
func TestInterpolationFuncConcatListOfMaps(t *testing.T) {
listOfMapsOne := ast.Variable{
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeMap,
Value: map[string]interface{}{"one": "foo"},
},
},
}
listOfMapsTwo := ast.Variable{
Type: ast.TypeList,
Value: []ast.Variable{
{
Type: ast.TypeMap,
Value: map[string]interface{}{"two": "bar"},
},
},
}
args := []interface{}{listOfMapsOne.Value, listOfMapsTwo.Value}
_, err := interpolationFuncConcat().Callback(args)
if err == nil || !strings.Contains(err.Error(), "concat() does not support lists of type map") {
t.Fatalf("Expected err, got: %v", err)
}
}
func TestInterpolateFuncDistinct(t *testing.T) { func TestInterpolateFuncDistinct(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{ Cases: []testFunctionCase{

View File

@ -168,6 +168,11 @@ The supported built-in functions are:
* `${length(split(",", "a,b,c"))}` = 3 * `${length(split(",", "a,b,c"))}` = 3
* `${length("a,b,c")}` = 5 * `${length("a,b,c")}` = 5
* `list(items...)` - Returns a list consisting of the arguments to the function.
This function provides a way of representing list literals in interpolation.
* `${list("a", "b", "c")}` returns a list of `"a", "b", "c"`.
* `${list()}` returns an empty list.
* `lookup(map, key [, default])` - Performs a dynamic lookup into a mapping * `lookup(map, key [, default])` - Performs a dynamic lookup into a mapping
variable. The `map` parameter should be another variable, such variable. The `map` parameter should be another variable, such
as `var.amis`. If `key` does not exist in `map`, the interpolation will as `var.amis`. If `key` does not exist in `map`, the interpolation will