go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/templates/bundle.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package templates
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"html/template"
    22  	"io"
    23  	"net/http"
    24  	"sync"
    25  
    26  	"github.com/julienschmidt/httprouter"
    27  )
    28  
    29  // Loader knows how to load template sets.
    30  type Loader func(context.Context, template.FuncMap) (map[string]*template.Template, error)
    31  
    32  // Args contains data passed to the template.
    33  type Args map[string]any
    34  
    35  // MergeArgs combines multiple Args instances into one. Returns nil if all
    36  // passed args are empty.
    37  func MergeArgs(args ...Args) Args {
    38  	total := 0
    39  	for _, a := range args {
    40  		total += len(a)
    41  	}
    42  	if total == 0 {
    43  		return nil
    44  	}
    45  	res := make(Args, total)
    46  	for _, a := range args {
    47  		for k, v := range a {
    48  			res[k] = v
    49  		}
    50  	}
    51  	return res
    52  }
    53  
    54  // Bundle is a bunch of templates lazily loaded at the same time. They may share
    55  // associated templates. Bundle is injected into the context.
    56  type Bundle struct {
    57  	// Loader will be called once to attempt to load templates on the first use.
    58  	//
    59  	// There are some predefined loaders you can use, see AssetsLoader(...)
    60  	// for example.
    61  	Loader Loader
    62  
    63  	// DebugMode, if not nil, can return true to enable template reloading before
    64  	// each use.
    65  	//
    66  	// It disables the caching of compiled templates, essentially. Useful during
    67  	// development, where it can be set to luci/gae's info service
    68  	// "IsDevAppServer" method directly.
    69  	DebugMode func(context.Context) bool
    70  
    71  	// FuncMap contains functions accessible from templates.
    72  	//
    73  	// Will be passed to Loader on first use. Not used after that.
    74  	FuncMap template.FuncMap
    75  
    76  	// DefaultTemplate is a name of subtemplate to pass to ExecuteTemplate when
    77  	// rendering a template via Render(...) or MustRender(...).
    78  	//
    79  	// For example, if all templates in a bundle are built around some base
    80  	// template (that defined structure of the page), DefaultTemplate can be set
    81  	// to the name of that base template.
    82  	//
    83  	// If DefaultTemplate is empty, Render(...) will use Execute(...) instead of
    84  	// ExecuteTemplate(...).
    85  	DefaultTemplate string
    86  
    87  	// DefaultArgs generates default arguments to use when rendering templates.
    88  	//
    89  	// Additional arguments passed to Render will be merged on top of the
    90  	// default ones. DefaultArgs is called each time Render is called.
    91  	//
    92  	// Extra will be whatever is passed to Render(...) or MustRender(...). Usually
    93  	// (when installing the bundle into the context via WithTemplates(...)
    94  	// middleware) Extra contains information about the request being processed.
    95  	DefaultArgs func(c context.Context, e *Extra) (Args, error)
    96  
    97  	once      sync.Once
    98  	templates map[string]*template.Template // result of call to Loader(...)
    99  	err       error                         // error from Loader, if any
   100  }
   101  
   102  // Extra is passed to DefaultArgs, it contains additional information about the
   103  // request being processed (usually populated by the middleware).
   104  //
   105  // Must be treated as read only.
   106  type Extra struct {
   107  	Request *http.Request
   108  	Params  httprouter.Params
   109  }
   110  
   111  // EnsureLoaded loads all the templates if they haven't been loaded yet.
   112  func (b *Bundle) EnsureLoaded(c context.Context) error {
   113  	// Always reload in debug mode. Load only once in non-debug mode.
   114  	if dm := b.DebugMode; dm != nil && dm(c) {
   115  		b.templates, b.err = b.Loader(c, b.FuncMap)
   116  	} else {
   117  		b.once.Do(func() {
   118  			b.templates, b.err = b.Loader(c, b.FuncMap)
   119  		})
   120  	}
   121  	return b.err
   122  }
   123  
   124  // Get returns the loaded template given its name or error if not found.
   125  //
   126  // The bundle must be loaded by this point (via call to EnsureLoaded).
   127  func (b *Bundle) Get(name string) (*template.Template, error) {
   128  	if b.err != nil {
   129  		return nil, b.err
   130  	}
   131  	if templ := b.templates[name]; templ != nil {
   132  		return templ, nil
   133  	}
   134  	return nil, fmt.Errorf("template: no such template %q in the bundle", name)
   135  }
   136  
   137  // Render finds template with given name and calls its Execute or
   138  // ExecuteTemplate method (depending on the value of DefaultTemplate).
   139  //
   140  // It passes the given context and Extra verbatim to DefaultArgs(...).
   141  // If DefaultArgs(...) doesn't access either of them, it is fine to pass nil
   142  // instead.
   143  //
   144  // It always renders output into byte buffer, to avoid partial results in case
   145  // of errors.
   146  //
   147  // The bundle must be loaded by this point (via call to EnsureLoaded).
   148  func (b *Bundle) Render(c context.Context, e *Extra, name string, args Args) ([]byte, error) {
   149  	templ, err := b.Get(name)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	var defArgs Args
   155  	if b.DefaultArgs != nil {
   156  		var err error
   157  		if defArgs, err = b.DefaultArgs(c, e); err != nil {
   158  			return nil, err
   159  		}
   160  	}
   161  
   162  	out := bytes.Buffer{}
   163  	if b.DefaultTemplate == "" {
   164  		err = templ.Execute(&out, MergeArgs(defArgs, args))
   165  	} else {
   166  		err = templ.ExecuteTemplate(&out, b.DefaultTemplate, MergeArgs(defArgs, args))
   167  	}
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return out.Bytes(), nil
   173  }
   174  
   175  // MustRender renders the template into the output writer or panics.
   176  //
   177  // It never writes partial output. It also panics if attempt to write to
   178  // the output fails.
   179  //
   180  // The bundle must be loaded by this point (via call to EnsureLoaded).
   181  func (b *Bundle) MustRender(c context.Context, e *Extra, out io.Writer, name string, args Args) {
   182  	blob, err := b.Render(c, e, name, args)
   183  	if err != nil {
   184  		panic(err)
   185  	}
   186  	_, err = out.Write(blob)
   187  	if err != nil {
   188  		panic(err)
   189  	}
   190  }