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