From 1ccad4d7292cd41360a6beb220fc518b93ec7c2e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Jan 2015 11:50:44 -0800 Subject: [PATCH] config: convert fucntions, put functions into Scope --- config/interpolate.go | 46 --------- config/interpolate_funcs.go | 110 ++++++++++------------ config/interpolate_funcs_test.go | 157 +++++++++++-------------------- config/interpolate_test.go | 49 ---------- config/raw_config.go | 25 +++-- 5 files changed, 118 insertions(+), 269 deletions(-) diff --git a/config/interpolate.go b/config/interpolate.go index 3e95642e05..806bac24de 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -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 diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index cbba62d0dd..c67e3b733c 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -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 } diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 332b9af4be..3a9a86fe14 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -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) } } } diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 2bafe43628..923ff660d9 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -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) } diff --git a/config/raw_config.go b/config/raw_config.go index 11481b087f..37867c638a 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -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, + }, + } +}