diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 91aa66da7..45f8e8915 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -71,6 +71,7 @@ func Funcs() map[string]ast.Function { "length": interpolationFuncLength(), "list": interpolationFuncList(), "lower": interpolationFuncLower(), + "map": interpolationFuncMap(), "md5": interpolationFuncMd5(), "uuid": interpolationFuncUUID(), "replace": interpolationFuncReplace(), @@ -123,6 +124,50 @@ func interpolationFuncList() ast.Function { } } +// interpolationFuncMap creates a map from the parameters passed +// to it. +func interpolationFuncMap() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{}, + ReturnType: ast.TypeMap, + Variadic: true, + VariadicType: ast.TypeAny, + Callback: func(args []interface{}) (interface{}, error) { + outputMap := make(map[string]ast.Variable) + + if len(args)%2 != 0 { + return nil, fmt.Errorf("requires an even number of arguments, got %d", len(args)) + } + + var firstType *ast.Type + for i := 0; i < len(args); i += 2 { + key, ok := args[i].(string) + if !ok { + return nil, fmt.Errorf("argument %d represents a key, so it must be a string", i+1) + } + val := args[i+1] + variable, err := hil.InterfaceToVariable(val) + if err != nil { + return nil, err + } + // Enforce map type homogeneity + if firstType == nil { + firstType = &variable.Type + } else if variable.Type != *firstType { + return nil, fmt.Errorf("all map values must have the same type, got %s then %s", firstType.Printable(), variable.Type.Printable()) + } + // Check for duplicate keys + if _, ok := outputMap[key]; ok { + return nil, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key) + } + outputMap[key] = variable + } + + return outputMap, nil + }, + } +} + // interpolationFuncCompact strips a list of multi-variable values // (e.g. as returned by "split") of any empty strings. func interpolationFuncCompact() ast.Function { diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 541bcffab..dcc563ecb 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -113,6 +113,81 @@ func TestInterpolateFuncList(t *testing.T) { }) } +func TestInterpolateFuncMap(t *testing.T) { + testFunction(t, testFunctionConfig{ + Cases: []testFunctionCase{ + // empty input returns empty map + { + `${map()}`, + map[string]interface{}{}, + false, + }, + + // odd args is error + { + `${map("odd")}`, + nil, + true, + }, + + // two args returns map w/ one k/v + { + `${map("hello", "world")}`, + map[string]interface{}{"hello": "world"}, + false, + }, + + // four args get two k/v + { + `${map("hello", "world", "what's", "up?")}`, + map[string]interface{}{"hello": "world", "what's": "up?"}, + false, + }, + + // map of lists is okay + { + `${map("hello", list("world"), "what's", list("up?"))}`, + map[string]interface{}{ + "hello": []interface{}{"world"}, + "what's": []interface{}{"up?"}, + }, + false, + }, + + // map of maps is okay + { + `${map("hello", map("there", "world"), "what's", map("really", "up?"))}`, + map[string]interface{}{ + "hello": map[string]interface{}{"there": "world"}, + "what's": map[string]interface{}{"really": "up?"}, + }, + false, + }, + + // keys have to be strings + { + `${map(list("listkey"), "val")}`, + nil, + true, + }, + + // types have to match + { + `${map("some", "strings", "also", list("lists"))}`, + nil, + true, + }, + + // duplicate keys are an error + { + `${map("key", "val", "key", "again")}`, + nil, + true, + }, + }, + }) +} + func TestInterpolateFuncCompact(t *testing.T) { testFunction(t, testFunctionConfig{ Cases: []testFunctionCase{ diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 338550377..ff555f2e6 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -181,6 +181,13 @@ The supported built-in functions are: * `lower(string)` - Returns a copy of the string with all Unicode letters mapped to their lower case. + * `map(key, value, ...)` - Returns a map consisting of the key/value pairs + specified as arguments. Every odd argument must be a string key, and every + even argument must have the same type as the other values specified. + Duplicate keys are not allowed. Examples: + * `map("hello", "world")` + * `map("us-east", list("a", "b", "c"), "us-west", list("b", "c", "d"))` + * `md5(string)` - Returns a (conventional) hexadecimal representation of the MD5 hash of the given string.