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 })()