Skip to content

Commit

Permalink
chore: add files missed from PR 2083 (#2084)
Browse files Browse the repository at this point in the history
Signed-off-by: Dave Henderson <[email protected]>
  • Loading branch information
hairyhenderson authored May 30, 2024
1 parent 95a06b9 commit c32d485
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 173 deletions.
6 changes: 3 additions & 3 deletions gomplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down
24 changes: 12 additions & 12 deletions gomplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
},
Expand All @@ -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, "")
Expand All @@ -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,
Expand All @@ -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,
},
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 }}`
Expand All @@ -151,7 +151,7 @@ func TestMissingKey(t *testing.T) {
}

func TestCustomDelim(t *testing.T) {
g := NewRenderer(Options{
g := newRenderer(Options{
LDelim: "[",
RDelim: "]",
})
Expand Down Expand Up @@ -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" },
Expand Down
189 changes: 165 additions & 24 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import (
"context"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"path"
"slices"
"strings"
"sync"
"text/template"
"time"
Expand Down Expand Up @@ -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
Expand All @@ -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()
}
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -246,16 +262,15 @@ 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()
}
}

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)
}
Expand All @@ -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()

Expand Down
Loading

0 comments on commit c32d485

Please sign in to comment.