diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index e13c75486..532dd50d1 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -409,16 +409,54 @@ func interpolationFuncJoin() ast.Function { } // interpolationFuncJSONEncode implements the "jsonencode" function that encodes -// a string as its JSON representation. +// a string, list, or map as its JSON representation. For now, values in the +// list or map may only be strings. func interpolationFuncJSONEncode() ast.Function { return ast.Function{ - ArgTypes: []ast.Type{ast.TypeString}, + ArgTypes: []ast.Type{ast.TypeAny}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { - s := args[0].(string) - jEnc, err := json.Marshal(s) + var toEncode interface{} + + switch typedArg := args[0].(type) { + case string: + toEncode = typedArg + + case []ast.Variable: + // We preallocate the list here. Note that it's important that in + // the length 0 case, we have an empty list rather than nil, as + // they encode differently. + // XXX It would be nice to support arbitrarily nested data here. Is + // there an inverse of hil.InterfaceToVariable? + strings := make([]string, len(typedArg)) + + for i, v := range typedArg { + if v.Type != ast.TypeString { + return "", fmt.Errorf("list elements must be strings") + } + strings[i] = v.Value.(string) + } + toEncode = strings + + case map[string]ast.Variable: + // XXX It would be nice to support arbitrarily nested data here. Is + // there an inverse of hil.InterfaceToVariable? + stringMap := make(map[string]string) + for k, v := range typedArg { + if v.Type != ast.TypeString { + return "", fmt.Errorf("map values must be strings") + } + stringMap[k] = v.Value.(string) + } + toEncode = stringMap + + default: + return "", fmt.Errorf("unknown type for JSON encoding: %T", args[0]) + } + + jEnc, err := json.Marshal(toEncode) if err != nil { - return "", fmt.Errorf("failed to encode JSON data '%s'", s) + return "", fmt.Errorf("failed to encode JSON data '%s'", toEncode) } return string(jEnc), nil }, diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index d5f44d498..5b99281f5 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -419,6 +419,22 @@ func TestInterpolateFuncJSONEncode(t *testing.T) { Value: " foo \\ \n \t \" bar ", Type: ast.TypeString, }, + "list": interfaceToVariableSwallowError([]string{"foo", "bar\tbaz"}), + // XXX can't use InterfaceToVariable as it converts empty slice into empty + // map. + "emptylist": ast.Variable{ + Value: []ast.Variable{}, + Type: ast.TypeList, + }, + "map": interfaceToVariableSwallowError(map[string]string{ + "foo": "bar", + "ba \n z": "q\\x", + }), + "emptymap": interfaceToVariableSwallowError(map[string]string{}), + + // Not yet supported (but it would be nice) + "nestedlist": interfaceToVariableSwallowError([][]string{{"foo"}}), + "nestedmap": interfaceToVariableSwallowError(map[string][]string{"foo": {"bar"}}), }, Cases: []testFunctionCase{ { @@ -446,6 +462,36 @@ func TestInterpolateFuncJSONEncode(t *testing.T) { nil, true, }, + { + `${jsonencode(list)}`, + `["foo","bar\tbaz"]`, + false, + }, + { + `${jsonencode(emptylist)}`, + `[]`, + false, + }, + { + `${jsonencode(map)}`, + `{"ba \n z":"q\\x","foo":"bar"}`, + false, + }, + { + `${jsonencode(emptymap)}`, + `{}`, + false, + }, + { + `${jsonencode(nestedlist)}`, + nil, + true, + }, + { + `${jsonencode(nestedmap)}`, + nil, + true, + }, }, }) } diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 1d5674e08..85d2708eb 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -150,8 +150,10 @@ The supported built-in functions are: only possible with splat variables from resources with a count greater than one. Example: `join(",", aws_instance.foo.*.id)` - * `jsonencode(string)` - Returns a JSON-encoded representation of the given - string (including double quotes). + * `jsonencode(item)` - Returns a JSON-encoded representation of the given + item, which may be a string, list of strings, or map from string to string. + Note that if the item is a string, the return value includes the double + quotes. * `length(list)` - Returns a number of members in a given list or a number of characters in a given string.