From 9751def7add406859fcdd76614b396497ac5c0b7 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Wed, 29 May 2024 20:38:08 -0400 Subject: [PATCH] chore: add files missed from PR 2083 Signed-off-by: Dave Henderson --- gomplate.go | 6 +- gomplate_test.go | 24 +++--- render.go | 189 +++++++++++++++++++++++++++++++++++++++++------ template.go | 132 --------------------------------- template_test.go | 7 +- 5 files changed, 185 insertions(+), 173 deletions(-) diff --git a/gomplate.go b/gomplate.go index 1e27f2533..61bda1fb3 100644 --- a/gomplate.go +++ b/gomplate.go @@ -49,7 +49,7 @@ func Run(ctx context.Context, cfg *config.Config) error { opts := optionsFromConfig(cfg) opts.Funcs = funcMap - tr := NewRenderer(opts) + tr := newRenderer(opts) start := time.Now() @@ -70,7 +70,7 @@ func Run(ctx context.Context, cfg *config.Config) error { return nil } -func chooseNamer(cfg *config.Config, tr *Renderer) 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) } @@ -84,7 +84,7 @@ func simpleNamer(outDir string) func(ctx context.Context, inPath string) (string } } -func mappingNamer(outMap string, tr *Renderer) 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) { tcontext, err := createTmplContext(ctx, tr.tctxAliases, tr.sr) if err != nil { diff --git a/gomplate_test.go b/gomplate_test.go index e9b885cf4..5bca78cc7 100644 --- a/gomplate_test.go +++ b/gomplate_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/require" ) -func testTemplate(t *testing.T, tr *Renderer, tmpl string) string { +func testTemplate(t *testing.T, tr *renderer, tmpl string) string { t.Helper() var out bytes.Buffer @@ -29,7 +29,7 @@ func testTemplate(t *testing.T, tr *Renderer, tmpl string) string { } func TestGetenvTemplates(t *testing.T) { - tr := NewRenderer(Options{ + tr := newRenderer(Options{ Funcs: template.FuncMap{ "getenv": env.Getenv, "bool": conv.ToBool, @@ -41,7 +41,7 @@ func TestGetenvTemplates(t *testing.T) { } func TestBoolTemplates(t *testing.T) { - g := NewRenderer(Options{ + g := newRenderer(Options{ Funcs: template.FuncMap{ "bool": conv.ToBool, }, @@ -53,9 +53,9 @@ func TestBoolTemplates(t *testing.T) { } func TestEc2MetaTemplates(t *testing.T) { - createGomplate := func(data map[string]string, region string) *Renderer { + createGomplate := func(data map[string]string, region string) *renderer { ec2meta := aws.MockEC2Meta(data, nil, region) - return NewRenderer(Options{Funcs: template.FuncMap{"ec2meta": ec2meta.Meta}}) + return newRenderer(Options{Funcs: template.FuncMap{"ec2meta": ec2meta.Meta}}) } g := createGomplate(nil, "") @@ -70,7 +70,7 @@ 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 := NewRenderer(Options{ + g := newRenderer(Options{ Funcs: template.FuncMap{ "ec2meta": ec2meta.Meta, "ec2dynamic": ec2meta.Dynamic, @@ -83,7 +83,7 @@ func TestEc2MetaTemplates_WithJSON(t *testing.T) { } func TestJSONArrayTemplates(t *testing.T) { - g := NewRenderer(Options{ + g := newRenderer(Options{ Funcs: template.FuncMap{ "jsonArray": parsers.JSONArray, }, @@ -94,7 +94,7 @@ func TestJSONArrayTemplates(t *testing.T) { } func TestYAMLTemplates(t *testing.T) { - g := NewRenderer(Options{ + g := newRenderer(Options{ Funcs: template.FuncMap{ "yaml": parsers.YAML, "yamlArray": parsers.YAMLArray, @@ -107,7 +107,7 @@ func TestYAMLTemplates(t *testing.T) { } func TestHasTemplate(t *testing.T) { - g := NewRenderer(Options{ + g := newRenderer(Options{ Funcs: template.FuncMap{ "yaml": parsers.YAML, "has": conv.Has, @@ -141,7 +141,7 @@ func TestMissingKey(t *testing.T) { } for name, tt := range tests { t.Run(name, func(t *testing.T) { - g := NewRenderer(Options{ + g := newRenderer(Options{ MissingKey: tt.MissingKey, }) tmpl := `{{ .name }}` @@ -151,7 +151,7 @@ func TestMissingKey(t *testing.T) { } func TestCustomDelim(t *testing.T) { - g := NewRenderer(Options{ + g := newRenderer(Options{ LDelim: "[", RDelim: "]", }) @@ -180,7 +180,7 @@ func TestSimpleNamer(t *testing.T) { func TestMappingNamer(t *testing.T) { ctx := context.Background() reg := datafs.NewRegistry() - tr := &Renderer{ + tr := &renderer{ sr: datafs.NewSourceReader(reg), funcs: map[string]interface{}{ "foo": func() string { return "foo" }, diff --git a/render.go b/render.go index fe0d7f74a..cf55e06ea 100644 --- a/render.go +++ b/render.go @@ -4,8 +4,12 @@ import ( "context" "fmt" "io" + "io/fs" "net/http" "net/url" + "path" + "slices" + "strings" "sync" "text/template" "time" @@ -103,11 +107,7 @@ type Datasource struct { Header http.Header } -// Renderer provides gomplate's core template rendering functionality. -// It should be initialized with NewRenderer. -// -// Experimental: subject to breaking changes before the next major release -type Renderer struct { +type renderer struct { sr datafs.DataSourceReader fsp fsimpl.FSProvider nested config.Templates @@ -118,12 +118,33 @@ type Renderer struct { tctxAliases []string } +// Renderer provides gomplate's core template rendering functionality. +// See [NewRenderer]. +// +// Experimental: subject to breaking changes before the next major release +type Renderer interface { + // RenderTemplates renders a list of templates, parsing each template's + // Text and executing it, outputting to its Writer. If a template's Writer + // is a non-[os.Stdout] [io.Closer], it will be closed after the template is + // rendered. + RenderTemplates(ctx context.Context, templates []Template) error + + // Render is a convenience method for rendering a single template. For more + // than one template, use [Renderer.RenderTemplates]. If wr is a non-[os.Stdout] + // [io.Closer], it will be closed after the template is rendered. + Render(ctx context.Context, name, text string, wr io.Writer) error +} + // NewRenderer creates a new template renderer with the specified options. // The returned renderer can be reused, but it is not (yet) safe for concurrent // use. // // Experimental: subject to breaking changes before the next major release -func NewRenderer(opts Options) *Renderer { +func NewRenderer(opts Options) Renderer { + return newRenderer(opts) +} + +func newRenderer(opts Options) *renderer { if Metrics == nil { Metrics = newMetrics() } @@ -177,7 +198,7 @@ func NewRenderer(opts Options) *Renderer { // TODO: move this in? sr := datafs.NewSourceReader(reg) - return &Renderer{ + return &renderer{ nested: nested, sr: sr, funcs: opts.Funcs, @@ -203,12 +224,7 @@ type Template struct { Text string } -// RenderTemplates renders a list of templates, parsing each template's Text -// and executing it, outputting to its Writer. If a template's Writer is a -// non-os.Stdout io.Closer, it will be closed after the template is rendered. -// -// Experimental: subject to breaking changes before the next major release -func (t *Renderer) RenderTemplates(ctx context.Context, templates []Template) error { +func (t *renderer) RenderTemplates(ctx context.Context, templates []Template) error { if datafs.FSProviderFromContext(ctx) == nil { ctx = datafs.ContextWithFSProvider(ctx, t.fsp) } @@ -223,7 +239,7 @@ func (t *Renderer) RenderTemplates(ctx context.Context, templates []Template) er return t.renderTemplatesWithData(ctx, templates, tmplctx) } -func (t *Renderer) renderTemplatesWithData(ctx context.Context, templates []Template, tmplctx interface{}) error { +func (t *renderer) renderTemplatesWithData(ctx context.Context, templates []Template, tmplctx interface{}) error { // update funcs with the current context // only done here to ensure the context is properly set in func namespaces f := CreateFuncs(ctx) @@ -246,7 +262,7 @@ func (t *Renderer) renderTemplatesWithData(ctx context.Context, templates []Temp return nil } -func (t *Renderer) renderTemplate(ctx context.Context, template Template, f template.FuncMap, tmplctx interface{}) error { +func (t *renderer) renderTemplate(ctx context.Context, template Template, f template.FuncMap, tmplctx interface{}) error { if template.Writer != nil { if wr, ok := template.Writer.(io.Closer); ok { defer wr.Close() @@ -254,8 +270,7 @@ func (t *Renderer) renderTemplate(ctx context.Context, template Template, f temp } tstart := time.Now() - tmpl, err := parseTemplate(ctx, template.Name, template.Text, - f, tmplctx, t.nested, t.lDelim, t.rDelim, t.missingKey) + tmpl, err := t.parseTemplate(ctx, template.Name, template.Text, f, tmplctx) if err != nil { return fmt.Errorf("parse template %s: %w", template.Name, err) } @@ -271,19 +286,145 @@ func (t *Renderer) renderTemplate(ctx context.Context, template Template, f temp return nil } -// Render is a convenience method for rendering a single template. For more -// than one template, use RenderTemplates. If wr is a non-os.Stdout -// io.Closer, it will be closed after the template is rendered. -// -// Experimental: subject to breaking changes before the next major release -func (t *Renderer) Render(ctx context.Context, name, text string, wr io.Writer) error { +func (t *renderer) Render(ctx context.Context, name, text string, wr io.Writer) error { return t.RenderTemplates(ctx, []Template{ {Name: name, Text: text, Writer: wr}, }) } +// parseTemplate - parses text as a Go template with the given name and options +func (t *renderer) parseTemplate(ctx context.Context, name, text string, funcs template.FuncMap, tmplctx interface{}) (tmpl *template.Template, err error) { + tmpl = template.New(name) + + missingKey := t.missingKey + if missingKey == "" { + missingKey = "error" + } + + missingKeyValues := []string{"error", "zero", "default", "invalid"} + if !slices.Contains(missingKeyValues, missingKey) { + return nil, fmt.Errorf("not allowed value for the 'missing-key' flag: %s. Allowed values: %s", missingKey, strings.Join(missingKeyValues, ",")) + } + + tmpl.Option("missingkey=" + missingKey) + + funcMap := copyFuncMap(funcs) + + // the "tmpl" funcs get added here because they need access to the root template and context + addTmplFuncs(funcMap, tmpl, tmplctx, name) + tmpl.Funcs(funcMap) + tmpl.Delims(t.lDelim, t.rDelim) + _, err = tmpl.Parse(text) + if err != nil { + return nil, err + } + + err = t.parseNestedTemplates(ctx, tmpl) + if err != nil { + return nil, fmt.Errorf("parse nested templates: %w", err) + } + + return tmpl, nil +} + +func (t *renderer) parseNestedTemplates(ctx context.Context, tmpl *template.Template) error { + fsp := datafs.FSProviderFromContext(ctx) + + for alias, n := range t.nested { + u := *n.URL + + fname := path.Base(u.Path) + if strings.HasSuffix(u.Path, "/") { + fname = "." + } + + u.Path = path.Dir(u.Path) + + fsys, err := fsp.New(&u) + if err != nil { + return fmt.Errorf("filesystem provider for %q unavailable: %w", &u, err) + } + + // TODO: maybe need to do something with root here? + _, reldir, err := datafs.ResolveLocalPath(fsys, u.Path) + if err != nil { + return fmt.Errorf("resolveLocalPath: %w", err) + } + + if reldir != "" && reldir != "." { + fsys, err = fs.Sub(fsys, reldir) + if err != nil { + return fmt.Errorf("sub filesystem for %q unavailable: %w", &u, err) + } + } + + // inject context & header in case they're useful... + fsys = fsimpl.WithContextFS(ctx, fsys) + fsys = fsimpl.WithHeaderFS(n.Header, fsys) + fsys = datafs.WithDataSourceRegistryFS(t.sr, fsys) + + // valid fs.FS paths have no trailing slash + fname = strings.TrimRight(fname, "/") + + // first determine if the template path is a directory, in which case we + // need to load all the files in the directory (but not recursively) + fi, err := fs.Stat(fsys, fname) + if err != nil { + return fmt.Errorf("stat %q: %w", fname, err) + } + + if fi.IsDir() { + err = parseNestedTemplateDir(ctx, fsys, alias, fname, tmpl) + } else { + err = parseNestedTemplate(ctx, fsys, alias, fname, tmpl) + } + + if err != nil { + return err + } + } + + return nil +} + +func parseNestedTemplateDir(ctx context.Context, fsys fs.FS, alias, fname string, tmpl *template.Template) error { + files, err := fs.ReadDir(fsys, fname) + if err != nil { + return fmt.Errorf("readDir %q: %w", fname, err) + } + + for _, f := range files { + if !f.IsDir() { + err = parseNestedTemplate(ctx, fsys, + path.Join(alias, f.Name()), + path.Join(fname, f.Name()), + tmpl, + ) + if err != nil { + return err + } + } + } + + return nil +} + +func parseNestedTemplate(_ context.Context, fsys fs.FS, alias, fname string, tmpl *template.Template) error { + b, err := fs.ReadFile(fsys, fname) + if err != nil { + return fmt.Errorf("readFile %q: %w", fname, err) + } + + _, err = tmpl.New(alias).Parse(string(b)) + if err != nil { + return fmt.Errorf("parse nested template %q: %w", fname, err) + } + + return nil +} + // DefaultFSProvider is the default filesystem provider used by gomplate -var DefaultFSProvider = sync.OnceValue[fsimpl.FSProvider]( +var DefaultFSProvider = sync.OnceValue( func() fsimpl.FSProvider { fsp := fsimpl.NewMux() diff --git a/template.go b/template.go index 812aafe42..bb8976cfd 100644 --- a/template.go +++ b/template.go @@ -7,14 +7,10 @@ import ( "io" "io/fs" "os" - "path" "path/filepath" - "slices" - "strings" "text/template" "github.com/hack-pad/hackpadfs" - "github.com/hairyhenderson/go-fsimpl" "github.com/hairyhenderson/gomplate/v4/internal/config" "github.com/hairyhenderson/gomplate/v4/internal/datafs" "github.com/hairyhenderson/gomplate/v4/internal/iohelpers" @@ -48,134 +44,6 @@ func copyFuncMap(funcMap template.FuncMap) template.FuncMap { return newFuncMap } -// parseTemplate - parses text as a Go template with the given name and options -func parseTemplate(ctx context.Context, name, text string, funcs template.FuncMap, tmplctx interface{}, nested config.Templates, leftDelim, rightDelim string, missingKey string) (tmpl *template.Template, err error) { - tmpl = template.New(name) - if missingKey == "" { - missingKey = "error" - } - - missingKeyValues := []string{"error", "zero", "default", "invalid"} - if !slices.Contains(missingKeyValues, missingKey) { - return nil, fmt.Errorf("not allowed value for the 'missing-key' flag: %s. Allowed values: %s", missingKey, strings.Join(missingKeyValues, ",")) - } - - tmpl.Option("missingkey=" + missingKey) - - funcMap := copyFuncMap(funcs) - - // the "tmpl" funcs get added here because they need access to the root template and context - addTmplFuncs(funcMap, tmpl, tmplctx, name) - tmpl.Funcs(funcMap) - tmpl.Delims(leftDelim, rightDelim) - _, err = tmpl.Parse(text) - if err != nil { - return nil, err - } - - err = parseNestedTemplates(ctx, nested, tmpl) - if err != nil { - return nil, fmt.Errorf("parse nested templates: %w", err) - } - - return tmpl, nil -} - -func parseNestedTemplates(ctx context.Context, nested config.Templates, tmpl *template.Template) error { - fsp := datafs.FSProviderFromContext(ctx) - - for alias, n := range nested { - u := *n.URL - - fname := path.Base(u.Path) - if strings.HasSuffix(u.Path, "/") { - fname = "." - } - - u.Path = path.Dir(u.Path) - - fsys, err := fsp.New(&u) - if err != nil { - return fmt.Errorf("filesystem provider for %q unavailable: %w", &u, err) - } - - // TODO: maybe need to do something with root here? - _, reldir, err := datafs.ResolveLocalPath(fsys, u.Path) - if err != nil { - return fmt.Errorf("resolveLocalPath: %w", err) - } - - if reldir != "" && reldir != "." { - fsys, err = fs.Sub(fsys, reldir) - if err != nil { - return fmt.Errorf("sub filesystem for %q unavailable: %w", &u, err) - } - } - - // inject context & header in case they're useful... - fsys = fsimpl.WithContextFS(ctx, fsys) - fsys = fsimpl.WithHeaderFS(n.Header, fsys) - - // valid fs.FS paths have no trailing slash - fname = strings.TrimRight(fname, "/") - - // first determine if the template path is a directory, in which case we - // need to load all the files in the directory (but not recursively) - fi, err := fs.Stat(fsys, fname) - if err != nil { - return fmt.Errorf("stat %q: %w", fname, err) - } - - if fi.IsDir() { - err = parseNestedTemplateDir(ctx, fsys, alias, fname, tmpl) - } else { - err = parseNestedTemplate(ctx, fsys, alias, fname, tmpl) - } - - if err != nil { - return err - } - } - - return nil -} - -func parseNestedTemplateDir(ctx context.Context, fsys fs.FS, alias, fname string, tmpl *template.Template) error { - files, err := fs.ReadDir(fsys, fname) - if err != nil { - return fmt.Errorf("readDir %q: %w", fname, err) - } - - for _, f := range files { - if !f.IsDir() { - err = parseNestedTemplate(ctx, fsys, - path.Join(alias, f.Name()), - path.Join(fname, f.Name()), - tmpl, - ) - if err != nil { - return err - } - } - } - - return nil -} - -func parseNestedTemplate(_ context.Context, fsys fs.FS, alias, fname string, tmpl *template.Template) error { - b, err := fs.ReadFile(fsys, fname) - if err != nil { - return fmt.Errorf("readFile %q: %w", fname, err) - } - - _, err = tmpl.New(alias).Parse(string(b)) - if err != nil { - return fmt.Errorf("parse nested template %q: %w", fname, err) - } - - return nil -} - // gatherTemplates - gather and prepare templates for rendering // //nolint:gocyclo diff --git a/template_test.go b/template_test.go index 60607b2fd..c9d716ad0 100644 --- a/template_test.go +++ b/template_test.go @@ -199,7 +199,9 @@ func TestParseNestedTemplates(t *testing.T) { tmpl, _ := template.New("root").Parse(`{{ template "foo" }}`) - err := parseNestedTemplates(ctx, nested, tmpl) + r := &renderer{nested: nested} + + err := r.parseNestedTemplates(ctx, tmpl) require.NoError(t, err) out := bytes.Buffer{} @@ -217,7 +219,8 @@ func TestParseNestedTemplates(t *testing.T) { tmpl, _ = template.New("root").Parse(`{{ template "dir/foo.t" }} {{ template "dir/bar.t" }}`) - err = parseNestedTemplates(ctx, nested, tmpl) + r = &renderer{nested: nested} + err = r.parseNestedTemplates(ctx, tmpl) require.NoError(t, err) out = bytes.Buffer{}