config: add formatlist

formatlist distributes formatting over lists.
See the docs for details.

As a colleague commented:

"It happens all the time that we want a set of
outputs, but in a slightly different way than
just simple joining or concatting."

formatlist (combined with join)
makes it easy to satisfy those needs.
This commit is contained in:
Josh Bleecher Snyder 2015-05-05 20:34:40 -07:00
parent a3f79cd790
commit 02e751e356
3 changed files with 135 additions and 7 deletions

View File

@ -2,6 +2,7 @@ package config
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"regexp"
@ -17,13 +18,14 @@ var Funcs map[string]ast.Function
func init() {
Funcs = map[string]ast.Function{
"file": interpolationFuncFile(),
"format": interpolationFuncFormat(),
"join": interpolationFuncJoin(),
"element": interpolationFuncElement(),
"replace": interpolationFuncReplace(),
"split": interpolationFuncSplit(),
"length": interpolationFuncLength(),
"file": interpolationFuncFile(),
"format": interpolationFuncFormat(),
"formatlist": interpolationFuncFormatList(),
"join": interpolationFuncJoin(),
"element": interpolationFuncElement(),
"replace": interpolationFuncReplace(),
"split": interpolationFuncSplit(),
"length": interpolationFuncLength(),
// Concat is a little useless now since we supported embeddded
// interpolations but we keep it around for backwards compat reasons.
@ -88,6 +90,69 @@ func interpolationFuncFormat() ast.Function {
}
}
// interpolationFuncFormatList implements the "formatlist" function that does
// string formatting on lists.
func interpolationFuncFormatList() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
Variadic: true,
VariadicType: ast.TypeAny,
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
// Make a copy of the variadic part of args
// to avoid modifying the original.
varargs := make([]interface{}, len(args)-1)
copy(varargs, args[1:])
// Convert arguments that are lists into slices.
// Confirm along the way that all lists have the same length (n).
var n int
for i := 1; i < len(args); i++ {
s, ok := args[i].(string)
if !ok {
continue
}
parts := strings.Split(s, InterpSplitDelim)
if len(parts) == 1 {
continue
}
varargs[i-1] = parts
if n == 0 {
// first list we've seen
n = len(parts)
continue
}
if n != len(parts) {
return nil, fmt.Errorf("format: mismatched list lengths: %d != %d", n, len(parts))
}
}
if n == 0 {
return nil, errors.New("no lists in arguments to formatlist")
}
// Do the formatting.
format := args[0].(string)
// Generate a list of formatted strings.
list := make([]string, n)
fmtargs := make([]interface{}, len(varargs))
for i := 0; i < n; i++ {
for j, arg := range varargs {
switch arg := arg.(type) {
default:
fmtargs[j] = arg
case []string:
fmtargs[j] = arg[i]
}
}
list[i] = fmt.Sprintf(format, fmtargs...)
}
return strings.Join(list, InterpSplitDelim), nil
},
}
}
// interpolationFuncJoin implements the "join" function that allows
// multi-variable values to be joined by some character.
func interpolationFuncJoin() ast.Function {

View File

@ -106,6 +106,59 @@ func TestInterpolateFuncFormat(t *testing.T) {
})
}
func TestInterpolateFuncFormatList(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{
// formatlist requires at least one list
{
`${formatlist("hello")}`,
nil,
true,
},
{
`${formatlist("hello %s", "world")}`,
nil,
true,
},
// formatlist applies to each list element in turn
{
`${formatlist("<%s>", split(",", "A,B"))}`,
"<A>" + InterpSplitDelim + "<B>",
false,
},
// formatlist repeats scalar elements
{
`${join(", ", formatlist("%s=%s", "x", split(",", "A,B,C")))}`,
"x=A, x=B, x=C",
false,
},
// Multiple lists are walked in parallel
{
`${join(", ", formatlist("%s=%s", split(",", "A,B,C"), split(",", "1,2,3")))}`,
"A=1, B=2, C=3",
false,
},
// formatlist of lists of length zero/one are repeated, just as scalars are
{
`${join(", ", formatlist("%s=%s", split(",", ""), split(",", "1,2,3")))}`,
"=1, =2, =3",
false,
},
{
`${join(", ", formatlist("%s=%s", split(",", "A"), split(",", "1,2,3")))}`,
"A=1, A=2, A=3",
false,
},
// Mismatched list lengths generate an error
{
`${formatlist("%s=%2s", split(",", "A,B,C,D"), split(",", "1,2,3"))}`,
nil,
true,
},
},
})
}
func TestInterpolateFuncJoin(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{

View File

@ -89,6 +89,16 @@ The supported built-in functions are:
Example to zero-prefix a count, used commonly for naming servers:
`format("web-%03d", count.index+1)`.
* `formatlist(format, args...)` - Formats each element of a list
according to the given format, similarly to `format`, and returns a list.
Non-list arguments are repeated for each list element.
For example, to convert a list of DNS addresses to a list of URLs, you might use:
`formatlist("https://%s:%s/", aws_instance.foo.*.public_dns, var.port)`.
If multiple args are lists, and they have the same number of elements, then the formatting is applied to the elements of the lists in parallel.
Example:
`formatlist("instance %v has private ip %v", aws_instance.foo.*.id, aws_instance.foo.*.private_ip)`.
Passing lists with different lengths to formatlist results in an error.
* `join(delim, list)` - Joins the list with the delimiter. A list is
only possible with splat variables from resources with a count
greater than one. Example: `join(",", aws_instance.foo.*.id)`