config: convert fucntions, put functions into Scope

This commit is contained in:
Mitchell Hashimoto 2015-01-13 11:50:44 -08:00
parent 4ba7de17a9
commit 1ccad4d729
5 changed files with 118 additions and 269 deletions

View File

@ -2,17 +2,12 @@ package config
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/hashicorp/terraform/config/lang/ast"
)
// 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.
//
@ -23,10 +18,6 @@ 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
@ -35,13 +26,6 @@ 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 []Interpolation
}
// LiteralInterpolation implements Interpolation for literals. Ex:
// ${"foo"} will equal "foo".
type LiteralInterpolation struct {
@ -130,36 +114,6 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) {
}
}
func (i *FunctionInterpolation) Interpolate(
vs map[string]string) (string, error) {
args := make([]string, len(i.Args))
for idx, a := range i.Args {
v, err := a.Interpolate(vs)
if err != nil {
return "", err
}
args[idx] = v
}
return i.Func(vs, args...)
}
func (i *FunctionInterpolation) GoString() string {
return fmt.Sprintf("*%#v", *i)
}
func (i *FunctionInterpolation) Variables() map[string]InterpolatedVariable {
result := make(map[string]InterpolatedVariable)
for _, a := range i.Args {
for k, v := range a.Variables() {
result[k] = v
}
}
return result
}
func (i *LiteralInterpolation) Interpolate(
map[string]string) (string, error) {
return i.Literal, nil

View File

@ -1,73 +1,60 @@
package config
import (
"bytes"
"fmt"
"io/ioutil"
"strconv"
"strings"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
)
// Funcs is the mapping of built-in functions for configuration.
var Funcs map[string]InterpolationFunc
var Funcs map[string]lang.Function
func init() {
Funcs = map[string]InterpolationFunc{
"concat": interpolationFuncConcat,
"file": interpolationFuncFile,
"join": interpolationFuncJoin,
"lookup": interpolationFuncLookup,
"element": interpolationFuncElement,
Funcs = map[string]lang.Function{
"file": interpolationFuncFile(),
"join": interpolationFuncJoin(),
//"lookup": interpolationFuncLookup(),
"element": interpolationFuncElement(),
}
}
// interpolationFuncConcat implements the "concat" function that allows
// strings to be joined together.
func interpolationFuncConcat(
vs map[string]string, args ...string) (string, error) {
var buf bytes.Buffer
for _, a := range args {
if _, err := buf.WriteString(a); err != nil {
return "", err
}
}
return buf.String(), nil
}
// interpolationFuncFile implements the "file" function that allows
// loading contents from a file.
func interpolationFuncFile(
vs map[string]string, args ...string) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf(
"file expects 1 arguments, got %d", len(args))
}
func interpolationFuncFile() lang.Function {
return lang.Function{
ArgTypes: []ast.Type{ast.TypeString},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
data, err := ioutil.ReadFile(args[0].(string))
if err != nil {
return "", err
}
data, err := ioutil.ReadFile(args[0])
if err != nil {
return "", err
return string(data), nil
},
}
return string(data), nil
}
// interpolationFuncJoin implements the "join" function that allows
// multi-variable values to be joined by some character.
func interpolationFuncJoin(
vs map[string]string, args ...string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("join expects 2 arguments")
}
func interpolationFuncJoin() lang.Function {
return lang.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
var list []string
for _, arg := range args[1:] {
parts := strings.Split(arg.(string), InterpSplitDelim)
list = append(list, parts...)
}
var list []string
for _, arg := range args[1:] {
parts := strings.Split(arg, InterpSplitDelim)
list = append(list, parts...)
return strings.Join(list, args[0].(string)), nil
},
}
return strings.Join(list, args[0]), nil
}
// interpolationFuncLookup implements the "lookup" function that allows
@ -93,22 +80,21 @@ func interpolationFuncLookup(
// interpolationFuncElement implements the "element" function that allows
// a specific index to be looked up in a multi-variable value. Note that this will
// wrap if the index is larger than the number of elements in the multi-variable value.
func interpolationFuncElement(
vs map[string]string, args ...string) (string, error) {
if len(args) != 2 {
return "", fmt.Errorf(
"element expects 2 arguments, got %d", len(args))
func interpolationFuncElement() lang.Function {
return lang.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
list := strings.Split(args[0].(string), InterpSplitDelim)
index, err := strconv.Atoi(args[1].(string))
if err != nil {
return "", fmt.Errorf(
"invalid number for index, got %s", args[1])
}
v := list[index%len(list)]
return v, nil
},
}
list := strings.Split(args[0], InterpSplitDelim)
index, err := strconv.Atoi(args[1])
if err != nil {
return "", fmt.Errorf(
"invalid number for index, got %s", args[1])
}
v := list[index % len(list)]
return v, nil
}

View File

@ -4,46 +4,12 @@ import (
"fmt"
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/hashicorp/terraform/config/lang"
)
func TestInterpolateFuncConcat(t *testing.T) {
cases := []struct {
Args []string
Result string
Error bool
}{
{
[]string{"foo", "bar", "baz"},
"foobarbaz",
false,
},
{
[]string{"foo", "bar"},
"foobar",
false,
},
{
[]string{"foo"},
"foo",
false,
},
}
for i, tc := range cases {
actual, err := interpolationFuncConcat(nil, tc.Args...)
if (err != nil) != tc.Error {
t.Fatalf("%d: err: %s", i, err)
}
if actual != tc.Result {
t.Fatalf("%d: bad: %#v", i, actual)
}
}
}
func TestInterpolateFuncFile(t *testing.T) {
tf, err := ioutil.TempFile("", "tf")
if err != nil {
@ -54,94 +20,67 @@ func TestInterpolateFuncFile(t *testing.T) {
tf.Close()
defer os.Remove(path)
cases := []struct {
Args []string
Result string
Error bool
}{
testFunction(t, []testFunctionCase{
{
[]string{path},
fmt.Sprintf(`${file("%s")}`, path),
"foo",
false,
},
// Invalid path
{
[]string{"/i/dont/exist"},
"",
`${file("/i/dont/exist")}`,
nil,
true,
},
// Too many args
{
[]string{"foo", "bar"},
"",
`${file("foo", "bar")}`,
nil,
true,
},
}
for i, tc := range cases {
actual, err := interpolationFuncFile(nil, tc.Args...)
if (err != nil) != tc.Error {
t.Fatalf("%d: err: %s", i, err)
}
if actual != tc.Result {
t.Fatalf("%d: bad: %#v", i, actual)
}
}
})
}
func TestInterpolateFuncJoin(t *testing.T) {
cases := []struct {
Args []string
Result string
Error bool
}{
testFunction(t, []testFunctionCase{
{
[]string{","},
"",
`${join(",")}`,
nil,
true,
},
{
[]string{",", "foo"},
`${join(",", "foo")}`,
"foo",
false,
},
{
[]string{",", "foo", "bar"},
"foo,bar",
false,
},
/*
TODO
{
`${join(",", "foo", "bar")}`,
"foo,bar",
false,
},
*/
{
[]string{
".",
fmt.Sprintf(`${join(".", "%s")}`,
fmt.Sprintf(
"foo%sbar%sbaz",
InterpSplitDelim,
InterpSplitDelim),
},
InterpSplitDelim)),
"foo.bar.baz",
false,
},
}
for i, tc := range cases {
actual, err := interpolationFuncJoin(nil, tc.Args...)
if (err != nil) != tc.Error {
t.Fatalf("%d: err: %s", i, err)
}
if actual != tc.Result {
t.Fatalf("%d: bad: %#v", i, actual)
}
}
})
}
/*
func TestInterpolateFuncLookup(t *testing.T) {
testFunction(t, []testFunctionCase{
cases := []struct {
M map[string]string
Args []string
@ -189,48 +128,62 @@ func TestInterpolateFuncLookup(t *testing.T) {
}
}
}
*/
func TestInterpolateFuncElement(t *testing.T) {
cases := []struct {
Args []string
Result string
Error bool
}{
testFunction(t, []testFunctionCase{
{
[]string{"foo" + InterpSplitDelim + "baz", "1"},
fmt.Sprintf(`${element("%s", "1")}`,
"foo"+InterpSplitDelim+"baz"),
"baz",
false,
},
{
[]string{"foo", "0"},
`${element("foo", "0")}`,
"foo",
false,
},
// Invalid index should wrap vs. out-of-bounds
{
[]string{"foo" + InterpSplitDelim + "baz", "2"},
fmt.Sprintf(`${element("%s", "2")}`,
"foo"+InterpSplitDelim+"baz"),
"foo",
false,
},
// Too many args
{
[]string{"foo" + InterpSplitDelim + "baz", "0", "1"},
"",
fmt.Sprintf(`${element("%s", "0", "2")}`,
"foo"+InterpSplitDelim+"baz"),
nil,
true,
},
}
})
}
type testFunctionCase struct {
Input string
Result interface{}
Error bool
}
func testFunction(t *testing.T, cases []testFunctionCase) {
for i, tc := range cases {
actual, err := interpolationFuncElement(nil, tc.Args...)
ast, err := lang.Parse(tc.Input)
if err != nil {
t.Fatalf("%d: err: %s", i, err)
}
engine := langEngine(nil)
out, _, err := engine.Execute(ast)
if (err != nil) != tc.Error {
t.Fatalf("%d: err: %s", i, err)
}
if actual != tc.Result {
t.Fatalf("%d: bad: %#v", i, actual)
if !reflect.DeepEqual(out, tc.Result) {
t.Fatalf("%d: bad: %#v", i, out)
}
}
}

View File

@ -2,7 +2,6 @@ package config
import (
"reflect"
"strings"
"testing"
"github.com/hashicorp/terraform/config/lang"
@ -123,54 +122,6 @@ func TestNewUserVariable_map(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: []Interpolation{
&VariableInterpolation{Variable: v1},
&VariableInterpolation{Variable: v2},
},
}
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 TestLiteralInterpolation_impl(t *testing.T) {
var _ Interpolation = new(LiteralInterpolation)
}

View File

@ -81,16 +81,7 @@ func (r *RawConfig) Config() map[string]interface{} {
//
// If a variable key is missing, this will panic.
func (r *RawConfig) Interpolate(vs map[string]string) error {
varMap := make(map[string]lang.Variable)
for k, v := range vs {
varMap[k] = lang.Variable{Value: v, Type: ast.TypeString}
}
engine := &lang.Engine{
GlobalScope: &lang.Scope{
VarMap: varMap,
},
}
engine := langEngine(vs)
return r.interpolate(func(root ast.Node) (string, error) {
out, _, err := engine.Execute(root)
if err != nil {
@ -210,3 +201,17 @@ type gobRawConfig struct {
Key string
Raw map[string]interface{}
}
// langEngine returns the lang.Engine to use for evaluating configurations.
func langEngine(vs map[string]string) *lang.Engine {
varMap := make(map[string]lang.Variable)
for k, v := range vs {
varMap[k] = lang.Variable{Value: v, Type: ast.TypeString}
}
return &lang.Engine{
GlobalScope: &lang.Scope{
VarMap: varMap,
FuncMap: Funcs,
},
}
}