config: get rid of the variable*Walkers, replace with more generic

This commit is contained in:
Mitchell Hashimoto 2014-07-21 11:45:56 -07:00
parent 4099c64833
commit cabc007ec4
5 changed files with 53 additions and 540 deletions

View File

@ -19,10 +19,11 @@ type interpolationWalker struct {
F interpolationWalkerFunc F interpolationWalkerFunc
Replace bool Replace bool
key []string key []string
loc reflectwalk.Location loc reflectwalk.Location
cs []reflect.Value cs []reflect.Value
csData interface{} csData interface{}
unknownKeys []string
} }
// interpolationWalkerFunc is the callback called by interpolationWalk. // interpolationWalkerFunc is the callback called by interpolationWalk.
@ -105,6 +106,13 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
} }
if w.Replace { if w.Replace {
// If this is an unknown variable, then we remove it from
// the configuration.
if replaceVal == UnknownVariableValue {
w.removeCurrent()
return nil
}
result = strings.Replace(result, match[0], replaceVal, -1) result = strings.Replace(result, match[0], replaceVal, -1)
} }
} }
@ -125,3 +133,19 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
return nil return nil
} }
func (w *interpolationWalker) removeCurrent() {
c := w.cs[len(w.cs)-1]
switch c.Kind() {
case reflect.Map:
// Zero value so that we delete the map key
var val reflect.Value
// Get the key and delete it
k := w.csData.(reflect.Value)
c.SetMapIndex(k, val)
}
// Append the key to the unknown keys
w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, "."))
}

View File

@ -70,10 +70,10 @@ func TestInterpolationWalker_replace(t *testing.T) {
{ {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": "${var.foo}", "foo": "hello, ${var.foo}",
}, },
Output: map[string]interface{}{ Output: map[string]interface{}{
"foo": "bar", "foo": "hello, bar",
}, },
}, },
} }

View File

@ -69,26 +69,43 @@ func (r *RawConfig) Interpolate(vs map[string]string) error {
if err != nil { if err != nil {
return err return err
} }
w := &variableReplaceWalker{Values: vs}
r.config = config.(map[string]interface{}) r.config = config.(map[string]interface{})
fn := func(i Interpolation) (string, error) {
return i.Interpolate(vs)
}
w := &interpolationWalker{F: fn, Replace: true}
err = reflectwalk.Walk(r.config, w) err = reflectwalk.Walk(r.config, w)
if err != nil { if err != nil {
return err return err
} }
r.unknownKeys = w.UnknownKeys r.unknownKeys = w.unknownKeys
return nil return nil
} }
func (r *RawConfig) init() error { func (r *RawConfig) init() error {
walker := new(variableDetectWalker) r.config = r.Raw
r.Variables = nil
fn := func(i Interpolation) (string, error) {
for k, v := range i.Variables() {
if r.Variables == nil {
r.Variables = make(map[string]InterpolatedVariable)
}
r.Variables[k] = v
}
return "", nil
}
walker := &interpolationWalker{F: fn}
if err := reflectwalk.Walk(r.Raw, walker); err != nil { if err := reflectwalk.Walk(r.Raw, walker); err != nil {
return err return err
} }
r.Variables = walker.Variables
r.config = r.Raw
return nil return nil
} }

View File

