diff --git a/controller/controller.go b/controller/controller.go index e6f1a85..7074a1b 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -2,18 +2,10 @@ package controller import ( "bytes" - "errors" "fmt" - "html/template" "net/http" - "path" - "path/filepath" "reflect" - "runtime" - "sync" - "goweb/config" - "goweb/funcmap" "goweb/middleware" "goweb/msg" "goweb/services" @@ -27,17 +19,6 @@ import ( "github.com/labstack/echo/v4" ) -var ( - // templates stores a cache of parsed page templates - templates = sync.Map{} - - // funcMap stores the Template function map - funcMap = funcmap.GetFuncMap() - - // templatePath stores the complete path to the templates directory - templatePath = getTemplatesDirectoryPath() -) - // Controller provides base functionality and dependencies to routes. // The proposed pattern is to embed a Controller in each individual route struct and to use // the router to inject the container so your routes have access to the services within the container @@ -133,50 +114,20 @@ func (t *Controller) cachePage(c echo.Context, p Page, html *bytes.Buffer) { // 3. All templates within the components directory // Also included is the function map provided by the funcmap package func (t *Controller) parsePageTemplates(p Page) error { - // Check if the template has not yet been parsed or if the app environment is local, so that templates reflect - // changes without having the restart the server - if _, ok := templates.Load(p.Name); !ok || t.Container.Config.App.Environment == config.EnvLocal { - // Parse the Layout and Name templates along with the function map - parsed, err := - template.New(p.Layout+config.TemplateExt). - Funcs(funcMap). - ParseFiles( - fmt.Sprintf("%s/layouts/%s%s", templatePath, p.Layout, config.TemplateExt), - fmt.Sprintf("%s/pages/%s%s", templatePath, p.Name, config.TemplateExt), - ) - - if err != nil { - return err - } - - // Parse all templates within the components directory - parsed, err = parsed.ParseGlob(fmt.Sprintf("%s/components/*%s", templatePath, config.TemplateExt)) - - if err != nil { - return err - } - - // Store the template so this process only happens once - templates.Store(p.Name, parsed) - } - - return nil + return t.Container.Templates.Parse( + "controller", + p.Name, + p.Layout, + []string{ + fmt.Sprintf("layouts/%s", p.Layout), + fmt.Sprintf("pages/%s", p.Name), + }, + []string{"components"}) } // executeTemplates executes the cached templates belonging to Page and renders the Page within them func (t *Controller) executeTemplates(p Page) (*bytes.Buffer, error) { - tmpl, ok := templates.Load(p.Name) - if !ok { - return nil, errors.New("uncached page template requested") - } - - buf := new(bytes.Buffer) - err := tmpl.(*template.Template).ExecuteTemplate(buf, p.Layout+config.TemplateExt, p) - if err != nil { - return nil, err - } - - return buf, nil + return t.Container.Templates.Execute("controller", p.Name, p.Layout, p) } // Redirect redirects to a given route name with optional route parameters @@ -227,12 +178,3 @@ func (t *Controller) SetValidationErrorMessages(c echo.Context, err error, data msg.Danger(c, fmt.Sprintf(message, ""+label+"")) } } - -// getTemplatesDirectoryPath gets the templates directory path -// This is needed incase this is called from a package outside of main, -// such as within tests -func getTemplatesDirectoryPath() string { - _, b, _, _ := runtime.Caller(0) - d := path.Join(path.Dir(b)) - return filepath.Join(filepath.Dir(d), config.TemplateDir) -} diff --git a/controller/controller_test.go b/controller/controller_test.go index 7c0ffde..236cfca 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -2,7 +2,6 @@ package controller import ( "context" - "html/template" "io/ioutil" "net/http" "net/http/httptest" @@ -128,22 +127,21 @@ func TestController_RenderPage(t *testing.T) { } // Check the template cache - parsed, ok := templates.Load(p.Name) - assert.True(t, ok) + parsed, err := c.Templates.Load("controller", p.Name) + assert.NoError(t, err) // Check that all expected templates were parsed. // This includes the name, layout and all components expectedTemplates := make(map[string]bool) expectedTemplates[p.Name+config.TemplateExt] = true expectedTemplates[p.Layout+config.TemplateExt] = true - components, err := ioutil.ReadDir(getTemplatesDirectoryPath() + "/components") + components, err := ioutil.ReadDir(c.Templates.GetTemplatesPath() + "/components") require.NoError(t, err) for _, f := range components { expectedTemplates[f.Name()] = true } - tmpl, ok := parsed.(*template.Template) - require.True(t, ok) - for _, v := range tmpl.Templates() { + + for _, v := range parsed.Templates() { delete(expectedTemplates, v.Name()) } assert.Empty(t, expectedTemplates) diff --git a/services/container.go b/services/container.go index c48eb0f..4e715d5 100644 --- a/services/container.go +++ b/services/container.go @@ -27,6 +27,7 @@ type Container struct { ORM *ent.Client Mail *MailClient Auth *AuthClient + Templates *TemplateRenderer } func NewContainer() *Container { @@ -38,6 +39,7 @@ func NewContainer() *Container { c.initORM() c.initMail() c.initAuth() + c.initTemplateRenderer() return c } @@ -144,3 +146,7 @@ func (c *Container) initMail() { func (c *Container) initAuth() { c.Auth = NewAuthClient(c.Config, c.ORM) } + +func (c *Container) initTemplateRenderer() { + c.Templates = NewTemplateRenderer(c.Config) +} diff --git a/services/services_test.go b/services/services_test.go index 98b08aa..ea02a90 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -27,6 +27,11 @@ func TestMain(m *testing.M) { // Create a new container c = NewContainer() + defer func() { + if err := c.Shutdown(); err != nil { + c.Web.Logger.Fatal(err) + } + }() // Create a web context req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("")) @@ -48,8 +53,5 @@ func TestMain(m *testing.M) { // Run tests exitVal := m.Run() - if err := c.Shutdown(); err != nil { - panic(err) - } os.Exit(exitVal) } diff --git a/services/templates.go b/services/templates.go new file mode 100644 index 0000000..e72468f --- /dev/null +++ b/services/templates.go @@ -0,0 +1,120 @@ +package services + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "path" + "path/filepath" + "runtime" + "sync" + + "goweb/config" + "goweb/funcmap" +) + +type TemplateRenderer struct { + // templateCache stores a cache of parsed page templates + templateCache sync.Map + + // funcMap stores the template function map + funcMap template.FuncMap + + // templatePath stores the complete path to the templates directory + templatesPath string + + // config stores application configuration + config *config.Config +} + +func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer { + t := &TemplateRenderer{ + templateCache: sync.Map{}, + funcMap: funcmap.GetFuncMap(), + config: cfg, + } + + // Gets the complete templates directory path + // This is needed incase this is called from a package outside of main, such as within tests + _, b, _, _ := runtime.Caller(0) + d := path.Join(path.Dir(b)) + t.templatesPath = filepath.Join(filepath.Dir(d), config.TemplateDir) + + return t +} + +func (t *TemplateRenderer) Parse(module, key, name string, files []string, directories []string) error { + cacheKey := t.getCacheKey(module, key) + + // Check if the template has not yet been parsed or if the app environment is local, so that templates reflect + // changes without having the restart the server + if _, err := t.Load(module, key); err != nil { + // Initialize the parsed template with the function map + parsed := template.New(name + config.TemplateExt). + Funcs(t.funcMap) + + // Parse all files provided + if len(files) > 0 { + for k, v := range files { + files[k] = fmt.Sprintf("%s/%s%s", t.templatesPath, v, config.TemplateExt) + } + + parsed, err = parsed.ParseFiles(files...) + if err != nil { + return err + } + } + + // Parse all templates within the provided directories + for _, dir := range directories { + dir = fmt.Sprintf("%s/%s/*%s", t.templatesPath, dir, config.TemplateExt) + parsed, err = parsed.ParseGlob(dir) + if err != nil { + return err + } + } + + // Store the template so this process only happens once + t.templateCache.Store(cacheKey, parsed) + } + + return nil +} + +func (t *TemplateRenderer) Execute(module, key, name string, data interface{}) (*bytes.Buffer, error) { + tmpl, err := t.Load(module, key) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + err = tmpl.ExecuteTemplate(buf, name+config.TemplateExt, data) + if err != nil { + return nil, err + } + + return buf, nil +} + +func (t *TemplateRenderer) Load(module, key string) (*template.Template, error) { + load, ok := t.templateCache.Load(t.getCacheKey(module, key)) + if !ok { + return nil, errors.New("uncached page template requested") + } + + tmpl, ok := load.(*template.Template) + if !ok { + return nil, errors.New("unable to cast cached template") + } + + return tmpl, nil +} + +func (t *TemplateRenderer) GetTemplatesPath() string { + return t.templatesPath +} + +func (t *TemplateRenderer) getCacheKey(module, key string) string { + return fmt.Sprintf("%s:%s", module, key) +}