config: function calls work

This commit is contained in:
Mitchell Hashimoto 2014-07-21 12:56:03 -07:00
parent cabc007ec4
commit 6a191d7395
3 changed files with 179 additions and 0 deletions

View File

@ -2,10 +2,15 @@ package config
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// We really need to replace this with a real parser.
var funcRegexp *regexp.Regexp = regexp.MustCompile(
`(?i)([a-z0-9_]+)\(\s*(?:([.a-z0-9_]+)\s*,\s*)*([.a-z0-9_]+)\s*\)`)
// Interpolation is something that can be contained in a "${}" in a
// configuration value.
//
@ -17,6 +22,10 @@ type Interpolation interface {
Variables() map[string]InterpolatedVariable
}
// InterpolationFunc is the function signature for implementing
// callable functions in Terraform configurations.
type InterpolationFunc func(map[string]string, ...string) (string, error)
// An InterpolatedVariable is a variable reference within an interpolation.
//
// Implementations of this interface represents various sources where
@ -25,6 +34,15 @@ type InterpolatedVariable interface {
FullKey() string
}
// FunctionInterpolation is an Interpolation that executes a function
// with some variable number of arguments to generate a value.
type FunctionInterpolation struct {
Func InterpolationFunc
Args []InterpolatedVariable
key string
}
// VariableInterpolation implements Interpolation for simple variable
// interpolation. Ex: "${var.foo}" or "${aws_instance.foo.bar}"
type VariableInterpolation struct {
@ -69,6 +87,33 @@ type UserMapVariable struct {
// interpolation could not be found or the interpolation itself
// is invalid.
func NewInterpolation(v string) (Interpolation, error) {
match := funcRegexp.FindStringSubmatch(v)
if match != nil {
fn, ok := Funcs[match[1]]
if !ok {
return nil, fmt.Errorf(
"%s: Unknown function '%s'",
v, match[1])
}
args := make([]InterpolatedVariable, 0, len(match)-2)
for i := 2; i < len(match); i++ {
v, err := NewInterpolatedVariable(match[i])
if err != nil {
return nil, err
}
args = append(args, v)
}
return &FunctionInterpolation{
Func: fn,
Args: args,
key: v,
}, nil
}
if idx := strings.Index(v, "."); idx >= 0 {
v, err := NewInterpolatedVariable(v)
if err != nil {
@ -99,6 +144,43 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) {
}
}
func (i *FunctionInterpolation) FullString() string {
return i.key
}
func (i *FunctionInterpolation) Interpolate(
vs map[string]string) (string, error) {
args := make([]string, len(i.Args))
for idx, a := range i.Args {
k := a.FullKey()
v, ok := vs[k]
if !ok {
return "", fmt.Errorf(
"%s: variable argument value unknown: %s",
i.FullString(),
k)
}
args[idx] = v
}
return i.Func(vs, args...)
}
func (i *FunctionInterpolation) Variables() map[string]InterpolatedVariable {
result := make(map[string]InterpolatedVariable)
for _, a := range i.Args {
k := a.FullKey()
if _, ok := result[k]; ok {
continue
}
result[k] = a
}
return result
}
func (i *VariableInterpolation) FullString() string {
return i.key
}
@ -166,6 +248,10 @@ func (v *UserVariable) FullKey() string {
return v.key
}
func (v *UserVariable) GoString() string {
return fmt.Sprintf("*%#v", *v)
}
func NewUserMapVariable(key string) (*UserMapVariable, error) {
name := key[len("var."):]
idx := strings.Index(name, ".")

View File

@ -0,0 +1,17 @@
package config
// Funcs is the mapping of built-in functions for configuration.
var Funcs map[string]InterpolationFunc
func init() {
Funcs = map[string]InterpolationFunc{
"lookup": interpolationFuncLookup,
}
}
// interpolationFuncLookup implements the "lookup" function that allows
// dynamic lookups of map types within a Terraform configuration.
func interpolationFuncLookup(
vs map[string]string, args ...string) (string, error) {
return "", nil
}

View File

@ -2,6 +2,7 @@ package config
import (
"reflect"
"strings"
"testing"
)
@ -28,6 +29,25 @@ func TestNewInterpolation(t *testing.T) {
},
false,
},
{
"lookup(var.foo, var.bar)",
&FunctionInterpolation{
Func: nil, // Funcs["lookup"]
Args: []InterpolatedVariable{
&UserVariable{
Name: "foo",
key: "var.foo",
},
&UserVariable{
Name: "bar",
key: "var.bar",
},
},
key: "lookup(var.foo, var.bar)",
},
false,
},
}
for i, tc := range cases {
@ -35,6 +55,13 @@ func TestNewInterpolation(t *testing.T) {
if (err != nil) != tc.Error {
t.Fatalf("%d. Error: %s", i, err)
}
// This is jank, but reflect.DeepEqual never has functions
// being the same.
if f, ok := actual.(*FunctionInterpolation); ok {
f.Func = nil
}
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d bad: %#v", i, actual)
}
@ -106,6 +133,55 @@ func TestNewUserVariable(t *testing.T) {
}
}
func TestFunctionInterpolation_impl(t *testing.T) {
var _ Interpolation = new(FunctionInterpolation)
}
func TestFunctionInterpolation(t *testing.T) {
v1, err := NewInterpolatedVariable("var.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
v2, err := NewInterpolatedVariable("var.bar")
if err != nil {
t.Fatalf("err: %s", err)
}
fn := func(vs map[string]string, args ...string) (string, error) {
return strings.Join(args, " "), nil
}
i := &FunctionInterpolation{
Func: fn,
Args: []InterpolatedVariable{v1, v2},
key: "foo",
}
if i.FullString() != "foo" {
t.Fatalf("err: %#v", i)
}
expected := map[string]InterpolatedVariable{
"var.foo": v1,
"var.bar": v2,
}
if !reflect.DeepEqual(i.Variables(), expected) {
t.Fatalf("bad: %#v", i.Variables())
}
actual, err := i.Interpolate(map[string]string{
"var.foo": "bar",
"var.bar": "baz",
})
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "bar baz" {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceVariable_impl(t *testing.T) {
var _ InterpolatedVariable = new(ResourceVariable)
}