@ -1,222 +0,0 @@
package config
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/mitchellh/reflectwalk"
)
// varRegexp is a regexp that matches variables such as ${foo.bar}
var varRegexp *regexp.Regexp
func init() {
varRegexp = regexp.MustCompile(`(?i)(\$+)\{([*-.a-z0-9_]+)\}`)
}
// ReplaceVariables takes a configuration and a mapping of variables
// and performs the structure walking necessary to properly replace
// all the variables.
func ReplaceVariables(
c interface{},
vs map[string]string) ([]string, error) {
w := &variableReplaceWalker{Values: vs}
if err := reflectwalk.Walk(c, w); err != nil {
return nil, err
}
return w.UnknownKeys, nil
}
// variableDetectWalker implements interfaces for the reflectwalk package
// (github.com/mitchellh/reflectwalk) that can be used to automatically
// pull out the variables that need replacing.
type variableDetectWalker struct {
Variables map[string]InterpolatedVariable
}
func (w *variableDetectWalker) 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 := varRegexp.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
}
// Otherwise, record it
key := match[2]
if w.Variables == nil {
w.Variables = make(map[string]InterpolatedVariable)
}
if _, ok := w.Variables[key]; ok {
continue
}
var err error
var iv InterpolatedVariable
if strings.Index(key, ".") == -1 {
return fmt.Errorf(
"Interpolated variable '%s' has bad format. "+
"Did you mean 'var.%s'?",
key, key)
}
if !strings.HasPrefix(key, "var.") {
iv, err = NewResourceVariable(key)
} else {
varKey := key[len("var."):]
if strings.Index(varKey, ".") == -1 {
iv, err = NewUserVariable(key)
} else {
iv, err = NewUserMapVariable(key)
}
}
if err != nil {
return err
}
w.Variables[key] = iv
}
return nil
}
// variableReplaceWalker implements interfaces for reflectwalk that
// is used to replace variables with their values.
//
// If Values does not have every available value, then the program
// will _panic_. The variableDetectWalker will tell you all variables
// you need.
type variableReplaceWalker struct {
Variables map[string]InterpolatedVariable
Values map[string]string
UnknownKeys []string
key []string
loc reflectwalk.Location
cs []reflect.Value
csData interface{}
}
func (w *variableReplaceWalker) Enter(loc reflectwalk.Location) error {
w.loc = loc
return nil
}
func (w *variableReplaceWalker) 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 *variableReplaceWalker) Map(m reflect.Value) error {
w.cs = append(w.cs, m)
return nil
}
func (w *variableReplaceWalker) MapElem(m, k, v reflect.Value) error {
w.csData = k
w.key = append(w.key, k.String())
return nil
}
func (w *variableReplaceWalker) Primitive(v reflect.Value) error {
// We only care about strings
setV := v
if v.Kind() == reflect.Interface {
setV = v
v = v.Elem()
}
if v.Kind() != reflect.String {
return nil
}
matches := varRegexp.FindAllStringSubmatch(v.String(), -1)
if len(matches) == 0 {
return nil
}
result := v.String()
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
}
// Get the key
key := match[2]
value, ok := w.Values[key]
if !ok {
panic("no value for variable key: " + key)
}
// If this is an unknown variable, then we remove it from
// the configuration.
if value == UnknownVariableValue {
w.removeCurrent()
return nil
}
// Replace
result = strings.Replace(result, match[0], value, -1)
}
resultVal := reflect.ValueOf(result)
if w.loc == reflectwalk.MapValue {
// If we're in a map, then the only way to set a map value is
// to set it directly.
m := w.cs[len(w.cs)-1]
mk := w.csData.(reflect.Value)
m.SetMapIndex(mk, resultVal)
} else {
// Otherwise, we should be addressable
setV.Set(resultVal)
}
return nil
}
func (w *variableReplaceWalker) removeCurrent() {
c := w.cs[len(w.cs)-1]
switch c.Kind() {
case reflect.Map:
// Zero value so that we delete the map key
var val reflect.Value
// Get the key and delete it
k := w.csData.(reflect.Value)
c.SetMapIndex(k, val)
}
// Append the key to the unknown keys
w.UnknownKeys = append(w.UnknownKeys, strings.Join(w.key, "."))
}

View File

