add HTML rendering capabilities and helper functions
This commit is contained in:
parent
e44f56635b
commit
f9990d37c0
@ -10,9 +10,14 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
r := ron.New()
|
r := ron.New()
|
||||||
|
|
||||||
|
htmlRender := ron.HTMLRender()
|
||||||
|
r.Renderer = htmlRender
|
||||||
|
|
||||||
r.GET("/", helloWorld)
|
r.GET("/", helloWorld)
|
||||||
r.GET("/json", helloWorldJSON)
|
r.GET("/json", helloWorldJSON)
|
||||||
r.POST("/another", anotherHelloWorld)
|
r.POST("/another", anotherHelloWorld)
|
||||||
|
r.GET("/html", helloWorldHTML)
|
||||||
|
r.GET("/component", componentHTML)
|
||||||
|
|
||||||
slog.Info("Server is running at http://localhost:8080")
|
slog.Info("Server is running at http://localhost:8080")
|
||||||
http.ListenAndServe(":8080", r)
|
http.ListenAndServe(":8080", r)
|
||||||
@ -27,5 +32,16 @@ func anotherHelloWorld(c *ron.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func helloWorldJSON(c *ron.Context) {
|
func helloWorldJSON(c *ron.Context) {
|
||||||
c.JSON(200, ron.D{"message": "hello world"})
|
c.JSON(200, ron.Data{"message": "hello world"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func helloWorldHTML(c *ron.Context) {
|
||||||
|
c.HTML(200, "page.index.gohtml", ron.Data{
|
||||||
|
"title": "hello world",
|
||||||
|
"message": "hello world from html",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func componentHTML(c *ron.Context) {
|
||||||
|
c.HTML(200, "component.list.gohtml", ron.Data{})
|
||||||
}
|
}
|
||||||
|
|||||||
4
example/templates/page/component.list.gohtml
Normal file
4
example/templates/page/component.list.gohtml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<ul>
|
||||||
|
<li>elem1</li>
|
||||||
|
<li>elem2</li>
|
||||||
|
</ul>
|
||||||
16
example/templates/page/page.index.gohtml
Normal file
16
example/templates/page/page.index.gohtml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ .Data.title }}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{ .Data.message }}
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
16
ron.go
16
ron.go
@ -6,7 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type D map[string]interface{}
|
type Data map[string]any
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
C context.Context
|
C context.Context
|
||||||
@ -15,7 +15,12 @@ type Context struct {
|
|||||||
E *Engine
|
E *Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) JSON(code int, data interface{}) {
|
type Engine struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
Renderer *Render
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) JSON(code int, data Data) {
|
||||||
c.W.WriteHeader(code)
|
c.W.WriteHeader(code)
|
||||||
c.W.Header().Set("Content-Type", "application/json")
|
c.W.Header().Set("Content-Type", "application/json")
|
||||||
encoder := json.NewEncoder(c.W)
|
encoder := json.NewEncoder(c.W)
|
||||||
@ -24,8 +29,11 @@ func (c *Context) JSON(code int, data interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Engine struct {
|
func (c *Context) HTML(code int, name string, data Data) {
|
||||||
mux *http.ServeMux
|
c.W.WriteHeader(code)
|
||||||
|
c.E.Renderer.Template(c.W, name, &TemplateData{
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Engine {
|
func New() *Engine {
|
||||||
|
|||||||
141
template.go
Normal file
141
template.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package ron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
templateCache map[string]*template.Template
|
||||||
|
|
||||||
|
TemplateData struct {
|
||||||
|
Data Data
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionFunc func(*Render)
|
||||||
|
Render struct {
|
||||||
|
EnableCache bool
|
||||||
|
TemplatesPath string
|
||||||
|
Functions template.FuncMap
|
||||||
|
templateCache templateCache
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultHTMLRender() *Render {
|
||||||
|
return &Render{
|
||||||
|
EnableCache: false,
|
||||||
|
TemplatesPath: "templates",
|
||||||
|
Functions: make(template.FuncMap),
|
||||||
|
templateCache: make(templateCache),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTMLRender(opts ...OptionFunc) *Render {
|
||||||
|
config := DefaultHTMLRender()
|
||||||
|
return config.apply(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *Render) apply(opts ...OptionFunc) *Render {
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
opt(re)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return re
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *Render) Template(w http.ResponseWriter, tmpl string, td *TemplateData) error {
|
||||||
|
var tc templateCache
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if td == nil {
|
||||||
|
td = &TemplateData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if re.EnableCache {
|
||||||
|
tc = re.templateCache
|
||||||
|
} else {
|
||||||
|
tc, err = re.createTemplateCache()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t, ok := tc[tmpl]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("can't get template from cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err = t.Execute(buf, td)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = buf.WriteTo(w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *Render) findHTMLFiles() ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
err := filepath.WalkDir(re.TemplatesPath, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.IsDir() && filepath.Ext(path) == ".gohtml" {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *Render) createTemplateCache() (templateCache, error) {
|
||||||
|
cache := templateCache{}
|
||||||
|
var baseTemplates []string
|
||||||
|
var renderTemplates []string
|
||||||
|
|
||||||
|
templates, err := re.findHTMLFiles()
|
||||||
|
if err != nil {
|
||||||
|
return cache, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range templates {
|
||||||
|
filePathBase := filepath.Base(file)
|
||||||
|
if strings.Contains(filePathBase, "layout") || strings.Contains(filePathBase, "fragment") {
|
||||||
|
baseTemplates = append(baseTemplates, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range templates {
|
||||||
|
filePathBase := filepath.Base(file)
|
||||||
|
if strings.Contains(filePathBase, "page") || strings.Contains(filePathBase, "component") {
|
||||||
|
renderTemplates = append(baseTemplates, file)
|
||||||
|
ts, err := template.New(filePathBase).Funcs(re.Functions).ParseFiles(append(baseTemplates, renderTemplates...)...)
|
||||||
|
if err != nil {
|
||||||
|
return cache, err
|
||||||
|
}
|
||||||
|
cache[filePathBase] = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache, nil
|
||||||
|
}
|
||||||
195
template_test.go
Normal file
195
template_test.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package ron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"ron/testhelpers"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_DefaultHTMLRender(t *testing.T) {
|
||||||
|
expected := &Render{
|
||||||
|
EnableCache: false,
|
||||||
|
TemplatesPath: "templates",
|
||||||
|
Functions: make(template.FuncMap),
|
||||||
|
templateCache: make(templateCache),
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := DefaultHTMLRender()
|
||||||
|
if reflect.DeepEqual(expected, actual) == false {
|
||||||
|
t.Errorf("Expected: %v, Actual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_HTMLRender(t *testing.T) {
|
||||||
|
expected := &Render{
|
||||||
|
EnableCache: false,
|
||||||
|
TemplatesPath: "templates",
|
||||||
|
Functions: make(template.FuncMap),
|
||||||
|
templateCache: make(templateCache),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
arg OptionFunc
|
||||||
|
}{
|
||||||
|
{"Empty OptionFunc", OptionFunc(func(r *Render) {})},
|
||||||
|
{"Nil", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual := HTMLRender(tt.arg)
|
||||||
|
if reflect.DeepEqual(expected, actual) == false {
|
||||||
|
t.Errorf("Expected: %v, Actual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_apply(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expected *Render
|
||||||
|
actual *Render
|
||||||
|
}{
|
||||||
|
{"Empty OptionFunc", &Render{
|
||||||
|
EnableCache: false,
|
||||||
|
TemplatesPath: "templates",
|
||||||
|
Functions: make(template.FuncMap),
|
||||||
|
templateCache: make(templateCache),
|
||||||
|
}, DefaultHTMLRender()},
|
||||||
|
{
|
||||||
|
name: "Two OptionFunc", expected: &Render{
|
||||||
|
EnableCache: true,
|
||||||
|
TemplatesPath: "foobar",
|
||||||
|
Functions: make(template.FuncMap),
|
||||||
|
templateCache: make(templateCache),
|
||||||
|
},
|
||||||
|
actual: HTMLRender(func(r *Render) {
|
||||||
|
r.EnableCache = true
|
||||||
|
r.TemplatesPath = "foobar"
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if reflect.DeepEqual(tt.expected, tt.actual) == false {
|
||||||
|
t.Errorf("Expected: %v, Actual: %v", tt.expected, tt.actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDummyFilesAndRender() *Render {
|
||||||
|
os.MkdirAll("templates", os.ModePerm)
|
||||||
|
|
||||||
|
f, _ := os.Create("templates/layout.base.gohtml")
|
||||||
|
f.Write([]byte("{{ define \"layout/base\" }}<p>layout.base.gohtml</p><p>{{ .Data.foo }}</p>{{ block \"base/content\" . }}{{ end }}{{ end }}"))
|
||||||
|
f.Close()
|
||||||
|
f, _ = os.Create("templates/layout.another.gohtml")
|
||||||
|
f.Write([]byte("{{ define \"layout/another\" }}<p>layout.another.gohtml</p><p>{{ .Data.bar }}</p>{{ block \"base/content\" . }}{{ end }}{{ end }}"))
|
||||||
|
f.Close()
|
||||||
|
f, _ = os.Create("templates/fragment.button.gohtml")
|
||||||
|
f.Close()
|
||||||
|
f, _ = os.Create("templates/component.list.gohtml")
|
||||||
|
f.Close()
|
||||||
|
f, _ = os.Create("templates/page.index.gohtml")
|
||||||
|
f.Write([]byte("{{ template \"layout/base\" .}}{{ define \"base/content\" }}<p>page.index.gohtml</p><p>{{ .Data.bar }}</p>{{ end }}"))
|
||||||
|
f.Close()
|
||||||
|
f, _ = os.Create("templates/page.another.gohtml")
|
||||||
|
f.Write([]byte("{{ template \"layout/another\" .}}{{ define \"base/content\" }}<p>page.another.gohtml</p><p>{{ .Data.foo }}</p>{{ end }}"))
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
render := DefaultHTMLRender()
|
||||||
|
return render
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findHTMLFiles(t *testing.T) {
|
||||||
|
render := createDummyFilesAndRender()
|
||||||
|
if render == nil {
|
||||||
|
t.Errorf("Error: %v", render)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll("templates")
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"templates\\layout.base.gohtml",
|
||||||
|
"templates\\layout.another.gohtml",
|
||||||
|
"templates\\fragment.button.gohtml",
|
||||||
|
"templates\\component.list.gohtml",
|
||||||
|
"templates\\page.index.gohtml",
|
||||||
|
"templates\\page.another.gohtml",
|
||||||
|
}
|
||||||
|
actual, err := render.findHTMLFiles()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAny := testhelpers.StringSliceToAnySlice(expected)
|
||||||
|
actualAny := testhelpers.StringSliceToAnySlice(actual)
|
||||||
|
|
||||||
|
if testhelpers.CheckSlicesEquality(expectedAny, actualAny) == false {
|
||||||
|
t.Errorf("Expected: %v, Actual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_createTemplateCache(t *testing.T) {
|
||||||
|
render := createDummyFilesAndRender()
|
||||||
|
if render == nil {
|
||||||
|
t.Errorf("Error: %v", render)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll("templates")
|
||||||
|
|
||||||
|
tc, err := render.createTemplateCache()
|
||||||
|
if err != nil || len(tc) != 3 {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
templateNames := []string{
|
||||||
|
"component.list.gohtml",
|
||||||
|
"page.index.gohtml",
|
||||||
|
"page.another.gohtml",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, templateName := range templateNames {
|
||||||
|
if _, ok := tc[templateName]; ok == false {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TemplateDefault(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"index", "<p>layout.base.gohtml</p><p>Foo</p><p>page.index.gohtml</p><p>Bar</p>"},
|
||||||
|
{"another", "<p>layout.another.gohtml</p><p>Bar</p><p>page.another.gohtml</p><p>Foo</p>"},
|
||||||
|
}
|
||||||
|
|
||||||
|
render := createDummyFilesAndRender()
|
||||||
|
if render == nil {
|
||||||
|
t.Errorf("Error: %v", render)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll("templates")
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
render.Template(rr, "page."+tt.name+".gohtml", &TemplateData{
|
||||||
|
Data: Data{
|
||||||
|
"foo": "Foo",
|
||||||
|
"bar": "Bar",
|
||||||
|
}})
|
||||||
|
|
||||||
|
if rr.Body.String() != tt.expected {
|
||||||
|
t.Errorf("Expected: %v, Actual: %v", tt.expected, rr.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
33
testhelpers/testhelpers.go
Normal file
33
testhelpers/testhelpers.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package testhelpers
|
||||||
|
|
||||||
|
func CheckSlicesEquality(a []any, b []any) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
aMap := make(map[any]int)
|
||||||
|
bMap := make(map[any]int)
|
||||||
|
|
||||||
|
for _, v := range a {
|
||||||
|
aMap[v]++
|
||||||
|
}
|
||||||
|
for _, v := range b {
|
||||||
|
bMap[v]++
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range aMap {
|
||||||
|
if bMap[k] != v {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceToAnySlice(s []string) []any {
|
||||||
|
var result []any
|
||||||
|
for _, v := range s {
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
70
testhelpers/testhelpers_test.go
Normal file
70
testhelpers/testhelpers_test.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package testhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_CheckSlicesEquality(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ok bool
|
||||||
|
sliceA []any
|
||||||
|
sliceB []any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Integers",
|
||||||
|
ok: true,
|
||||||
|
sliceA: []any{2, 3, 1},
|
||||||
|
sliceB: []any{1, 2, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Strings",
|
||||||
|
ok: true,
|
||||||
|
sliceA: []any{"x", "y", "z"},
|
||||||
|
sliceB: []any{"z", "y", "x"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Integers 2",
|
||||||
|
ok: true,
|
||||||
|
sliceA: []any{1, 2, 3},
|
||||||
|
sliceB: []any{1, 2, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Different lengths",
|
||||||
|
ok: false,
|
||||||
|
sliceA: []any{1, 2, 3},
|
||||||
|
sliceB: []any{1, 2, 3, 4},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Different lengths 2",
|
||||||
|
ok: false,
|
||||||
|
sliceA: []any{1, 2, 3, 4},
|
||||||
|
sliceB: []any{1, 2, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Different types",
|
||||||
|
ok: false,
|
||||||
|
sliceA: []any{1, 2, 3},
|
||||||
|
sliceB: []any{"1", "2", "3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if result := CheckSlicesEquality(tt.sliceA, tt.sliceB); result != tt.ok {
|
||||||
|
t.Errorf("CheckSlicesEquality() = %v, want %v", result, tt.ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_StringSliceToAnySlice(t *testing.T) {
|
||||||
|
expected := []any{"a", "b", "c"}
|
||||||
|
actual := StringSliceToAnySlice([]string{"a", "b", "c"})
|
||||||
|
|
||||||
|
if !CheckSlicesEquality(expected, actual) {
|
||||||
|
t.Errorf("StringSliceToAnySlice() = %v, want %v", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user