Added template renderer service.
This commit is contained in:
parent
d2ce5c34c4
commit
38cf009b70
5 changed files with 146 additions and 78 deletions
|
|
@ -2,18 +2,10 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"goweb/config"
|
|
||||||
"goweb/funcmap"
|
|
||||||
"goweb/middleware"
|
"goweb/middleware"
|
||||||
"goweb/msg"
|
"goweb/msg"
|
||||||
"goweb/services"
|
"goweb/services"
|
||||||
|
|
@ -27,17 +19,6 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"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.
|
// 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 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
|
// 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
|
// 3. All templates within the components directory
|
||||||
// Also included is the function map provided by the funcmap package
|
// Also included is the function map provided by the funcmap package
|
||||||
func (t *Controller) parsePageTemplates(p Page) error {
|
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
|
return t.Container.Templates.Parse(
|
||||||
// changes without having the restart the server
|
"controller",
|
||||||
if _, ok := templates.Load(p.Name); !ok || t.Container.Config.App.Environment == config.EnvLocal {
|
p.Name,
|
||||||
// Parse the Layout and Name templates along with the function map
|
p.Layout,
|
||||||
parsed, err :=
|
[]string{
|
||||||
template.New(p.Layout+config.TemplateExt).
|
fmt.Sprintf("layouts/%s", p.Layout),
|
||||||
Funcs(funcMap).
|
fmt.Sprintf("pages/%s", p.Name),
|
||||||
ParseFiles(
|
},
|
||||||
fmt.Sprintf("%s/layouts/%s%s", templatePath, p.Layout, config.TemplateExt),
|
[]string{"components"})
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeTemplates executes the cached templates belonging to Page and renders the Page within them
|
// executeTemplates executes the cached templates belonging to Page and renders the Page within them
|
||||||
func (t *Controller) executeTemplates(p Page) (*bytes.Buffer, error) {
|
func (t *Controller) executeTemplates(p Page) (*bytes.Buffer, error) {
|
||||||
tmpl, ok := templates.Load(p.Name)
|
return t.Container.Templates.Execute("controller", p.Name, p.Layout, p)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect redirects to a given route name with optional route parameters
|
// 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, "<strong>"+label+"</strong>"))
|
msg.Danger(c, fmt.Sprintf(message, "<strong>"+label+"</strong>"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
@ -128,22 +127,21 @@ func TestController_RenderPage(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the template cache
|
// Check the template cache
|
||||||
parsed, ok := templates.Load(p.Name)
|
parsed, err := c.Templates.Load("controller", p.Name)
|
||||||
assert.True(t, ok)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check that all expected templates were parsed.
|
// Check that all expected templates were parsed.
|
||||||
// This includes the name, layout and all components
|
// This includes the name, layout and all components
|
||||||
expectedTemplates := make(map[string]bool)
|
expectedTemplates := make(map[string]bool)
|
||||||
expectedTemplates[p.Name+config.TemplateExt] = true
|
expectedTemplates[p.Name+config.TemplateExt] = true
|
||||||
expectedTemplates[p.Layout+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)
|
require.NoError(t, err)
|
||||||
for _, f := range components {
|
for _, f := range components {
|
||||||
expectedTemplates[f.Name()] = true
|
expectedTemplates[f.Name()] = true
|
||||||
}
|
}
|
||||||
tmpl, ok := parsed.(*template.Template)
|
|
||||||
require.True(t, ok)
|
for _, v := range parsed.Templates() {
|
||||||
for _, v := range tmpl.Templates() {
|
|
||||||
delete(expectedTemplates, v.Name())
|
delete(expectedTemplates, v.Name())
|
||||||
}
|
}
|
||||||
assert.Empty(t, expectedTemplates)
|
assert.Empty(t, expectedTemplates)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ type Container struct {
|
||||||
ORM *ent.Client
|
ORM *ent.Client
|
||||||
Mail *MailClient
|
Mail *MailClient
|
||||||
Auth *AuthClient
|
Auth *AuthClient
|
||||||
|
Templates *TemplateRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer() *Container {
|
func NewContainer() *Container {
|
||||||
|
|
@ -38,6 +39,7 @@ func NewContainer() *Container {
|
||||||
c.initORM()
|
c.initORM()
|
||||||
c.initMail()
|
c.initMail()
|
||||||
c.initAuth()
|
c.initAuth()
|
||||||
|
c.initTemplateRenderer()
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,3 +146,7 @@ func (c *Container) initMail() {
|
||||||
func (c *Container) initAuth() {
|
func (c *Container) initAuth() {
|
||||||
c.Auth = NewAuthClient(c.Config, c.ORM)
|
c.Auth = NewAuthClient(c.Config, c.ORM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) initTemplateRenderer() {
|
||||||
|
c.Templates = NewTemplateRenderer(c.Config)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ func TestMain(m *testing.M) {
|
||||||
|
|
||||||
// Create a new container
|
// Create a new container
|
||||||
c = NewContainer()
|
c = NewContainer()
|
||||||
|
defer func() {
|
||||||
|
if err := c.Shutdown(); err != nil {
|
||||||
|
c.Web.Logger.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Create a web context
|
// Create a web context
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(""))
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(""))
|
||||||
|
|
@ -48,8 +53,5 @@ func TestMain(m *testing.M) {
|
||||||
|
|
||||||
// Run tests
|
// Run tests
|
||||||
exitVal := m.Run()
|
exitVal := m.Run()
|
||||||
if err := c.Shutdown(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
os.Exit(exitVal)
|
os.Exit(exitVal)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
services/templates.go
Normal file
120
services/templates.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue