github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/render.go (about)

     1  package gomplate
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"sync"
    10  	"text/template"
    11  	"time"
    12  
    13  	"github.com/hairyhenderson/go-fsimpl"
    14  	"github.com/hairyhenderson/go-fsimpl/autofs"
    15  	"github.com/hairyhenderson/gomplate/v4/data"
    16  	"github.com/hairyhenderson/gomplate/v4/internal/config"
    17  	"github.com/hairyhenderson/gomplate/v4/internal/datafs"
    18  )
    19  
    20  // Options for template rendering.
    21  //
    22  // Experimental: subject to breaking changes before the next major release
    23  type Options struct {
    24  	// FSProvider - allows lookups of data source filesystems. Defaults to
    25  	// [DefaultFSProvider].
    26  	FSProvider fsimpl.FSProvider
    27  
    28  	// Datasources - map of datasources to be read on demand when the
    29  	// 'datasource'/'ds'/'include' functions are used.
    30  	Datasources map[string]Datasource
    31  	// Context - map of datasources to be read immediately and added to the
    32  	// template's context
    33  	Context map[string]Datasource
    34  	// Templates - map of templates that can be referenced as nested templates
    35  	Templates map[string]Datasource
    36  
    37  	// Extra HTTP headers not attached to pre-defined datsources. Potentially
    38  	// used by datasources defined in the template.
    39  	ExtraHeaders map[string]http.Header
    40  
    41  	// Funcs - map of functions to be added to the default template functions.
    42  	// Duplicate functions will be overwritten by entries in this map.
    43  	Funcs template.FuncMap
    44  
    45  	// LeftDelim - set the left action delimiter for the template and all nested
    46  	// templates to the specified string. Defaults to "{{"
    47  	LDelim string
    48  	// RightDelim - set the right action delimiter for the template and all nested
    49  	// templates to the specified string. Defaults to "{{"
    50  	RDelim string
    51  
    52  	// MissingKey controls the behavior during execution if a map is indexed with a key that is not present in the map
    53  	MissingKey string
    54  
    55  	// Experimental - enable experimental features
    56  	Experimental bool
    57  }
    58  
    59  // optionsFromConfig - create a set of options from the internal config struct.
    60  // Does not set the Funcs field.
    61  func optionsFromConfig(cfg *config.Config) Options {
    62  	ds := make(map[string]Datasource, len(cfg.DataSources))
    63  	for k, v := range cfg.DataSources {
    64  		ds[k] = Datasource{
    65  			URL:    v.URL,
    66  			Header: v.Header,
    67  		}
    68  	}
    69  	cs := make(map[string]Datasource, len(cfg.Context))
    70  	for k, v := range cfg.Context {
    71  		cs[k] = Datasource{
    72  			URL:    v.URL,
    73  			Header: v.Header,
    74  		}
    75  	}
    76  	ts := make(map[string]Datasource, len(cfg.Templates))
    77  	for k, v := range cfg.Templates {
    78  		ts[k] = Datasource{
    79  			URL:    v.URL,
    80  			Header: v.Header,
    81  		}
    82  	}
    83  
    84  	opts := Options{
    85  		Datasources:  ds,
    86  		Context:      cs,
    87  		Templates:    ts,
    88  		ExtraHeaders: cfg.ExtraHeaders,
    89  		LDelim:       cfg.LDelim,
    90  		RDelim:       cfg.RDelim,
    91  		MissingKey:   cfg.MissingKey,
    92  		Experimental: cfg.Experimental,
    93  	}
    94  
    95  	return opts
    96  }
    97  
    98  // Datasource - a datasource URL with optional headers
    99  //
   100  // Experimental: subject to breaking changes before the next major release
   101  type Datasource struct {
   102  	URL    *url.URL
   103  	Header http.Header
   104  }
   105  
   106  // Renderer provides gomplate's core template rendering functionality.
   107  // It should be initialized with NewRenderer.
   108  //
   109  // Experimental: subject to breaking changes before the next major release
   110  type Renderer struct {
   111  	//nolint:staticcheck
   112  	data        *data.Data
   113  	fsp         fsimpl.FSProvider
   114  	nested      config.Templates
   115  	funcs       template.FuncMap
   116  	lDelim      string
   117  	rDelim      string
   118  	missingKey  string
   119  	tctxAliases []string
   120  }
   121  
   122  // NewRenderer creates a new template renderer with the specified options.
   123  // The returned renderer can be reused, but it is not (yet) safe for concurrent
   124  // use.
   125  //
   126  // Experimental: subject to breaking changes before the next major release
   127  func NewRenderer(opts Options) *Renderer {
   128  	if Metrics == nil {
   129  		Metrics = newMetrics()
   130  	}
   131  
   132  	tctxAliases := []string{}
   133  	sources := map[string]config.DataSource{}
   134  
   135  	for alias, ds := range opts.Context {
   136  		tctxAliases = append(tctxAliases, alias)
   137  		sources[alias] = config.DataSource{
   138  			URL:    ds.URL,
   139  			Header: ds.Header,
   140  		}
   141  	}
   142  	for alias, ds := range opts.Datasources {
   143  		sources[alias] = config.DataSource{
   144  			URL:    ds.URL,
   145  			Header: ds.Header,
   146  		}
   147  	}
   148  
   149  	// convert the internal config.Templates to a map[string]Datasource
   150  	// TODO: simplify when config.Templates is removed
   151  	nested := config.Templates{}
   152  	for alias, ds := range opts.Templates {
   153  		nested[alias] = config.DataSource{
   154  			URL:    ds.URL,
   155  			Header: ds.Header,
   156  		}
   157  	}
   158  
   159  	//nolint:staticcheck
   160  	d := &data.Data{
   161  		ExtraHeaders: opts.ExtraHeaders,
   162  		Sources:      sources,
   163  	}
   164  
   165  	if opts.Funcs == nil {
   166  		opts.Funcs = template.FuncMap{}
   167  	}
   168  
   169  	missingKey := opts.MissingKey
   170  	if missingKey == "" {
   171  		missingKey = "error"
   172  	}
   173  
   174  	if opts.FSProvider == nil {
   175  		opts.FSProvider = DefaultFSProvider
   176  	}
   177  
   178  	return &Renderer{
   179  		nested:      nested,
   180  		data:        d,
   181  		funcs:       opts.Funcs,
   182  		tctxAliases: tctxAliases,
   183  		lDelim:      opts.LDelim,
   184  		rDelim:      opts.RDelim,
   185  		missingKey:  missingKey,
   186  		fsp:         opts.FSProvider,
   187  	}
   188  }
   189  
   190  // Template contains the basic data needed to render a template with a Renderer
   191  //
   192  // Experimental: subject to breaking changes before the next major release
   193  type Template struct {
   194  	// Writer is the writer to output the rendered template to. If this writer
   195  	// is a non-os.Stdout io.Closer, it will be closed after the template is
   196  	// rendered.
   197  	Writer io.Writer
   198  	// Name is the name of the template - used for error messages
   199  	Name string
   200  	// Text is the template text
   201  	Text string
   202  }
   203  
   204  // RenderTemplates renders a list of templates, parsing each template's Text
   205  // and executing it, outputting to its Writer. If a template's Writer is a
   206  // non-os.Stdout io.Closer, it will be closed after the template is rendered.
   207  //
   208  // Experimental: subject to breaking changes before the next major release
   209  func (t *Renderer) RenderTemplates(ctx context.Context, templates []Template) error {
   210  	if datafs.FSProviderFromContext(ctx) == nil {
   211  		ctx = datafs.ContextWithFSProvider(ctx, t.fsp)
   212  	}
   213  
   214  	// configure the template context with the refreshed Data value
   215  	// only done here because the data context may have changed
   216  	tmplctx, err := createTmplContext(ctx, t.tctxAliases, t.data)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	return t.renderTemplatesWithData(ctx, templates, tmplctx)
   222  }
   223  
   224  func (t *Renderer) renderTemplatesWithData(ctx context.Context, templates []Template, tmplctx interface{}) error {
   225  	// update funcs with the current context
   226  	// only done here to ensure the context is properly set in func namespaces
   227  	f := CreateFuncs(ctx, t.data)
   228  
   229  	// add user-defined funcs last so they override the built-in funcs
   230  	addToMap(f, t.funcs)
   231  
   232  	// track some metrics for debug output
   233  	start := time.Now()
   234  	defer func() { Metrics.TotalRenderDuration = time.Since(start) }()
   235  	for _, template := range templates {
   236  		err := t.renderTemplate(ctx, template, f, tmplctx)
   237  		if err != nil {
   238  			return fmt.Errorf("renderTemplate: %w", err)
   239  		}
   240  	}
   241  	return nil
   242  }
   243  
   244  func (t *Renderer) renderTemplate(ctx context.Context, template Template, f template.FuncMap, tmplctx interface{}) error {
   245  	if template.Writer != nil {
   246  		if wr, ok := template.Writer.(io.Closer); ok {
   247  			defer wr.Close()
   248  		}
   249  	}
   250  
   251  	tstart := time.Now()
   252  	tmpl, err := parseTemplate(ctx, template.Name, template.Text,
   253  		f, tmplctx, t.nested, t.lDelim, t.rDelim, t.missingKey)
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	err = tmpl.Execute(template.Writer, tmplctx)
   259  	Metrics.RenderDuration[template.Name] = time.Since(tstart)
   260  	if err != nil {
   261  		Metrics.Errors++
   262  		return fmt.Errorf("failed to render template %s: %w", template.Name, err)
   263  	}
   264  	Metrics.TemplatesProcessed++
   265  
   266  	return nil
   267  }
   268  
   269  // Render is a convenience method for rendering a single template. For more
   270  // than one template, use RenderTemplates. If wr is a non-os.Stdout
   271  // io.Closer, it will be closed after the template is rendered.
   272  //
   273  // Experimental: subject to breaking changes before the next major release
   274  func (t *Renderer) Render(ctx context.Context, name, text string, wr io.Writer) error {
   275  	return t.RenderTemplates(ctx, []Template{
   276  		{Name: name, Text: text, Writer: wr},
   277  	})
   278  }
   279  
   280  // DefaultFSProvider is the default filesystem provider used by gomplate
   281  var DefaultFSProvider = sync.OnceValue[fsimpl.FSProvider](
   282  	func() fsimpl.FSProvider {
   283  		fsp := fsimpl.NewMux()
   284  
   285  		// start with all go-fsimpl filesystems
   286  		fsp.Add(autofs.FS)
   287  
   288  		// override go-fsimpl's filefs with wdfs to handle working directories
   289  		fsp.Add(datafs.WdFS)
   290  
   291  		// gomplate-only filesystem
   292  		fsp.Add(datafs.EnvFS)
   293  		fsp.Add(datafs.StdinFS)
   294  		fsp.Add(datafs.MergeFS)
   295  
   296  		return fsp
   297  	})()