From 985ce3eba3c225479d0af3594113299cb0191ae9 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 7 Jul 2020 09:01:04 +0200 Subject: [PATCH] template/html: refactor template loading Allow customization of the template loading process by providing a Loader implementation. Allow auto-reloading of templates in development mode. --- template/html/loader.go | 56 ++++++++++++++++++ template/html/option.go | 9 +++ template/html/provider.go | 6 +- template/html/service.go | 116 ++++++++++++++++++-------------------- 4 files changed, 124 insertions(+), 63 deletions(-) create mode 100644 template/html/loader.go diff --git a/template/html/loader.go b/template/html/loader.go new file mode 100644 index 0000000..3eff842 --- /dev/null +++ b/template/html/loader.go @@ -0,0 +1,56 @@ +package html + +import ( + "io/ioutil" + "path/filepath" +) + +type Loader interface { + Load(*TemplateService) error +} + +type DirectoryLoader struct { + rootDir string +} + +func (l *DirectoryLoader) Load(srv *TemplateService) error { + blockFiles, err := filepath.Glob(filepath.Join(l.rootDir, "blocks", "*.tmpl")) + if err != nil { + return err + } + + for _, f := range blockFiles { + blockContent, err := ioutil.ReadFile(f) + if err != nil { + return err + } + + blockName := filepath.Base(f) + srv.AddBlock(blockName, string(blockContent)) + } + + layoutFiles, err := filepath.Glob(filepath.Join(l.rootDir, "layouts", "*.tmpl")) + if err != nil { + return err + } + + // Generate our templates map from our layouts/ and blocks/ directories + for _, f := range layoutFiles { + templateData, err := ioutil.ReadFile(f) + if err != nil { + return err + } + + templateName := filepath.Base(f) + + if err := srv.LoadLayout(templateName, string(templateData)); err != nil { + return err + } + } + + return nil +} + +func NewDirectoryLoader(rootDir string) *DirectoryLoader { + return &DirectoryLoader{rootDir} +} diff --git a/template/html/option.go b/template/html/option.go index 988fa3a..a6ad175 100644 --- a/template/html/option.go +++ b/template/html/option.go @@ -6,6 +6,7 @@ import "html/template" type Options struct { Helpers template.FuncMap PoolSize int + DevMode bool } // OptionFunc configures options for the template service @@ -48,6 +49,14 @@ func WithPoolSize(size int) OptionFunc { } } +// WithDevMode configures the template service +// to use the development mode (auto reload of templates). +func WithDevMode(enabled bool) OptionFunc { + return func(opts *Options) { + opts.DevMode = enabled + } +} + func defaultOptions() *Options { options := &Options{} funcs := []OptionFunc{ diff --git a/template/html/provider.go b/template/html/provider.go index 92263a3..13c456d 100644 --- a/template/html/provider.go +++ b/template/html/provider.go @@ -4,9 +4,9 @@ import "gitlab.com/wpetit/goweb/service" // ServiceProvider returns a service.Provider for the // the HTML template service implementation -func ServiceProvider(templateDir string, funcs ...OptionFunc) service.Provider { - templateService := NewTemplateService(funcs...) - err := templateService.LoadTemplatesDir(templateDir) +func ServiceProvider(loader Loader, funcs ...OptionFunc) service.Provider { + templateService := NewTemplateService(loader, funcs...) + err := templateService.Load() return func(container *service.Container) (interface{}, error) { if err != nil { return nil, err diff --git a/template/html/service.go b/template/html/service.go index f6497bc..3c32101 100644 --- a/template/html/service.go +++ b/template/html/service.go @@ -1,91 +1,79 @@ package html import ( - "fmt" + "errors" "html/template" "io" - "io/ioutil" "net/http" - "path/filepath" + "sync" "github.com/oxtoacart/bpool" ) +var ( + ErrLayoutNotFound = errors.New("layout not found") +) + // TemplateService is a template/html based templating service type TemplateService struct { - templates map[string]*template.Template - pool *bpool.BufferPool - helpers template.FuncMap + mutex sync.RWMutex + blocks map[string]string + layouts map[string]*template.Template + loader Loader + pool *bpool.BufferPool + helpers template.FuncMap + devMode bool } -func (t *TemplateService) LoadLayout(name string, layout string, blocks []string) error { +func (t *TemplateService) AddBlock(name string, blockContent string) { + t.mutex.Lock() + defer t.mutex.Unlock() + + t.blocks[name] = blockContent +} + +func (t *TemplateService) LoadLayout(name string, layoutContent string) error { tmpl := template.New(name) tmpl.Funcs(t.helpers) - for _, b := range blocks { + t.mutex.RLock() + for _, b := range t.blocks { if _, err := tmpl.Parse(b); err != nil { + t.mutex.RUnlock() return err } } + t.mutex.RUnlock() - tmpl, err := tmpl.Parse(layout) + tmpl, err := tmpl.Parse(layoutContent) if err != nil { return err } - t.templates[name] = tmpl + t.mutex.Lock() + t.layouts[name] = tmpl + t.mutex.Unlock() return nil } -// LoadTemplatesDir loads the templates used by the service -func (t *TemplateService) LoadTemplatesDir(templatesDir string) error { - layoutFiles, err := filepath.Glob(filepath.Join(templatesDir, "layouts", "*.tmpl")) - if err != nil { - return err - } - - blockFiles, err := filepath.Glob(filepath.Join(templatesDir, "blocks", "*.tmpl")) - if err != nil { - return err - } - - blockTemplates := make([]string, 0, len(blockFiles)) - - for _, f := range blockFiles { - templateData, err := ioutil.ReadFile(f) - if err != nil { - return err - } - - blockTemplates = append(blockTemplates, string(templateData)) - } - - // Generate our templates map from our layouts/ and blocks/ directories - for _, f := range layoutFiles { - templateData, err := ioutil.ReadFile(f) - if err != nil { - return err - } - - templateName := filepath.Base(f) - - if err := t.LoadLayout(templateName, string(templateData), blockTemplates); err != nil { - return err - } - } - - return nil - +// Load parses templates via the configured template loader. +func (t *TemplateService) Load() error { + return t.loader.Load(t) } // RenderPage renders a template to the given http.ResponseWriter func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, data interface{}) error { + if t.devMode { + if err := t.loader.Load(t); err != nil { + return err + } + } // Ensure the template exists in the map. - tmpl, ok := t.templates[templateName] + tmpl, ok := t.layouts[templateName] if !ok { - return fmt.Errorf("the template '%s' does not exist", templateName) + return ErrLayoutNotFound } // Create a buffer to temporarily write to and check if any errors were encountered. @@ -103,20 +91,25 @@ func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, } -// Render renders a template to the given io.Writer -func (t *TemplateService) Render(w io.Writer, templateName string, data interface{}) error { +// Render renders a layout to the given io.Writer. +func (t *TemplateService) Render(w io.Writer, layoutName string, data interface{}) error { + if t.devMode { + if err := t.loader.Load(t); err != nil { + return err + } + } // Ensure the template exists in the map. - tmpl, ok := t.templates[templateName] + tmpl, ok := t.layouts[layoutName] if !ok { - return fmt.Errorf("the template '%s' does not exist", templateName) + return ErrLayoutNotFound } // Create a buffer to temporarily write to and check if any errors were encountered. buf := t.pool.Get() defer t.pool.Put(buf) - if err := tmpl.ExecuteTemplate(buf, templateName, data); err != nil { + if err := tmpl.ExecuteTemplate(buf, layoutName, data); err != nil { return err } @@ -126,14 +119,17 @@ func (t *TemplateService) Render(w io.Writer, templateName string, data interfac } // NewTemplateService returns a new Service -func NewTemplateService(funcs ...OptionFunc) *TemplateService { +func NewTemplateService(loader Loader, funcs ...OptionFunc) *TemplateService { options := defaultOptions() for _, f := range funcs { f(options) } return &TemplateService{ - templates: make(map[string]*template.Template), - pool: bpool.NewBufferPool(options.PoolSize), - helpers: options.Helpers, + blocks: make(map[string]string), + layouts: make(map[string]*template.Template), + pool: bpool.NewBufferPool(options.PoolSize), + helpers: options.Helpers, + loader: loader, + devMode: options.DevMode, } }