From 4c9e0f395c05056ec222f09f5d324285dab7b115 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Jul 2014 11:24:44 -0700 Subject: [PATCH] config: basic interpolationWalker --- config/interpolate.go | 97 ++++++++++++++++++++--------- config/interpolate_walk.go | 107 ++++++++++++++++++++++++++++++++ config/interpolate_walk_test.go | 55 ++++++++++++++++ 3 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 config/interpolate_walk.go create mode 100644 config/interpolate_walk_test.go diff --git a/config/interpolate.go b/config/interpolate.go index 7c0a9812c..dc4f39fd1 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -33,19 +33,6 @@ type VariableInterpolation struct { key string } -func (i *VariableInterpolation) FullString() string { - return i.key -} - -func (i *VariableInterpolation) Interpolate( - vs map[string]string) (string, error) { - return vs[i.key], nil -} - -func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable { - return map[string]InterpolatedVariable{i.key: i.Variable} -} - // A ResourceVariable is a variable that is referencing the field // of a resource, such as "${aws_instance.foo.ami}" type ResourceVariable struct { @@ -59,6 +46,72 @@ type ResourceVariable struct { key string } +// A UserVariable is a variable that is referencing a user variable +// that is inputted from outside the configuration. This looks like +// "${var.foo}" +type UserVariable struct { + Name string + + key string +} + +// A UserMapVariable is a variable that is referencing a user +// variable that is a map. This looks like "${var.amis.us-east-1}" +type UserMapVariable struct { + Name string + Elem string + + key string +} + +// NewInterpolation takes some string and returns the valid +// Interpolation associated with it, or error if a valid +// interpolation could not be found or the interpolation itself +// is invalid. +func NewInterpolation(v string) (Interpolation, error) { + if idx := strings.Index(v, "."); idx >= 0 { + v, err := NewInterpolatedVariable(v) + if err != nil { + return nil, err + } + + return &VariableInterpolation{ + Variable: v, + key: v.FullKey(), + }, nil + } + + return nil, fmt.Errorf( + "Interpolation '%s' is not a valid interpolation. " + + "Please check your syntax and try again.") +} + +func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { + if !strings.HasPrefix(v, "var.") { + return NewResourceVariable(v) + } + + varKey := v[len("var."):] + if strings.Index(varKey, ".") == -1 { + return NewUserVariable(v) + } else { + return NewUserMapVariable(v) + } +} + +func (i *VariableInterpolation) FullString() string { + return i.key +} + +func (i *VariableInterpolation) Interpolate( + vs map[string]string) (string, error) { + return vs[i.key], nil +} + +func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable { + return map[string]InterpolatedVariable{i.key: i.Variable} +} + func NewResourceVariable(key string) (*ResourceVariable, error) { parts := strings.SplitN(key, ".", 3) field := parts[2] @@ -101,15 +154,6 @@ func (v *ResourceVariable) FullKey() string { return v.key } -// A UserVariable is a variable that is referencing a user variable -// that is inputted from outside the configuration. This looks like -// "${var.foo}" -type UserVariable struct { - Name string - - key string -} - func NewUserVariable(key string) (*UserVariable, error) { name := key[len("var."):] return &UserVariable{ @@ -122,15 +166,6 @@ func (v *UserVariable) FullKey() string { return v.key } -// A UserMapVariable is a variable that is referencing a user -// variable that is a map. This looks like "${var.amis.us-east-1}" -type UserMapVariable struct { - Name string - Elem string - - key string -} - func NewUserMapVariable(key string) (*UserMapVariable, error) { name := key[len("var."):] idx := strings.Index(name, ".") diff --git a/config/interpolate_walk.go b/config/interpolate_walk.go new file mode 100644 index 000000000..15bc84586 --- /dev/null +++ b/config/interpolate_walk.go @@ -0,0 +1,107 @@ +package config + +import ( + "reflect" + "regexp" + + "github.com/mitchellh/reflectwalk" +) + +// interpRegexp is a regexp that matches interpolations such as ${foo.bar} +var interpRegexp *regexp.Regexp = regexp.MustCompile( + `(?i)(\$+)\{([*-.a-z0-9_]+)\}`) + +// interpolationWalker implements interfaces for the reflectwalk package +// (github.com/mitchellh/reflectwalk) that can be used to automatically +// execute a callback for an interpolation. +type interpolationWalker struct { + // F must be one of interpolationWalkerFunc or + // interpolationReplaceWalkerFunc. + F interpolationWalkerFunc + Replace bool + + key []string + loc reflectwalk.Location + cs []reflect.Value + csData interface{} +} + +type interpolationWalkerFunc func(Interpolation) (string, error) + +func (w *interpolationWalker) Enter(loc reflectwalk.Location) error { + w.loc = loc + return nil +} + +func (w *interpolationWalker) Exit(loc reflectwalk.Location) error { + w.loc = reflectwalk.None + + switch loc { + case reflectwalk.Map: + w.cs = w.cs[:len(w.cs)-1] + case reflectwalk.MapValue: + w.key = w.key[:len(w.key)-1] + } + + return nil +} + +func (w *interpolationWalker) Map(m reflect.Value) error { + w.cs = append(w.cs, m) + return nil +} + +func (w *interpolationWalker) MapElem(m, k, v reflect.Value) error { + w.csData = k + w.key = append(w.key, k.String()) + return nil +} + +func (w *interpolationWalker) Primitive(v reflect.Value) error { + // We only care about strings + if v.Kind() == reflect.Interface { + v = v.Elem() + } + if v.Kind() != reflect.String { + return nil + } + + // XXX: This can be a lot more efficient if we used a real + // parser. A regexp is a hammer though that will get this working. + + matches := interpRegexp.FindAllStringSubmatch(v.String(), -1) + if len(matches) == 0 { + return nil + } + + for _, match := range matches { + dollars := len(match[1]) + + // If there are even amounts of dollar signs, then it is escaped + if dollars%2 == 0 { + continue + } + + // Interpolation found, instantiate it + key := match[2] + + i, err := NewInterpolation(key) + if err != nil { + return err + } + + replaceVal, err := w.F(i) + if err != nil { + return err + } + + if w.Replace { + // TODO(mitchellh): replace + println(replaceVal) + } + + return nil + } + + return nil +} diff --git a/config/interpolate_walk_test.go b/config/interpolate_walk_test.go new file mode 100644 index 000000000..15fa29d50 --- /dev/null +++ b/config/interpolate_walk_test.go @@ -0,0 +1,55 @@ +package config + +import ( + "reflect" + "testing" + + "github.com/mitchellh/reflectwalk" +) + +func TestInterpolationWalker_detect(t *testing.T) { + cases := []struct { + Input interface{} + Result []Interpolation + }{ + { + Input: map[string]interface{}{ + "foo": "$${var.foo}", + }, + Result: nil, + }, + + { + Input: map[string]interface{}{ + "foo": "${var.foo}", + }, + Result: []Interpolation{ + &VariableInterpolation{ + Variable: &UserVariable{ + Name: "foo", + key: "var.foo", + }, + key: "var.foo", + }, + }, + }, + } + + for i, tc := range cases { + var actual []Interpolation + + detectFn := func(i Interpolation) (string, error) { + actual = append(actual, i) + return "", nil + } + + w := &interpolationWalker{F: detectFn} + if err := reflectwalk.Walk(tc.Input, w); err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(actual, tc.Result) { + t.Fatalf("%d: bad:\n\n%#v", i, actual) + } + } +}