Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add files missed from PR 2083 #2084

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading