Skip to content

Commit

Permalink
Added support for structured template layout
Browse files Browse the repository at this point in the history
  • Loading branch information
akclace committed Apr 3, 2024
1 parent 9b9df75 commit ec6448c
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 6 deletions.
76 changes: 73 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"os"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions internal/app/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions internal/app/tests/tmpl_test.go
Original file line number Diff line number Diff line change
@@ -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"}} <head></head>{{end}}`,
"base_templates/foot.go.html": `{{define "footer"}} <footer></footer>{{end}}`,
"base_templates/full.go.html": `{{define "full"}} <html>{{template "head" .}}{{block "body" .}}{{end}}{{template "footer" .}}</html>{{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", "<html> <head></head> indexbody <footer></footer></html>", 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", "<html> <head></head> secondbody <footer></footer></html>", response.Body.String())
}
2 changes: 2 additions & 0 deletions internal/app/util/appconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions tests/testapp/config_gen.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"template_locations": [
"*.go.html"
],
"base_templates": "base_templates",
"static_dir": "static",
"static_root_dir": "static_root",
"push_events": false,
Expand Down

0 comments on commit ec6448c

Please sign in to comment.