From 41a6fe9fda564e4add99254d3f1782a46fd4b2ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 May 2015 10:07:51 -0700 Subject: [PATCH] template/interpolate: RenderMap to render a complex structure --- template/interpolate/render.go | 273 ++++++++++++++++++++++++++++ template/interpolate/render_test.go | 92 ++++++++++ 2 files changed, 365 insertions(+) create mode 100644 template/interpolate/render.go create mode 100644 template/interpolate/render_test.go diff --git a/template/interpolate/render.go b/template/interpolate/render.go new file mode 100644 index 000000000..0225f762c --- /dev/null +++ b/template/interpolate/render.go @@ -0,0 +1,273 @@ +package interpolate + +import ( + "fmt" + "reflect" + "strings" + "sync" + + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/reflectwalk" +) + +// RenderFilter is an option for filtering what gets rendered and +// doesn't within an interface. +type RenderFilter struct { + Include []string + + once sync.Once + includeSet map[string]struct{} +} + +// RenderMap renders all the strings in the given interface. The +// interface must decode into a map[string]interface{}, but is left +// as an interface{} type to ease backwards compatibility with the way +// arguments are passed around in Packer. +func RenderMap(v interface{}, ctx *Context, f *RenderFilter) (map[string]interface{}, error) { + // First decode it into the map + var m map[string]interface{} + if err := mapstructure.Decode(v, &m); err != nil { + return nil, err + } + + // Now go through each value and render it + for k, raw := range m { + if !f.include(k) { + continue + } + + raw, err := renderInterface(raw, ctx) + if err != nil { + return nil, fmt.Errorf("render '%s': %s", k, err) + } + + m[k] = raw + } + + return m, nil +} + +func renderInterface(v interface{}, ctx *Context) (interface{}, error) { + f := func(v string) (string, error) { + return Render(v, ctx) + } + + walker := &renderWalker{ + F: f, + Replace: true, + } + err := reflectwalk.Walk(v, walker) + if err != nil { + return nil, err + } + + if walker.Top != nil { + v = walker.Top + } + return v, nil +} + +// Include checks whether a key should be included. +func (f *RenderFilter) include(k string) bool { + if f == nil { + return true + } + + f.once.Do(f.init) + _, ok := f.includeSet[k] + return ok +} + +func (f *RenderFilter) init() { + f.includeSet = make(map[string]struct{}) + for _, v := range f.Include { + f.includeSet[v] = struct{}{} + } +} + +// renderWalker implements interfaces for the reflectwalk package +// (github.com/mitchellh/reflectwalk) that can be used to automatically +// execute a callback for an interpolation. +type renderWalker struct { + // F is the function to call for every interpolation. It can be nil. + // + // If Replace is true, then the return value of F will be used to + // replace the interpolation. + F renderWalkerFunc + Replace bool + + // ContextF is an advanced version of F that also receives the + // location of where it is in the structure. This lets you do + // context-aware validation. + ContextF renderWalkerContextFunc + + // Top is the top value of the walk. This might get replaced if the + // top value needs to be modified. It is valid to read after any walk. + // If it is nil, it means the top wasn't replaced. + Top interface{} + + key []string + lastValue reflect.Value + loc reflectwalk.Location + cs []reflect.Value + csKey []reflect.Value + csData interface{} + sliceIndex int + unknownKeys []string +} + +// renderWalkerFunc is the callback called by interpolationWalk. +// It is called with any interpolation found. It should return a value +// to replace the interpolation with, along with any errors. +// +// If Replace is set to false in renderWalker, then the replace +// value can be anything as it will have no effect. +type renderWalkerFunc func(string) (string, error) + +// renderWalkerContextFunc is called by interpolationWalk if +// ContextF is set. This receives both the interpolation and the location +// where the interpolation is. +// +// This callback can be used to validate the location of the interpolation +// within the configuration. +type renderWalkerContextFunc func(reflectwalk.Location, string) + +func (w *renderWalker) Enter(loc reflectwalk.Location) error { + w.loc = loc + return nil +} + +func (w *renderWalker) 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] + w.csKey = w.csKey[:len(w.csKey)-1] + case reflectwalk.Slice: + // Split any values that need to be split + w.cs = w.cs[:len(w.cs)-1] + case reflectwalk.SliceElem: + w.csKey = w.csKey[:len(w.csKey)-1] + } + + return nil +} + +func (w *renderWalker) Map(m reflect.Value) error { + w.cs = append(w.cs, m) + return nil +} + +func (w *renderWalker) MapElem(m, k, v reflect.Value) error { + w.csData = k + w.csKey = append(w.csKey, k) + w.key = append(w.key, k.String()) + w.lastValue = v + return nil +} + +func (w *renderWalker) Slice(s reflect.Value) error { + w.cs = append(w.cs, s) + return nil +} + +func (w *renderWalker) SliceElem(i int, elem reflect.Value) error { + w.csKey = append(w.csKey, reflect.ValueOf(i)) + w.sliceIndex = i + return nil +} + +func (w *renderWalker) Primitive(v reflect.Value) error { + setV := v + + // We only care about strings + if v.Kind() == reflect.Interface { + setV = v + v = v.Elem() + } + if v.Kind() != reflect.String { + return nil + } + + strV := v.String() + if w.ContextF != nil { + w.ContextF(w.loc, strV) + } + + if w.F == nil { + return nil + } + + replaceVal, err := w.F(strV) + if err != nil { + return fmt.Errorf( + "%s in:\n\n%s", + err, v.String()) + } + + if w.Replace { + resultVal := reflect.ValueOf(replaceVal) + switch w.loc { + case reflectwalk.MapKey: + m := w.cs[len(w.cs)-1] + + // Delete the old value + var zero reflect.Value + m.SetMapIndex(w.csData.(reflect.Value), zero) + + // Set the new key with the existing value + m.SetMapIndex(resultVal, w.lastValue) + + // Set the key to be the new key + w.csData = resultVal + case 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) + case reflectwalk.WalkLoc: + // At the root element, we can't write that, so we just save it + w.Top = resultVal.Interface() + default: + // Otherwise, we should be addressable + setV.Set(resultVal) + } + } + + return nil +} + +func (w *renderWalker) removeCurrent() { + // Append the key to the unknown keys + w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, ".")) + + for i := 1; i <= len(w.cs); i++ { + c := w.cs[len(w.cs)-i] + 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) + return + } + } + + panic("No container found for removeCurrent") +} + +func (w *renderWalker) replaceCurrent(v reflect.Value) { + c := w.cs[len(w.cs)-2] + switch c.Kind() { + case reflect.Map: + // Get the key and delete it + k := w.csKey[len(w.csKey)-1] + c.SetMapIndex(k, v) + } +} diff --git a/template/interpolate/render_test.go b/template/interpolate/render_test.go new file mode 100644 index 000000000..f6e466029 --- /dev/null +++ b/template/interpolate/render_test.go @@ -0,0 +1,92 @@ +package interpolate + +import ( + "reflect" + "testing" +) + +func TestRenderMap(t *testing.T) { + cases := map[string]struct { + Input interface{} + Output interface{} + Filter *RenderFilter + }{ + "basic": { + map[string]interface{}{ + "foo": "{{upper `bar`}}", + }, + map[string]interface{}{ + "foo": "BAR", + }, + nil, + }, + + "map keys shouldn't be interpolated": { + map[string]interface{}{ + "{{foo}}": "{{upper `bar`}}", + }, + map[string]interface{}{ + "{{foo}}": "BAR", + }, + nil, + }, + + "nested values": { + map[string]interface{}{ + "foo": map[string]string{ + "bar": "{{upper `baz`}}", + }, + }, + map[string]interface{}{ + "foo": map[string]string{ + "bar": "BAZ", + }, + }, + nil, + }, + + "nested value keys": { + map[string]interface{}{ + "foo": map[string]string{ + "{{upper `bar`}}": "{{upper `baz`}}", + }, + }, + map[string]interface{}{ + "foo": map[string]string{ + "BAR": "BAZ", + }, + }, + nil, + }, + + "filter": { + map[string]interface{}{ + "bar": "{{upper `baz`}}", + "foo": map[string]string{ + "{{upper `bar`}}": "{{upper `baz`}}", + }, + }, + map[string]interface{}{ + "bar": "BAZ", + "foo": map[string]string{ + "{{upper `bar`}}": "{{upper `baz`}}", + }, + }, + &RenderFilter{ + Include: []string{"bar"}, + }, + }, + } + + ctx := &Context{} + for k, tc := range cases { + actual, err := RenderMap(tc.Input, ctx, tc.Filter) + if err != nil { + t.Fatalf("err: %s\n\n%s", k, err) + } + + if !reflect.DeepEqual(actual, tc.Output) { + t.Fatalf("err: %s\n\n%#v\n\n%#v", k, actual, tc.Output) + } + } +}