From ec6448cf9a74096c001c341ad51511341dbfdb27 Mon Sep 17 00:00:00 2001 From: Ajay Kidave Date: Wed, 3 Apr 2024 11:13:23 -0700 Subject: [PATCH] Added support for structured template layout --- internal/app/app.go | 76 ++++++++++++++++++++++++-- internal/app/handler.go | 6 +-- internal/app/tests/tmpl_test.go | 96 +++++++++++++++++++++++++++++++++ internal/app/util/appconfig.go | 2 + tests/testapp/config_gen.lock | 1 + 5 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 internal/app/tests/tmpl_test.go diff --git a/internal/app/app.go b/internal/app/app.go index 26adfc0..c35b8ec 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "html/template" + "io" "io/fs" "net/http" "os" @@ -49,8 +50,9 @@ type App struct { appDef *starlarkstruct.Struct errorHandler starlark.Callable appRouter *chi.Mux - usesHtmlTemplate bool // Whether the app uses HTML templates, false if only JSON APIs - template *template.Template + usesHtmlTemplate bool // Whether the app uses HTML templates, false if only JSON APIs + template *template.Template // unstructured templates, no base_templates defined + templateMap map[string]*template.Template // structured templates, base_templates defined watcher *fsnotify.Watcher sseListeners []chan SSEMessage funcMap template.FuncMap @@ -243,9 +245,43 @@ func (a *App) Reload(force, immediate bool) (bool, error) { // Parse HTML templates if there are HTML routes if a.usesHtmlTemplate { - if a.template, err = a.sourceFS.ParseFS(a.funcMap, a.Config.Routing.TemplateLocations...); err != nil { + baseFiles, err := a.sourceFS.Glob(path.Join(a.Config.Routing.BaseTemplates, "*.go.html")) + if err != nil { return false, err } + + if len(baseFiles) == 0 { + // No base templates found, use the default unstructured templates + if a.template, err = a.sourceFS.ParseFS(a.funcMap, a.Config.Routing.TemplateLocations...); err != nil { + return false, err + } + } else { + // Base templates found, using structured templates + base, err := a.sourceFS.ParseFS(a.funcMap, baseFiles...) + if err != nil { + return false, err + } + + a.templateMap = make(map[string]*template.Template) + for _, paths := range a.Config.Routing.TemplateLocations { + files, err := a.sourceFS.Glob(paths) + if err != nil { + return false, err + } + + for _, file := range files { + tmpl, err := base.Clone() + if err != nil { + return false, err + } + + a.templateMap[file], err = tmpl.ParseFS(a.sourceFS.ReadableFS, file) + if err != nil { + return false, err + } + } + } + } } a.initialized = true @@ -255,6 +291,40 @@ func (a *App) Reload(force, immediate bool) (bool, error) { return true, nil } +func (a *App) executeTemplate(w io.Writer, template, partial string, data any) error { + var err error + if a.template != nil { + exec := partial + if partial == "" { + exec = template + } + if err = a.template.ExecuteTemplate(w, exec, data); err != nil { + return err + } + } else { + if template == "" { + if _, ok := a.templateMap[partial]; ok { + template = partial + } else { + template = "index.go.html" + } + } + + t, ok := a.templateMap[template] + if !ok { + return fmt.Errorf("template %s not found", template) + } + exec := partial + if partial == "" { + exec = template + } + if err = t.ExecuteTemplate(w, exec, data); err != nil { + return err + } + } + return err +} + func (a *App) loadSchemaInfo(sourceFS *util.SourceFs) error { // Load the schema info schemaInfoData, err := sourceFS.ReadFile(util.SCHEMA_FILE_NAME) diff --git a/internal/app/handler.go b/internal/app/handler.go index 48afe61..ad87f6a 100644 --- a/internal/app/handler.go +++ b/internal/app/handler.go @@ -238,7 +238,7 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r var err error if isHtmxRequest && block != "" { a.Trace().Msgf("Rendering block %s", block) - err = a.template.ExecuteTemplate(w, block, requestData) + err = a.executeTemplate(w, html, block, requestData) } else { referrer := r.Header.Get("Referer") isUpdateRequest := r.Method != http.MethodGet && r.Method != http.MethodHead && r.Method != http.MethodOptions @@ -250,7 +250,7 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r return } else { a.Trace().Msgf("Rendering page %s", html) - err = a.template.ExecuteTemplate(w, html, requestData) + err = a.executeTemplate(w, html, "", requestData) } } @@ -392,7 +392,7 @@ func (a *App) handleResponse(retStruct *starlarkstruct.Struct, r *http.Request, return true, nil } w.WriteHeader(int(code)) - err = a.template.ExecuteTemplate(w, templateBlock, requestData) + err = a.executeTemplate(w, "", templateBlock, requestData) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return true, nil diff --git a/internal/app/tests/tmpl_test.go b/internal/app/tests/tmpl_test.go new file mode 100644 index 0000000..f33c00e --- /dev/null +++ b/internal/app/tests/tmpl_test.go @@ -0,0 +1,96 @@ +// Copyright (c) ClaceIO, LLC +// SPDX-License-Identifier: Apache-2.0 + +package app_test + +import ( + "net/http/httptest" + "testing" + + "github.com/claceio/clace/internal/testutil" +) + +func TestBaseTemplate(t *testing.T) { + logger := testutil.TestLogger() + fileData := map[string]string{ + "app.star": ` +app = ace.app("testApp", custom_layout=True, pages = [ace.page("/")]) + +def handler(req): + return {"key": "myvalue"}`, + "index.go.html": `ABC {{.Data.key}} {{- template "base" . -}}`, + "base_templates/aaa.go.html": `{{define "base"}} aaa{{end}}`, + } + a, _, err := CreateTestAppRoot(logger, fileData) + if err != nil { + t.Fatalf("Error %s", err) + } + + request := httptest.NewRequest("GET", "/", nil) + response := httptest.NewRecorder() + a.ServeHTTP(response, request) + + testutil.AssertEqualsInt(t, "code", 200, response.Code) + testutil.AssertEqualsString(t, "body", "ABC myvalue aaa", response.Body.String()) +} + +func TestBaseTemplateResponse(t *testing.T) { + logger := testutil.TestLogger() + fileData := map[string]string{ + "app.star": ` +app = ace.app("testApp", custom_layout=True, pages = [ace.page("/")]) + +def handler(req): + return ace.response({"key": "myvalue"}, "index.go.html")`, + "index.go.html": `ABC {{.Data.key}} {{- template "base" . -}}`, + "base_templates/aaa.go.html": `{{define "base"}} aaa{{end}}`, + } + a, _, err := CreateTestAppRoot(logger, fileData) + if err != nil { + t.Fatalf("Error %s", err) + } + + request := httptest.NewRequest("GET", "/", nil) + response := httptest.NewRecorder() + a.ServeHTTP(response, request) + + testutil.AssertEqualsInt(t, "code", 200, response.Code) + testutil.AssertEqualsString(t, "body", "ABC myvalue aaa", response.Body.String()) +} + +func TestBaseTemplateComplete(t *testing.T) { + logger := testutil.TestLogger() + fileData := map[string]string{ + "app.star": ` +app = ace.app("testApp", custom_layout=True, pages = [ + ace.page("/"), + ace.page("/second", "second.go.html"), + ]) + +def handler(req): + return {}`, + "index.go.html": `{{define "body"}} indexbody {{end}} {{- template "full" . -}}`, + "second.go.html": `{{define "body"}} secondbody {{end}} {{- template "full" . -}}`, + "base_templates/head.go.html": `{{define "head"}} {{end}}`, + "base_templates/foot.go.html": `{{define "footer"}} {{end}}`, + "base_templates/full.go.html": `{{define "full"}} {{template "head" .}}{{block "body" .}}{{end}}{{template "footer" .}}{{end}}`, + } + a, _, err := CreateTestAppRoot(logger, fileData) + if err != nil { + t.Fatalf("Error %s", err) + } + + request := httptest.NewRequest("GET", "/", nil) + response := httptest.NewRecorder() + a.ServeHTTP(response, request) + + testutil.AssertEqualsInt(t, "code", 200, response.Code) + testutil.AssertStringMatch(t, "body", " indexbody ", response.Body.String()) + + request = httptest.NewRequest("GET", "/second", nil) + response = httptest.NewRecorder() + a.ServeHTTP(response, request) + + testutil.AssertEqualsInt(t, "code", 200, response.Code) + testutil.AssertStringMatch(t, "body", " secondbody ", response.Body.String()) +} diff --git a/internal/app/util/appconfig.go b/internal/app/util/appconfig.go index a1205c6..f813149 100644 --- a/internal/app/util/appconfig.go +++ b/internal/app/util/appconfig.go @@ -10,6 +10,7 @@ type AppConfig struct { type RouteConfig struct { TemplateLocations []string `json:"template_locations"` + BaseTemplates string `json:"base_templates"` StaticDir string `json:"static_dir"` StaticRootDir string `json:"static_root_dir"` PushEvents bool `json:"push_events"` @@ -28,6 +29,7 @@ func NewAppConfig() *AppConfig { return &AppConfig{ Routing: RouteConfig{ TemplateLocations: []string{"*.go.html"}, + BaseTemplates: "base_templates", StaticDir: "static", StaticRootDir: "static_root", PushEvents: false, diff --git a/tests/testapp/config_gen.lock b/tests/testapp/config_gen.lock index 9af7b88..4fa39e4 100644 --- a/tests/testapp/config_gen.lock +++ b/tests/testapp/config_gen.lock @@ -3,6 +3,7 @@ "template_locations": [ "*.go.html" ], + "base_templates": "base_templates", "static_dir": "static", "static_root_dir": "static_root", "push_events": false,