Skip to content

Commit

Permalink
Merge pull request #1422 from hairyhenderson/renderer-interface
Browse files Browse the repository at this point in the history
New experimental gomplate.Renderer API
  • Loading branch information
hairyhenderson committed Jun 12, 2022
2 parents 0158ecc + 13b0d86 commit a7685b1
Show file tree
Hide file tree
Showing 10 changed files with 555 additions and 235 deletions.
1 change: 1 addition & 0 deletions data/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func FromConfig(ctx context.Context, cfg *config.Config) *Data {
}

// Source - a data source
// Deprecated: will be replaced in future
type Source struct {
Alias string
URL *url.URL
Expand Down
5 changes: 3 additions & 2 deletions docs/content/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,9 @@ Hello World

All arguments provided to the function will be passed as positional arguments to
the plugin, and the plugin's standard output stream (`Stdout`) will be printed
to the rendered output. Currently there is no way to set the plugin's standard
input stream (`Stdin`).
to the rendered output. To instead pipe the final argument of the function to
the plugin's standard input stream, use the [config file](../config/#plugins)
and set the `pipe` field.

If the plugin exits with a non-zero exit code, gomplate will also fail. All signals
caught by gomplate will be propagated to the plugin. Any output on the standard
Expand Down
114 changes: 29 additions & 85 deletions gomplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"text/template"
Expand All @@ -16,47 +14,11 @@ import (
"github.com/hairyhenderson/gomplate/v3/data"
"github.com/hairyhenderson/gomplate/v3/internal/config"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)

// gomplate -
type gomplate struct {
tmplctx interface{}
funcMap template.FuncMap
nestedTemplates config.Templates

leftDelim, rightDelim string
}

// runTemplate -
func (g *gomplate) runTemplate(ctx context.Context, t *tplate) error {
tmpl, err := t.toGoTemplate(ctx, g)
if err != nil {
return err
}

wr, ok := t.target.(io.Closer)
if ok && wr != os.Stdout {
defer wr.Close()
}

return tmpl.Execute(t.target, g.tmplctx)
}

// newGomplate -
func newGomplate(funcMap template.FuncMap, leftDelim, rightDelim string, nested config.Templates, tctx interface{}) *gomplate {
return &gomplate{
leftDelim: leftDelim,
rightDelim: rightDelim,
funcMap: funcMap,
nestedTemplates: nested,
tmplctx: tctx,
}
}

// RunTemplates - run all gomplate templates specified by the given configuration
//
// Deprecated: use Run instead
// Deprecated: use the Renderer interface instead
func RunTemplates(o *Config) error {
cfg, err := o.toNewConfig()
if err != nil {
Expand All @@ -67,8 +29,6 @@ func RunTemplates(o *Config) error {

// Run all gomplate templates specified by the given configuration
func Run(ctx context.Context, cfg *config.Config) error {
log := zerolog.Ctx(ctx)

Metrics = newMetrics()
defer runCleanupHooks()

Expand All @@ -80,59 +40,43 @@ func Run(ctx context.Context, cfg *config.Config) error {
return fmt.Errorf("failed to validate config: %w\n%+v", err, cfg)
}

d := data.FromConfig(ctx, cfg)
log.Debug().Str("data", fmt.Sprintf("%+v", d)).Msg("created data from config")

addCleanupHook(d.Cleanup)

aliases := []string{}
for k := range cfg.Context {
aliases = append(aliases, k)
}
c, err := createTmplContext(ctx, aliases, d)
if err != nil {
return err
}

funcMap := CreateFuncs(ctx, d)
funcMap := template.FuncMap{}
err = bindPlugins(ctx, cfg, funcMap)
if err != nil {
return err
}
g := newGomplate(funcMap, cfg.LDelim, cfg.RDelim, cfg.Templates, c)

return g.runTemplates(ctx, cfg)
}
// if a custom Stdin is set in the config, inject it into the context now
ctx = data.ContextWithStdin(ctx, cfg.Stdin)

opts := optionsFromConfig(cfg)
opts.Funcs = funcMap
tr := NewRenderer(opts)

func (g *gomplate) runTemplates(ctx context.Context, cfg *config.Config) error {
start := time.Now()
tmpl, err := gatherTemplates(ctx, cfg, chooseNamer(cfg, g))

namer := chooseNamer(cfg, tr)
tmpl, err := gatherTemplates(ctx, cfg, namer)
Metrics.GatherDuration = time.Since(start)
if err != nil {
Metrics.Errors++
return fmt.Errorf("failed to gather templates for rendering: %w", err)
}
Metrics.TemplatesGathered = len(tmpl)
start = time.Now()
defer func() { Metrics.TotalRenderDuration = time.Since(start) }()
for _, t := range tmpl {
tstart := time.Now()
err := g.runTemplate(ctx, t)
Metrics.RenderDuration[t.name] = time.Since(tstart)
if err != nil {
Metrics.Errors++
return fmt.Errorf("failed to render template %s: %w", t.name, err)
}
Metrics.TemplatesProcessed++

err = tr.RenderTemplates(ctx, tmpl)
if err != nil {
return err
}

return nil
}

func chooseNamer(cfg *config.Config, g *gomplate) func(context.Context, string) (string, error) {
func chooseNamer(cfg *config.Config, tr *Renderer) func(context.Context, string) (string, error) {
if cfg.OutputMap == "" {
return simpleNamer(cfg.OutputDir)
}
return mappingNamer(cfg.OutputMap, g)
return mappingNamer(cfg.OutputMap, tr)
}

func simpleNamer(outDir string) func(ctx context.Context, inPath string) (string, error) {
Expand All @@ -142,32 +86,32 @@ func simpleNamer(outDir string) func(ctx context.Context, inPath string) (string
}
}

func mappingNamer(outMap string, g *gomplate) func(context.Context, string) (string, error) {
func mappingNamer(outMap string, tr *Renderer) func(context.Context, string) (string, error) {
return func(ctx context.Context, inPath string) (string, error) {
out := &bytes.Buffer{}
t := &tplate{
name: "<OutputMap>",
contents: outMap,
target: out,
}
tpl, err := t.toGoTemplate(ctx, g)
tr.data.Ctx = ctx
tcontext, err := createTmplContext(ctx, tr.tctxAliases, tr.data)
if err != nil {
return "", err
}

// add '.in' to the template context and preserve the original context
// in '.ctx'
tctx := &tmplctx{}
// nolint: gocritic
switch c := g.tmplctx.(type) {
switch c := tcontext.(type) {
case *tmplctx:
for k, v := range *c {
if k != "in" && k != "ctx" {
(*tctx)[k] = v
}
}
}
(*tctx)["ctx"] = g.tmplctx
(*tctx)["ctx"] = tcontext
(*tctx)["in"] = inPath

err = tpl.Execute(t.target, tctx)
out := &bytes.Buffer{}
err = tr.renderTemplatesWithData(ctx,
[]Template{{Name: "<OutputMap>", Text: outMap, Writer: out}}, tctx)
if err != nil {
return "", errors.Wrapf(err, "failed to render outputMap with ctx %+v and inPath %s", tctx, inPath)
}
Expand Down
78 changes: 40 additions & 38 deletions gomplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,44 @@ import (
"github.com/stretchr/testify/assert"
)

func testTemplate(t *testing.T, g *gomplate, tmpl string) string {
func testTemplate(t *testing.T, tr *Renderer, tmpl string) string {
t.Helper()

var out bytes.Buffer
err := g.runTemplate(context.Background(), &tplate{name: "testtemplate", contents: tmpl, target: &out})
err := tr.Render(context.Background(), "testtemplate", tmpl, &out)
assert.NoError(t, err)

return out.String()
}

func TestGetenvTemplates(t *testing.T) {
g := &gomplate{
funcMap: template.FuncMap{
tr := NewRenderer(Options{
Funcs: template.FuncMap{
"getenv": env.Getenv,
"bool": conv.Bool,
},
}
assert.Empty(t, testTemplate(t, g, `{{getenv "BLAHBLAHBLAH"}}`))
assert.Equal(t, os.Getenv("USER"), testTemplate(t, g, `{{getenv "USER"}}`))
assert.Equal(t, "default value", testTemplate(t, g, `{{getenv "BLAHBLAHBLAH" "default value"}}`))
})
assert.Empty(t, testTemplate(t, tr, `{{getenv "BLAHBLAHBLAH"}}`))
assert.Equal(t, os.Getenv("USER"), testTemplate(t, tr, `{{getenv "USER"}}`))
assert.Equal(t, "default value", testTemplate(t, tr, `{{getenv "BLAHBLAHBLAH" "default value"}}`))
}

func TestBoolTemplates(t *testing.T) {
g := &gomplate{
funcMap: template.FuncMap{
g := NewRenderer(Options{
Funcs: template.FuncMap{
"bool": conv.Bool,
},
}
})
assert.Equal(t, "true", testTemplate(t, g, `{{bool "true"}}`))
assert.Equal(t, "false", testTemplate(t, g, `{{bool "false"}}`))
assert.Equal(t, "false", testTemplate(t, g, `{{bool "foo"}}`))
assert.Equal(t, "false", testTemplate(t, g, `{{bool ""}}`))
}

func TestEc2MetaTemplates(t *testing.T) {
createGomplate := func(data map[string]string, region string) *gomplate {
createGomplate := func(data map[string]string, region string) *Renderer {
ec2meta := aws.MockEC2Meta(data, nil, region)
return &gomplate{funcMap: template.FuncMap{"ec2meta": ec2meta.Meta}}
return NewRenderer(Options{Funcs: template.FuncMap{"ec2meta": ec2meta.Meta}})
}

g := createGomplate(nil, "")
Expand All @@ -69,60 +69,60 @@ func TestEc2MetaTemplates(t *testing.T) {
func TestEc2MetaTemplates_WithJSON(t *testing.T) {
ec2meta := aws.MockEC2Meta(map[string]string{"obj": `"foo": "bar"`}, map[string]string{"obj": `"foo": "baz"`}, "")

g := &gomplate{
funcMap: template.FuncMap{
g := NewRenderer(Options{
Funcs: template.FuncMap{
"ec2meta": ec2meta.Meta,
"ec2dynamic": ec2meta.Dynamic,
"json": data.JSON,
},
}
})

assert.Equal(t, "bar", testTemplate(t, g, `{{ (ec2meta "obj" | json).foo }}`))
assert.Equal(t, "baz", testTemplate(t, g, `{{ (ec2dynamic "obj" | json).foo }}`))
}

func TestJSONArrayTemplates(t *testing.T) {
g := &gomplate{
funcMap: template.FuncMap{
g := NewRenderer(Options{
Funcs: template.FuncMap{
"jsonArray": data.JSONArray,
},
}
})

assert.Equal(t, "[foo bar]", testTemplate(t, g, `{{jsonArray "[\"foo\",\"bar\"]"}}`))
assert.Equal(t, "bar", testTemplate(t, g, `{{ index (jsonArray "[\"foo\",\"bar\"]") 1 }}`))
}

func TestYAMLTemplates(t *testing.T) {
g := &gomplate{
funcMap: template.FuncMap{
g := NewRenderer(Options{
Funcs: template.FuncMap{
"yaml": data.YAML,
"yamlArray": data.YAMLArray,
},
}
})

assert.Equal(t, "bar", testTemplate(t, g, `{{(yaml "foo: bar").foo}}`))
assert.Equal(t, "[foo bar]", testTemplate(t, g, `{{yamlArray "- foo\n- bar\n"}}`))
assert.Equal(t, "bar", testTemplate(t, g, `{{ index (yamlArray "[\"foo\",\"bar\"]") 1 }}`))
}

func TestSliceTemplates(t *testing.T) {
g := &gomplate{
funcMap: template.FuncMap{
g := NewRenderer(Options{
Funcs: template.FuncMap{
"slice": conv.Slice,
},
}
})
assert.Equal(t, "foo", testTemplate(t, g, `{{index (slice "foo") 0}}`))
assert.Equal(t, `[foo bar 42]`, testTemplate(t, g, `{{slice "foo" "bar" 42}}`))
assert.Equal(t, `helloworld`, testTemplate(t, g, `{{range slice "hello" "world"}}{{.}}{{end}}`))
}

func TestHasTemplate(t *testing.T) {
g := &gomplate{
funcMap: template.FuncMap{
g := NewRenderer(Options{
Funcs: template.FuncMap{
"yaml": data.YAML,
"has": conv.Has,
},
}
})
assert.Equal(t, "true", testTemplate(t, g, `{{has ("foo:\n bar: true" | yaml) "foo"}}`))
assert.Equal(t, "true", testTemplate(t, g, `{{has ("foo:\n bar: true" | yaml).foo "bar"}}`))
assert.Equal(t, "false", testTemplate(t, g, `{{has ("foo: true" | yaml) "bah"}}`))
Expand All @@ -141,11 +141,10 @@ func TestHasTemplate(t *testing.T) {
}

func TestCustomDelim(t *testing.T) {
g := &gomplate{
leftDelim: "[",
rightDelim: "]",
funcMap: template.FuncMap{},
}
g := NewRenderer(Options{
LDelim: "[",
RDelim: "]",
})
assert.Equal(t, "hi", testTemplate(t, g, `[print "hi"]`))
}

Expand All @@ -170,16 +169,19 @@ func TestSimpleNamer(t *testing.T) {

func TestMappingNamer(t *testing.T) {
ctx := context.Background()
g := &gomplate{funcMap: map[string]interface{}{
"foo": func() string { return "foo" },
}}
n := mappingNamer("out/{{ .in }}", g)
tr := &Renderer{
data: &data.Data{},
funcs: map[string]interface{}{
"foo": func() string { return "foo" },
},
}
n := mappingNamer("out/{{ .in }}", tr)
out, err := n(ctx, "file")
assert.NoError(t, err)
expected := filepath.FromSlash("out/file")
assert.Equal(t, expected, out)

n = mappingNamer("out/{{ foo }}{{ .in }}", g)
n = mappingNamer("out/{{ foo }}{{ .in }}", tr)
out, err = n(ctx, "file")
assert.NoError(t, err)
expected = filepath.FromSlash("out/foofile")
Expand Down

0 comments on commit a7685b1

Please sign in to comment.