@ -1,306 +0,0 @@
package config
import (
"reflect"
"testing"
"github.com/mitchellh/reflectwalk"
)
func BenchmarkVariableDetectWalker(b *testing.B) {
w := new(variableDetectWalker)
str := reflect.ValueOf(`foo ${var.bar} bar ${bar.baz.bing} $${escaped}`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
w.Variables = nil
w.Primitive(str)
}
}
func BenchmarkVariableReplaceWalker(b *testing.B) {
w := &variableReplaceWalker{
Values: map[string]string{
"var.bar": "bar",
"bar.baz.bing": "baz",
},
}
str := `foo ${var.bar} bar ${bar.baz.bing} $${escaped}`
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := reflectwalk.Walk(&str, w); err != nil {
panic(err)
}
}
}
func TestReplaceVariables(t *testing.T) {
input := "foo-${var.bar}"
expected := "foo-bar"
unk, err := ReplaceVariables(&input, map[string]string{
"var.bar": "bar",
})
if err != nil {
t.Fatalf("err: %s", err)
}
if len(unk) > 0 {
t.Fatal("bad: %#v", unk)
}
if input != expected {
t.Fatalf("bad: %#v", input)
}
}
func TestVariableDetectWalker(t *testing.T) {
w := new(variableDetectWalker)
str := `foo ${var.bar}`
if err := w.Primitive(reflect.ValueOf(str)); err != nil {
t.Fatalf("err: %s", err)
}
if len(w.Variables) != 1 {
t.Fatalf("bad: %#v", w.Variables)
}
if w.Variables["var.bar"].(*UserVariable).FullKey() != "var.bar" {
t.Fatalf("bad: %#v", w.Variables)
}
}
func TestVariableDetectWalker_resource(t *testing.T) {
w := new(variableDetectWalker)
str := `foo ${ec2.foo.bar}`
if err := w.Primitive(reflect.ValueOf(str)); err != nil {
t.Fatalf("err: %s", err)
}
if len(w.Variables) != 1 {
t.Fatalf("bad: %#v", w.Variables)
}
if w.Variables["ec2.foo.bar"].(*ResourceVariable).FullKey() != "ec2.foo.bar" {
t.Fatalf("bad: %#v", w.Variables)
}
}
func TestVariableDetectWalker_resourceMulti(t *testing.T) {
w := new(variableDetectWalker)
str := `foo ${ec2.foo.*.bar}`
if err := w.Primitive(reflect.ValueOf(str)); err != nil {
t.Fatalf("err: %s", err)
}
if len(w.Variables) != 1 {
t.Fatalf("bad: %#v", w.Variables)
}
if w.Variables["ec2.foo.*.bar"].(*ResourceVariable).FullKey() != "ec2.foo.*.bar" {
t.Fatalf("bad: %#v", w.Variables)
}
}
func TestVariableDetectWalker_bad(t *testing.T) {
w := new(variableDetectWalker)
str := `foo ${bar}`
if err := w.Primitive(reflect.ValueOf(str)); err == nil {
t.Fatal("should error")
}
}
func TestVariableDetectWalker_escaped(t *testing.T) {
w := new(variableDetectWalker)
str := `foo $${var.bar}`
if err := w.Primitive(reflect.ValueOf(str)); err != nil {
t.Fatalf("err: %s", err)
}
if len(w.Variables) > 0 {
t.Fatalf("bad: %#v", w.Variables)
}
}
func TestVariableDetectWalker_empty(t *testing.T) {
w := new(variableDetectWalker)
str := `foo`
if err := w.Primitive(reflect.ValueOf(str)); err != nil {
t.Fatalf("err: %s", err)
}
if len(w.Variables) > 0 {
t.Fatalf("bad: %#v", w.Variables)
}
}
func TestVariableDetectWalker_userMap(t *testing.T) {
w := new(variableDetectWalker)
str := `foo ${var.foo.bar}`
if err := w.Primitive(reflect.ValueOf(str)); err != nil {
t.Fatalf("err: %s", err)
}
if len(w.Variables) != 1 {
t.Fatalf("bad: %#v", w.Variables)
}
v := w.Variables["var.foo.bar"].(*UserMapVariable)
if v.FullKey() != "var.foo.bar" {
t.Fatalf("bad: %#v", w.Variables)
}
if v.Name != "foo" {
t.Fatalf("bad: %#v", w.Variables)
}
if v.Elem != "bar" {
t.Fatalf("bad: %#v", w.Variables)
}
}
func TestVariableReplaceWalker(t *testing.T) {
w := &variableReplaceWalker{
Values: map[string]string{
"var.bar": "bar",
"var.unknown": UnknownVariableValue,
},
}
cases := []struct {
Input interface{}
Output interface{}
}{
{
`foo ${var.bar}`,
"foo bar",
},
{
[]string{"foo", "${var.bar}"},
[]string{"foo", "bar"},
},
{
map[string]interface{}{
"ami": "${var.bar}",
"security_groups": []interface{}{
"foo",
"${var.bar}",
},
},
map[string]interface{}{
"ami": "bar",
"security_groups": []interface{}{
"foo",
"bar",
},
},
},
{
map[string]interface{}{
"foo": map[string]interface{}{
"foo": []string{"${var.bar}"},
},
},
map[string]interface{}{
"foo": map[string]interface{}{
"foo": []string{"bar"},
},
},
},
{
map[string]interface{}{
"foo": "bar",
"bar": "hello${var.unknown}world",
},
map[string]interface{}{
"foo": "bar",
},
},
{
map[string]interface{}{
"foo": []string{"foo", "${var.unknown}", "bar"},
},
map[string]interface{}{},
},
}
for i, tc := range cases {
var input interface{} = tc.Input
if reflect.ValueOf(tc.Input).Kind() == reflect.String {
input = &tc.Input
}
if err := reflectwalk.Walk(input, w); err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(tc.Input, tc.Output) {
t.Fatalf("bad %d: %#v", i, tc.Input)
}
}
}
func TestVariableReplaceWalker_unknown(t *testing.T) {
cases := []struct {
Input interface{}
Output interface{}
Keys []string
}{
{
map[string]interface{}{
"foo": "bar",
"bar": "hello${var.unknown}world",
},
map[string]interface{}{
"foo": "bar",
},
[]string{"bar"},
},
{
map[string]interface{}{
"foo": []string{"foo", "${var.unknown}", "bar"},
},
map[string]interface{}{},
[]string{"foo"},
},
{
map[string]interface{}{
"foo": map[string]interface{}{
"bar": "${var.unknown}",
},
},
map[string]interface{}{
"foo": map[string]interface{}{},
},
[]string{"foo.bar"},
},
}
for i, tc := range cases {
var input interface{} = tc.Input
w := &variableReplaceWalker{
Values: map[string]string{
"var.unknown": UnknownVariableValue,
},
}
if reflect.ValueOf(tc.Input).Kind() == reflect.String {
input = &tc.Input
}
if err := reflectwalk.Walk(input, w); err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(tc.Input, tc.Output) {
t.Fatalf("bad %d: %#v", i, tc.Input)
}
if !reflect.DeepEqual(tc.Keys, w.UnknownKeys) {
t.Fatalf("bad: %#v", w.UnknownKeys)
}
}
}