From 10dc07cecbfb571c6a72993ba57cac3effd9ac47 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 23 Jan 2024 08:57:00 -0500 Subject: [PATCH 01/30] Fix Pug concurrency, Expand benchmarks for Pug and HTML --- html/html_test.go | 117 +++++++++++++++++++++++++++++++++++++++++----- pug/pug.go | 6 +++ pug/pug_test.go | 116 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 214 insertions(+), 25 deletions(-) diff --git a/html/html_test.go b/html/html_test.go index b100c639..8ed7509e 100644 --- a/html/html_test.go +++ b/html/html_test.go @@ -2,12 +2,13 @@ package html import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + + "github.com/stretchr/testify/require" ) const ( @@ -267,34 +268,126 @@ func Benchmark_Html(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == admin }) - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") } + }) + + b.Run("SimpleAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + + require.NoError(b, err) + require.Equal(b, expectSimple, trim(buf.String())) + } + }) + + b.Run("ExtendedAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + + require.NoError(b, err) + require.Equal(b, expectExtended, trim(buf.String())) + } + }) +} + +func Benchmark_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Main

Header

Hello, Admin!

Footer

` + engine := New("./views", ".html") + + engine.AddFunc("isAdmin", func(user string) bool { + return user == admin + }) + require.NoError(b, engine.Load()) - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) + b.Run("ConcurrentSimple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("ConcurrentExtended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + } + }) + }) + + b.Run("ConcurrentSimpleAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("ConcurrentExtendedAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) }) } diff --git a/pug/pug.go b/pug/pug.go index 4787b0b7..25b79b86 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -136,6 +136,9 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { + e.Mutex.Lock() + sdefer e.Mutex.Unlock() + if !e.Loaded || e.ShouldReload { if e.ShouldReload { e.Loaded = false @@ -144,10 +147,13 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout return err } } + tmpl := e.Templates.Lookup(name) + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + if len(layout) > 0 && layout[0] != "" { lay := e.Templates.Lookup(layout[0]) if lay == nil { diff --git a/pug/pug_test.go b/pug/pug_test.go index c909915b..796e2d3c 100644 --- a/pug/pug_test.go +++ b/pug/pug_test.go @@ -2,12 +2,13 @@ package pug import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + + "github.com/stretchr/testify/require" ) const ( @@ -189,34 +190,123 @@ func Benchmark_Pug(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == admin }) - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) - b.Run("simple", func(bb *testing.B) { + b.Run("Simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } + }) - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) + b.Run("Extended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + } }) - b.Run("extended", func(bb *testing.B) { + b.Run("SimpleAsserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + + b.Run("ExtendedAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) } + }) +} - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) +func Benchmark_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Main

Header

Hello, Admin!

Footer

` + engine := New("./views", ".pug") + engine.AddFunc("isAdmin", func(user string) bool { + return user == admin + }) + require.NoError(b, engine.Load()) + + b.Run("ConcurrentSimple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("ConcurrentExtended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + } + }) + }) + + b.Run("ConcurrentSimpleAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("ConcurrentExtendedAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) }) } From f72deed8b1c875c6210dac5e0b054a320f46baf5 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 23 Jan 2024 09:00:31 -0500 Subject: [PATCH 02/30] Fix typo --- pug/pug.go | 2 +- pug/views/reload.pug | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pug/pug.go b/pug/pug.go index 25b79b86..cc899022 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -137,7 +137,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { e.Mutex.Lock() - sdefer e.Mutex.Unlock() + defer e.Mutex.Unlock() if !e.Loaded || e.ShouldReload { if e.ShouldReload { diff --git a/pug/views/reload.pug b/pug/views/reload.pug index ec0a8684..666bbb91 100644 --- a/pug/views/reload.pug +++ b/pug/views/reload.pug @@ -1 +1 @@ -before reload +after reload From cf69da7a82d2913344a0390b41da0f87ab585fc8 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 23 Jan 2024 09:01:43 -0500 Subject: [PATCH 03/30] Revert change to view --- pug/views/reload.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pug/views/reload.pug b/pug/views/reload.pug index 666bbb91..ec0a8684 100644 --- a/pug/views/reload.pug +++ b/pug/views/reload.pug @@ -1 +1 @@ -after reload +before reload From d5d63af91ac06bc955d0a3a13b912e25cd620724 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 23 Jan 2024 09:33:56 -0500 Subject: [PATCH 04/30] Expand benchmarks for Slim engine --- slim/slim_test.go | 65 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/slim/slim_test.go b/slim/slim_test.go index 23179dde..a686a5af 100644 --- a/slim/slim_test.go +++ b/slim/slim_test.go @@ -2,13 +2,14 @@ package slim import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/mattn/go-slim" ) @@ -182,20 +183,68 @@ func Benchmark_Slim(b *testing.B) { engine.AddFunc("isAdmin", func(s ...slim.Value) (slim.Value, error) { return s[0].(string) == "admin", nil }) - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) - b.Run("simple", func(bb *testing.B) { + b.Run("Simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } + }) - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) + b.Run("SimpleAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(b, err) + require.Equal(b, expectSimple, trim(buf.String())) + } + }) +} + +func Benchmark_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + engine := New("./views", ".slim") + engine.AddFunc("isAdmin", func(s ...slim.Value) (slim.Value, error) { + return s[0].(string) == "admin", nil + }) + require.NoError(b, engine.Load()) + + b.Run("ConcurrentSimple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("ConcurrentSimpleAsserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) }) } From 9820641f78e167d9968762ee6504c79d89707ce8 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Wed, 24 Jan 2024 09:11:37 -0500 Subject: [PATCH 05/30] Fix Pug unit-tests --- html/html_test.go | 12 ++++++------ pug/pug.go | 23 ++++++++++++++++++++--- pug/pug_test.go | 16 ++++++++-------- slim/slim_test.go | 8 ++++---- template.go | 8 ++++++++ 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/html/html_test.go b/html/html_test.go index 8ed7509e..6976c6df 100644 --- a/html/html_test.go +++ b/html/html_test.go @@ -294,7 +294,7 @@ func Benchmark_Html(b *testing.B) { } }) - b.Run("SimpleAsserted", func(bb *testing.B) { + b.Run("simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -308,7 +308,7 @@ func Benchmark_Html(b *testing.B) { } }) - b.Run("ExtendedAsserted", func(bb *testing.B) { + b.Run("extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -333,7 +333,7 @@ func Benchmark_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("ConcurrentSimple", func(bb *testing.B) { + b.Run("concurrent_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -347,7 +347,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentExtended", func(bb *testing.B) { + b.Run("concurrent_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -361,7 +361,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentSimpleAsserted", func(bb *testing.B) { + b.Run("concurrent_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -376,7 +376,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentExtendedAsserted", func(bb *testing.B) { + b.Run("concurrent_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/pug/pug.go b/pug/pug.go index cc899022..34cf80bb 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -136,35 +136,52 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - e.Mutex.Lock() - defer e.Mutex.Unlock() + // Check if templates need to be loaded/reloaded + e.Mutex.RLock() + shouldReload := e.ShouldReload + e.Mutex.RUnlock() - if !e.Loaded || e.ShouldReload { + if !e.Loaded || shouldReload { + e.Mutex.Lock() if e.ShouldReload { e.Loaded = false } + e.Mutex.Unlock() + if err := e.Load(); err != nil { return err } } + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates.Lookup(name) + e.Mutex.RUnlock() if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + + // Handle layout if specified if len(layout) > 0 && layout[0] != "" { lay := e.Templates.Lookup(layout[0]) + if lay == nil { return fmt.Errorf("render: layout %s does not exist", layout[0]) } + lay.Funcs(map[string]interface{}{ e.LayoutName: func() error { return tmpl.Execute(out, binding) }, }) + return lay.Execute(out, binding) } + return tmpl.Execute(out, binding) } diff --git a/pug/pug_test.go b/pug/pug_test.go index 796e2d3c..7474f612 100644 --- a/pug/pug_test.go +++ b/pug/pug_test.go @@ -192,7 +192,7 @@ func Benchmark_Pug(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("Simple", func(bb *testing.B) { + b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -204,7 +204,7 @@ func Benchmark_Pug(b *testing.B) { } }) - b.Run("Extended", func(bb *testing.B) { + b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -216,7 +216,7 @@ func Benchmark_Pug(b *testing.B) { } }) - b.Run("SimpleAsserted", func(bb *testing.B) { + b.Run("simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -229,7 +229,7 @@ func Benchmark_Pug(b *testing.B) { } }) - b.Run("ExtendedAsserted", func(bb *testing.B) { + b.Run("extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -252,7 +252,7 @@ func Benchmark_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("ConcurrentSimple", func(bb *testing.B) { + b.Run("concurrent_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -266,7 +266,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentExtended", func(bb *testing.B) { + b.Run("concurrent_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -280,7 +280,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentSimpleAsserted", func(bb *testing.B) { + b.Run("concurrent_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -295,7 +295,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentExtendedAsserted", func(bb *testing.B) { + b.Run("concurrent_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/slim/slim_test.go b/slim/slim_test.go index a686a5af..f892da5a 100644 --- a/slim/slim_test.go +++ b/slim/slim_test.go @@ -185,7 +185,7 @@ func Benchmark_Slim(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("Simple", func(bb *testing.B) { + b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -197,7 +197,7 @@ func Benchmark_Slim(b *testing.B) { } }) - b.Run("SimpleAsserted", func(bb *testing.B) { + b.Run("simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { @@ -219,7 +219,7 @@ func Benchmark_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("ConcurrentSimple", func(bb *testing.B) { + b.Run("concurrent_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -233,7 +233,7 @@ func Benchmark_Concurrent(b *testing.B) { }) }) - b.Run("ConcurrentSimpleAsserted", func(bb *testing.B) { + b.Run("concurrent_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/template.go b/template.go index bc71d7e4..964fb0ef 100644 --- a/template.go +++ b/template.go @@ -72,7 +72,9 @@ func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore { // Debug will print the parsed templates when Load is triggered. func (e *Engine) Debug(enabled bool) IEngineCore { + e.Mutex.Lock() e.Verbose = enabled + e.Mutex.Unlock() return e } @@ -80,7 +82,9 @@ func (e *Engine) Debug(enabled bool) IEngineCore { // templates. An empty delimiter stands for the // corresponding default: "{{" and "}}". func (e *Engine) Delims(left, right string) IEngineCore { + e.Mutex.Lock() e.Left, e.Right = left, right + e.Mutex.Unlock() return e } @@ -91,7 +95,9 @@ func (e *Engine) FuncMap() map[string]interface{} { // Layout defines the variable name that will incapsulate the template func (e *Engine) Layout(key string) IEngineCore { + e.Mutex.Lock() e.LayoutName = key + e.Mutex.Unlock() return e } @@ -99,6 +105,8 @@ func (e *Engine) Layout(key string) IEngineCore { // use it when you're in development and you don't want to restart // the application when you edit a template file. func (e *Engine) Reload(enabled bool) IEngineCore { + e.Mutex.Lock() e.ShouldReload = enabled + e.Mutex.Unlock() return e } From b60cef2e213d7bc7e8b69e22ccb885010db45cb1 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Wed, 24 Jan 2024 09:31:37 -0500 Subject: [PATCH 06/30] Update benchmarks for Ace --- ace/ace.go | 25 +++++++++- ace/ace_test.go | 116 +++++++++++++++++++++++++++++++++++++++++----- html/html_test.go | 4 +- pug/pug.go | 2 - pug/pug_test.go | 2 +- slim/slim_test.go | 2 +- 6 files changed, 130 insertions(+), 21 deletions(-) diff --git a/ace/ace.go b/ace/ace.go index f8d908cf..17272229 100644 --- a/ace/ace.go +++ b/ace/ace.go @@ -140,25 +140,45 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { + // Check if templates need to be loaded/reloaded + e.Mutex.RLock() + shouldReload := e.ShouldReload + e.Mutex.RUnlock() + // ShouldReload the views - if !e.Loaded || e.ShouldReload { + if !e.Loaded || shouldReload { + e.Mutex.Lock() if e.ShouldReload { e.Loaded = false } + e.Mutex.Unlock() + ace.FlushCache() if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates.Lookup(name) + e.Mutex.RUnlock() + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + + // Handle layout if specified if len(layout) > 0 && layout[0] != "" { lay := e.Templates.Lookup(layout[0]) if lay == nil { - return fmt.Errorf("render: LayoutName %s does not exist", layout[0]) + return fmt.Errorf("render: layout %s does not exist", layout[0]) } + lay.Funcs(map[string]interface{}{ e.LayoutName: func() error { return tmpl.Execute(out, binding) @@ -166,5 +186,6 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout }) return lay.Execute(out, binding) } + return tmpl.Execute(out, binding) } diff --git a/ace/ace_test.go b/ace/ace_test.go index ad5951ad..5fbf08df 100644 --- a/ace/ace_test.go +++ b/ace/ace_test.go @@ -2,12 +2,13 @@ package ace import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + + "github.com/stretchr/testify/require" ) const ( @@ -197,34 +198,125 @@ func Benchmark_Ace(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == admin }) - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") } + }) + + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) + require.NoError(b, err) + require.Equal(b, expectSimple, trim(buf.String())) + } + }) + + b.Run("extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + + require.NoError(b, err) + require.Equal(b, expectExtended, trim(buf.String())) + } + }) +} + +func Benchmark_Ace_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Main

Header

Hello, Admin!

Footer

` + engine := New("./views", ".ace") + engine.AddFunc("isAdmin", func(user string) bool { + return user == admin + }) + require.NoError(b, engine.Load()) + + b.Run("concurrent_simple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("concurrent_extended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + } + }) + }) + + b.Run("concurrent_simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("concurrent_extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) }) } diff --git a/html/html_test.go b/html/html_test.go index 6976c6df..fb02ce69 100644 --- a/html/html_test.go +++ b/html/html_test.go @@ -263,7 +263,6 @@ func Test_Reload(t *testing.T) { func Benchmark_Html(b *testing.B) { expectSimple := `

Hello, World!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` - engine := New("./views", ".html") engine.AddFunc("isAdmin", func(user string) bool { return user == admin @@ -323,11 +322,10 @@ func Benchmark_Html(b *testing.B) { }) } -func Benchmark_Concurrent(b *testing.B) { +func Benchmark_Html_Concurrent(b *testing.B) { expectSimple := `

Hello, Concurrent!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".html") - engine.AddFunc("isAdmin", func(user string) bool { return user == admin }) diff --git a/pug/pug.go b/pug/pug.go index 34cf80bb..25149d57 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -169,7 +169,6 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout // Handle layout if specified if len(layout) > 0 && layout[0] != "" { lay := e.Templates.Lookup(layout[0]) - if lay == nil { return fmt.Errorf("render: layout %s does not exist", layout[0]) } @@ -179,7 +178,6 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout return tmpl.Execute(out, binding) }, }) - return lay.Execute(out, binding) } diff --git a/pug/pug_test.go b/pug/pug_test.go index 7474f612..d9de0c51 100644 --- a/pug/pug_test.go +++ b/pug/pug_test.go @@ -243,7 +243,7 @@ func Benchmark_Pug(b *testing.B) { }) } -func Benchmark_Concurrent(b *testing.B) { +func Benchmark_Pug_Concurrent(b *testing.B) { expectSimple := `

Hello, Concurrent!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".pug") diff --git a/slim/slim_test.go b/slim/slim_test.go index f892da5a..d1967611 100644 --- a/slim/slim_test.go +++ b/slim/slim_test.go @@ -211,7 +211,7 @@ func Benchmark_Slim(b *testing.B) { }) } -func Benchmark_Concurrent(b *testing.B) { +func Benchmark_Slim_Concurrent(b *testing.B) { expectSimple := `

Hello, Concurrent!

` engine := New("./views", ".slim") engine.AddFunc("isAdmin", func(s ...slim.Value) (slim.Value, error) { From e612189f8ce725be3139e7d8a32eee6153f7b54f Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 27 Jan 2024 00:50:47 -0500 Subject: [PATCH 07/30] Update pug.go --- amber/amber_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++-- html/html.go | 2 +- pug/pug.go | 22 ++++++------- template.go | 60 +++++++++++++++++++++++++++--------- 4 files changed, 129 insertions(+), 30 deletions(-) diff --git a/amber/amber_test.go b/amber/amber_test.go index b7e43cfb..cbd704cb 100644 --- a/amber/amber_test.go +++ b/amber/amber_test.go @@ -3,15 +3,17 @@ package amber import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + + "github.com/stretchr/testify/require" ) const ( + admin = "admin" complexexpect = `Main

Header

Hello, World!

Footer

` ) @@ -175,6 +177,7 @@ func Benchmark_Amber(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == "admin" }) + require.NoError(b, engine.Load()) var buf bytes.Buffer var err error @@ -199,7 +202,7 @@ func Benchmark_Amber(b *testing.B) { for i := 0; i < bb.N; i++ { buf.Reset() err = engine.Render(&buf, "extended", map[string]interface{}{ - "User": "admin", + "User": admin, }, "layouts/main") } @@ -207,3 +210,71 @@ func Benchmark_Amber(b *testing.B) { require.Equal(b, expectExtended, trim(buf.String())) }) } + +func Benchmark_Amber_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Main

Header

Hello, Admin!

Footer

` + engine := New("./views", ".amber") + engine.AddFunc("isAdmin", func(user string) bool { + return user == "admin" + }) + require.NoError(b, engine.Load()) + + b.Run("concurrent_simple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("concurrent_extended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + } + }) + }) + + b.Run("concurrent_simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("concurrent_extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) + }) +} diff --git a/html/html.go b/html/html.go index 66ba14a4..0a0c213d 100644 --- a/html/html.go +++ b/html/html.go @@ -60,7 +60,7 @@ func NewFileSystem(fs http.FileSystem, extension string) *Engine { // Load parses the templates to the engine. func (e *Engine) Load() error { - if e.Loaded { + if e.Loaded() { return nil } // race safe diff --git a/pug/pug.go b/pug/pug.go index 25149d57..db8d2150 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -120,33 +120,30 @@ func (e *Engine) Load() error { if err != nil { return err } - // Debugging - if e.Verbose { + + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } + // notify Engine that we parsed all templates - e.Loaded = true + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } + return filepath.Walk(e.Directory, walkFn) } // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - e.Mutex.RLock() - shouldReload := e.ShouldReload - e.Mutex.RUnlock() - - if !e.Loaded || shouldReload { - e.Mutex.Lock() - if e.ShouldReload { - e.Loaded = false + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } - e.Mutex.Unlock() if err := e.Load(); err != nil { return err @@ -169,6 +166,7 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout // Handle layout if specified if len(layout) > 0 && layout[0] != "" { lay := e.Templates.Lookup(layout[0]) + if lay == nil { return fmt.Errorf("render: layout %s does not exist", layout[0]) } diff --git a/template.go b/template.go index 964fb0ef..8b098c17 100644 --- a/template.go +++ b/template.go @@ -22,6 +22,10 @@ type IEngineCore interface { FuncMap() map[string]interface{} Layout(key string) IEngineCore Reload(enabled bool) IEngineCore + ShouldReload() bool + Loaded() bool + SetLoaded(enabled bool) IEngineCore + LockAndSetLoaded(enabled bool) IEngineCore } // Engine engine struct @@ -38,24 +42,24 @@ type Engine struct { Extension string // layout variable name that incapsulates the template LayoutName string - // determines if the engine parsed all templates - Loaded bool - // reload on each render - ShouldReload bool - // debug prints the parsed templates - Verbose bool // lock for funcmap and templates Mutex sync.RWMutex // template funcmap Funcmap map[string]interface{} + // debug prints the parsed templates + verbose bool + // determines if the engine parsed all templates + loaded bool + // reload on each render + shouldReload bool } // AddFunc adds the function to the template's function map. // It is legal to overwrite elements of the default actions func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { e.Mutex.Lock() + defer e.Mutex.Unlock() e.Funcmap[name] = fn - e.Mutex.Unlock() return e } @@ -63,18 +67,16 @@ func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { // It is legal to overwrite elements of the default actions func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore { e.Mutex.Lock() + defer e.Mutex.Unlock() for name, fn := range m { e.Funcmap[name] = fn } - e.Mutex.Unlock() return e } // Debug will print the parsed templates when Load is triggered. func (e *Engine) Debug(enabled bool) IEngineCore { - e.Mutex.Lock() - e.Verbose = enabled - e.Mutex.Unlock() + e.verbose = enabled return e } @@ -83,21 +85,23 @@ func (e *Engine) Debug(enabled bool) IEngineCore { // corresponding default: "{{" and "}}". func (e *Engine) Delims(left, right string) IEngineCore { e.Mutex.Lock() + defer e.Mutex.Unlock() e.Left, e.Right = left, right - e.Mutex.Unlock() return e } // FuncMap returns the template's function map. func (e *Engine) FuncMap() map[string]interface{} { + e.Mutex.RLock() + defer e.Mutex.RUnlock() return e.Funcmap } // Layout defines the variable name that will incapsulate the template func (e *Engine) Layout(key string) IEngineCore { e.Mutex.Lock() + defer e.Mutex.Unlock() e.LayoutName = key - e.Mutex.Unlock() return e } @@ -106,7 +110,33 @@ func (e *Engine) Layout(key string) IEngineCore { // the application when you edit a template file. func (e *Engine) Reload(enabled bool) IEngineCore { e.Mutex.Lock() - e.ShouldReload = enabled - e.Mutex.Unlock() + defer e.Mutex.Unlock() + e.shouldReload = enabled + return e +} + +func (e *Engine) ShouldReload() bool { + e.Mutex.RLock() + defer e.Mutex.RUnlock() + return e.shouldReload +} + +func (e *Engine) Loaded() bool { + e.Mutex.RLock() + defer e.Mutex.RUnlock() + return e.loaded +} +func (e *Engine) SetLoaded(enabled bool) IEngineCore { + e.loaded = enabled return e } + +func (e *Engine) LockAndSetLoaded(enabled bool) IEngineCore { + e.Mutex.Lock() + defer e.Mutex.Unlock() + return e.SetLoaded(enabled) +} + +func (e *Engine) Verbose() bool { + return e.verbose +} From e6ea29f1a5e49d71e8cf2f2f8a9c01afd6464860 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 28 Jan 2024 22:42:06 -0500 Subject: [PATCH 08/30] Simplify pug --- pug/pug.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/pug/pug.go b/pug/pug.go index db8d2150..cdc9aa0f 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -25,35 +25,29 @@ type Engine struct { // New returns a Pug render engine for Fiber func New(directory, extension string) *Engine { - engine := &Engine{ - Engine: core.Engine{ - Left: "{{", - Right: "}}", - Directory: directory, - Extension: extension, - LayoutName: "embed", - Funcmap: make(map[string]interface{}), - }, - } - engine.AddFunc(engine.LayoutName, func() error { - return fmt.Errorf("layoutName called unexpectedly") - }) - return engine + return newEngine(directory, extension, nil) } // NewFileSystem returns a Pug render engine for Fiber with file system func NewFileSystem(fs http.FileSystem, extension string) *Engine { + return newEngine("/", extension, fs) +} + +// newEngine creates a new Engine instance with common initialization logic. +func newEngine(directory, extension string, fs http.FileSystem) *Engine { engine := &Engine{ Engine: core.Engine{ Left: "{{", Right: "}}", - Directory: "/", + Directory: directory, FileSystem: fs, Extension: extension, LayoutName: "embed", Funcmap: make(map[string]interface{}), }, } + // Add a default function that throws an error if called unexpectedly. + // This can be useful for debugging or ensuring certain functions are used correctly. engine.AddFunc(engine.LayoutName, func() error { return fmt.Errorf("layoutName called unexpectedly") }) From 302a63f942fd6640bf5aa7507444e7b3d2e12acd Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 6 Feb 2024 08:33:23 -0500 Subject: [PATCH 09/30] Update Slim Engine --- django/django.go | 2 +- django/django_test.go | 26 +++++++++---------- handlebars/handlebars_test.go | 3 ++- jet/jet_test.go | 3 ++- mustache/mustache_test.go | 3 ++- pug/pug.go | 3 ++- slim/slim.go | 48 ++++++++++++++++++++--------------- template.go | 1 + 8 files changed, 51 insertions(+), 38 deletions(-) diff --git a/django/django.go b/django/django.go index 669d3f6e..188282b5 100644 --- a/django/django.go +++ b/django/django.go @@ -74,7 +74,7 @@ func NewPathForwardingFileSystem(fs http.FileSystem, directory, extension string LayoutName: "embed", Funcmap: make(map[string]interface{}), }, - autoEscape: true, + autoEscape: true, forwardPath: true, } return engine diff --git a/django/django_test.go b/django/django_test.go index 91cac797..e4c228a2 100644 --- a/django/django_test.go +++ b/django/django_test.go @@ -327,19 +327,19 @@ func Test_XSS(t *testing.T) { } func Test_XSS_WithAutoEscapeDisabled(t *testing.T) { - engine := New("./views", ".django") - engine.SetAutoEscape(false) - require.NoError(t, engine.Load()) - - var buf bytes.Buffer - err := engine.Render(&buf, "index", map[string]interface{}{ - "Title": "", - }, "layouts/main") - require.NoError(t, err) - - expect := `Main

Header

Footer

` - result := trim(buf.String()) - require.Equal(t, expect, result) + engine := New("./views", ".django") + engine.SetAutoEscape(false) + require.NoError(t, engine.Load()) + + var buf bytes.Buffer + err := engine.Render(&buf, "index", map[string]interface{}{ + "Title": "", + }, "layouts/main") + require.NoError(t, err) + + expect := `Main

Header

Footer

` + result := trim(buf.String()) + require.Equal(t, expect, result) } func Benchmark_Django(b *testing.B) { diff --git a/handlebars/handlebars_test.go b/handlebars/handlebars_test.go index dcade1f0..427610da 100644 --- a/handlebars/handlebars_test.go +++ b/handlebars/handlebars_test.go @@ -2,13 +2,14 @@ package handlebars import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/gofiber/fiber/v2" ) diff --git a/jet/jet_test.go b/jet/jet_test.go index 3791f96c..5cccfd08 100644 --- a/jet/jet_test.go +++ b/jet/jet_test.go @@ -2,12 +2,13 @@ package jet import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + + "github.com/stretchr/testify/require" ) func trim(str string) string { diff --git a/mustache/mustache_test.go b/mustache/mustache_test.go index 2d83d2e9..6dd1ca74 100644 --- a/mustache/mustache_test.go +++ b/mustache/mustache_test.go @@ -2,13 +2,14 @@ package mustache import ( "bytes" - "github.com/stretchr/testify/require" "net/http" "os" "regexp" "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/gofiber/fiber/v2" ) diff --git a/pug/pug.go b/pug/pug.go index cdc9aa0f..d61cf6ad 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -92,13 +92,14 @@ func (e *Engine) Load() error { name := filepath.ToSlash(rel) // Remove ext from name 'index.tmpl' -> 'index' name = strings.TrimSuffix(name, e.Extension) - // name = strings.Replace(name, e.extension, "", -1) + // Read the file // #gosec G304 buf, err := utils.ReadFile(path, e.FileSystem) if err != nil { return err } + // Create new template associated with the current one // This enable use to invoke other templates {{ template .. }} var pug string diff --git a/slim/slim.go b/slim/slim.go index 8e77c7a4..b7942f8f 100644 --- a/slim/slim.go +++ b/slim/slim.go @@ -27,26 +27,21 @@ type slimFunc = func(...slim.Value) (slim.Value, error) // New returns a Slim render engine for Fiber func New(directory, extension string) *Engine { - engine := &Engine{ - Engine: core.Engine{ - Left: "{{", - Right: "}}", - Directory: directory, - Extension: extension, - LayoutName: "embed", - Funcmap: make(map[string]interface{}), - }, - } - return engine + return newEngine(directory, extension, nil) } // NewFileSystem returns a Slim render engine for Fiber with file system func NewFileSystem(fs http.FileSystem, extension string) *Engine { + return newEngine("/", extension, fs) +} + +// newEngine creates a new Engine instance with common initialization logic. +func newEngine(directory, extension string, fs http.FileSystem) *Engine { engine := &Engine{ Engine: core.Engine{ Left: "{{", Right: "}}", - Directory: "/", + Directory: directory, FileSystem: fs, Extension: extension, LayoutName: "embed", @@ -111,16 +106,17 @@ func (e *Engine) Load() error { newFuncMap[key] = slimFunc } tmpl.FuncMap(newFuncMap) - e.Templates[name] = tmpl - // Debugging - if e.Verbose { + + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } - // notify engine that we parsed all templates - e.Loaded = true + + // notify Engine that we parsed all templates + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } @@ -129,18 +125,30 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates[name] + e.Mutex.RUnlock() + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + if len(layout) > 0 && layout[0] != "" { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) diff --git a/template.go b/template.go index 8b098c17..ab51a351 100644 --- a/template.go +++ b/template.go @@ -126,6 +126,7 @@ func (e *Engine) Loaded() bool { defer e.Mutex.RUnlock() return e.loaded } + func (e *Engine) SetLoaded(enabled bool) IEngineCore { e.loaded = enabled return e From ab910eea4205250d962be2f4dfdff2c2619ff801 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 6 Feb 2024 09:20:26 -0500 Subject: [PATCH 10/30] Update Mustache Engine --- mustache/mustache.go | 36 +++++++++++++++++++++++++++++------- mustache/mustache_test.go | 23 ++++++++++++++++------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/mustache/mustache.go b/mustache/mustache.go index ac0501d0..1ebaf9c7 100644 --- a/mustache/mustache.go +++ b/mustache/mustache.go @@ -85,20 +85,24 @@ func (e *Engine) Load() error { if err != nil { return err } + // Skip file if it's a directory or has no file info if info == nil || info.IsDir() { return nil } + // Skip file if it does not equal the given template extension if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension { return nil } + // Get the relative file path // ./views/html/index.tmpl -> index.tmpl rel, err := filepath.Rel(e.Directory, path) if err != nil { return err } + // Reverse slashes '\' -> '/' and // partials\footer.tmpl -> partials/footer.tmpl name := filepath.ToSlash(rel) @@ -111,6 +115,7 @@ func (e *Engine) Load() error { if err != nil { return err } + // Create new template associated with the current one // This enable use to invoke other templates {{ template .. }} var tmpl *mustache.Template @@ -122,15 +127,17 @@ func (e *Engine) Load() error { if err != nil { return err } + e.Templates[name] = tmpl - // Debugging - if e.Verbose { + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } - // notify engine that we parsed all templates - e.Loaded = true + + // notify Engine that we parsed all templates + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } @@ -139,25 +146,39 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates[name] + e.Mutex.RUnlock() + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + if len(layout) > 0 && layout[0] != "" { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) if err := tmpl.FRender(buf, binding); err != nil { return err } + var bind map[string]interface{} + switch binds := binding.(type) { case fiber.Map: bind = binds @@ -166,6 +187,7 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout default: bind = make(map[string]interface{}, 1) } + bind[e.LayoutName] = buf.String() lay := e.Templates[layout[0]] if lay == nil { diff --git a/mustache/mustache_test.go b/mustache/mustache_test.go index 6dd1ca74..417de33f 100644 --- a/mustache/mustache_test.go +++ b/mustache/mustache_test.go @@ -124,21 +124,30 @@ func Test_Reload(t *testing.T) { func Benchmark_Mustache(b *testing.B) { expectSimple := `

Hello, World!

` engine := New("./views", ".mustache") - - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() for i := 0; i < bb.N; i++ { - buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } + }) - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for i := 0; i < bb.N; i++ { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } }) } From 4f7ab6b14ceae9a9baf0d413b5b57c3797c201a4 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 6 Feb 2024 22:22:28 -0500 Subject: [PATCH 11/30] Update Jet Engine --- jet/jet.go | 33 ++++++++++---- jet/jet_test.go | 111 ++++++++++++++++++++++++++++++++++++++++++++---- pug/pug_test.go | 12 ++++-- 3 files changed, 135 insertions(+), 21 deletions(-) diff --git a/jet/jet.go b/jet/jet.go index e46e14b0..972998c9 100644 --- a/jet/jet.go +++ b/jet/jet.go @@ -71,7 +71,6 @@ func (e *Engine) Load() error { // parse templates // e.Templates = jet.NewHTMLSet(e.Directory) - var loader jet.Loader var err error @@ -84,7 +83,8 @@ func (e *Engine) Load() error { } else { loader = jet.NewInMemLoader() } - if e.Verbose { + + if e.Verbose() { e.Templates = jet.NewSet( loader, jet.WithDelims(e.Left, e.Right), @@ -107,19 +107,23 @@ func (e *Engine) Load() error { if err != nil { return err } + // Skip file if it's a directory or has no file info if info == nil || info.IsDir() { return nil } + // Skip file if it does not equal the given template Extension if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension { return nil } + // ./views/html/index.tmpl -> index.tmpl rel, err := filepath.Rel(e.Directory, path) if err != nil { return err } + name := strings.TrimSuffix(rel, e.Extension) // Read the file // #gosec G304 @@ -129,15 +133,15 @@ func (e *Engine) Load() error { } l.Set(name, string(buf)) - // Debugging - if e.Verbose { + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } - e.Loaded = true + // notify Engine that we parsed all templates + e.SetLoaded(true) if _, ok := loader.(*jet.InMemLoader); ok { return filepath.Walk(e.Directory, walkFn) @@ -148,19 +152,32 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl, err := e.Templates.GetTemplate(name) + e.Mutex.RUnlock() + if err != nil || tmpl == nil { return fmt.Errorf("render: template %s could not be Loaded: %w", name, err) } + + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + bind := jetVarMap(binding) + if len(layout) > 0 && layout[0] != "" { lay, err := e.Templates.GetTemplate(layout[0]) if err != nil { diff --git a/jet/jet_test.go b/jet/jet_test.go index 5cccfd08..eef2c91b 100644 --- a/jet/jet_test.go +++ b/jet/jet_test.go @@ -169,34 +169,127 @@ func Benchmark_Jet(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == "admin" }) - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": "admin", }, "layouts/main") } + }) + + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + + b.Run("extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) +} + +func Benchmark_Jet_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Title

Header

Hello, Admin!

Footer

` + engine := New("./views", ".jet") + engine.AddFunc("isAdmin", func(user string) bool { + return user == "admin" + }) + require.NoError(b, engine.Load()) + + b.Run("concurrent_simple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) + b.Run("concurrent_extended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + }, "layouts/main") + } + }) + }) + + b.Run("concurrent_simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("concurrent_extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) }) } diff --git a/pug/pug_test.go b/pug/pug_test.go index d9de0c51..c6f5095f 100644 --- a/pug/pug_test.go +++ b/pug/pug_test.go @@ -195,8 +195,9 @@ func Benchmark_Pug(b *testing.B) { b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", @@ -207,8 +208,9 @@ func Benchmark_Pug(b *testing.B) { b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, @@ -219,8 +221,9 @@ func Benchmark_Pug(b *testing.B) { b.Run("simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() err := engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) @@ -232,8 +235,9 @@ func Benchmark_Pug(b *testing.B) { b.Run("extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() err := engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") From 51c1c165c69c5f57cd6a75e586bb6fe279d0c0a0 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 6 Feb 2024 22:31:56 -0500 Subject: [PATCH 12/30] Update HTML engine --- html/html.go | 58 ++++++++++++++++++++++++++++------------------- html/html_test.go | 12 ++++++---- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/html/html.go b/html/html.go index 0a0c213d..38b320f3 100644 --- a/html/html.go +++ b/html/html.go @@ -21,37 +21,31 @@ type Engine struct { Templates *template.Template } -// New returns an HTML render engine for Fiber +// New returns a HTML render engine for Fiber func New(directory, extension string) *Engine { - engine := &Engine{ - Engine: core.Engine{ - Left: "{{", - Right: "}}", - Directory: directory, - Extension: extension, - LayoutName: "embed", - Funcmap: make(map[string]interface{}), - }, - } - engine.AddFunc(engine.LayoutName, func() error { - return fmt.Errorf("layoutName called unexpectedly") - }) - return engine + return newEngine(directory, extension, nil) } -// NewFileSystem returns an HTML render engine for Fiber with file system +// NewFileSystem returns a HTML render engine for Fiber with file system func NewFileSystem(fs http.FileSystem, extension string) *Engine { + return newEngine("/", extension, fs) +} + +// newEngine creates a new Engine instance with common initialization logic. +func newEngine(directory, extension string, fs http.FileSystem) *Engine { engine := &Engine{ Engine: core.Engine{ Left: "{{", Right: "}}", - Directory: "/", + Directory: directory, FileSystem: fs, Extension: extension, LayoutName: "embed", Funcmap: make(map[string]interface{}), }, } + // Add a default function that throws an error if called unexpectedly. + // This can be useful for debugging or ensuring certain functions are used correctly. engine.AddFunc(engine.LayoutName, func() error { return fmt.Errorf("layoutName called unexpectedly") }) @@ -63,6 +57,7 @@ func (e *Engine) Load() error { if e.Loaded() { return nil } + // race safe e.Mutex.Lock() defer e.Mutex.Unlock() @@ -77,20 +72,24 @@ func (e *Engine) Load() error { if err != nil { return err } + // Skip file if it's a directory or has no file info if info == nil || info.IsDir() { return nil } + // Skip file if it does not equal the given template Extension if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension { return nil } + // Get the relative file path // ./views/html/index.tmpl -> index.tmpl rel, err := filepath.Rel(e.Directory, path) if err != nil { return err } + // Reverse slashes '\' -> '/' and // partials\footer.tmpl -> partials/footer.tmpl name := filepath.ToSlash(rel) @@ -103,20 +102,24 @@ func (e *Engine) Load() error { if err != nil { return err } + // Create new template associated with the current one // This enable use to invoke other templates {{ template .. }} _, err = e.Templates.New(name).Parse(string(buf)) if err != nil { return err } + // Debugging - if e.Verbose { + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } - // notify engine that we parsed all templates - e.Loaded = true + + // notify Engine that we parsed all templates + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } @@ -125,23 +128,32 @@ func (e *Engine) Load() error { // Render will execute the template name along with the given values. func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates.Lookup(name) + e.Mutex.RUnlock() + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + render := renderFuncCreate(e, out, binding, *tmpl, nil) if len(layout) > 0 && layout[0] != "" { e.Mutex.Lock() defer e.Mutex.Unlock() } + // construct a nested render function to embed templates in layouts for _, layName := range layout { if layName == "" { diff --git a/html/html_test.go b/html/html_test.go index fb02ce69..17e05de9 100644 --- a/html/html_test.go +++ b/html/html_test.go @@ -272,8 +272,9 @@ func Benchmark_Html(b *testing.B) { b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", @@ -284,8 +285,9 @@ func Benchmark_Html(b *testing.B) { b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, @@ -296,8 +298,9 @@ func Benchmark_Html(b *testing.B) { b.Run("simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() err := engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) @@ -310,8 +313,9 @@ func Benchmark_Html(b *testing.B) { b.Run("extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { - var buf bytes.Buffer + buf.Reset() err := engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") From 25460a2ad3227fdc693646cffd0e2aded3b2f043 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 19:33:54 -0500 Subject: [PATCH 13/30] More fixes --- ace/ace.go | 23 ++-- amber/amber.go | 26 +++- amber/amber_test.go | 43 +++++-- django/django.go | 102 ++++++++-------- django/django_test.go | 221 +++++++++++++++++++++++++++++++--- handlebars/handlebars.go | 48 ++++++-- handlebars/handlebars_test.go | 115 ++++++++++++++++-- 7 files changed, 459 insertions(+), 119 deletions(-) diff --git a/ace/ace.go b/ace/ace.go index 17272229..469702aa 100644 --- a/ace/ace.go +++ b/ace/ace.go @@ -124,14 +124,16 @@ func (e *Engine) Load() error { if err != nil { return err } - // Debugging - if e.Verbose { + + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } - // notify engine that we parsed all templates - e.Loaded = true + + // notify Engine that we parsed all templates + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } @@ -141,17 +143,10 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - e.Mutex.RLock() - shouldReload := e.ShouldReload - e.Mutex.RUnlock() - - // ShouldReload the views - if !e.Loaded || shouldReload { - e.Mutex.Lock() - if e.ShouldReload { - e.Loaded = false + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } - e.Mutex.Unlock() ace.FlushCache() if err := e.Load(); err != nil { diff --git a/amber/amber.go b/amber/amber.go index 4cf3813d..44b37f8a 100644 --- a/amber/amber.go +++ b/amber/amber.go @@ -120,14 +120,16 @@ func (e *Engine) Load() error { return err } e.Templates[name] = tmpl - // Debugging - if e.Verbose { + + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } + // notify Engine that we parsed all templates - e.Loaded = true + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } @@ -136,18 +138,30 @@ func (e *Engine) Load() error { // Render will execute the template name along with the given values. func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates[name] + e.Mutex.RUnlock() + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + if len(layout) > 0 && layout[0] != "" { lay := e.Templates[layout[0]] if lay == nil { diff --git a/amber/amber_test.go b/amber/amber_test.go index cbd704cb..466c1003 100644 --- a/amber/amber_test.go +++ b/amber/amber_test.go @@ -179,35 +179,58 @@ func Benchmark_Amber(b *testing.B) { }) require.NoError(b, engine.Load()) - var buf bytes.Buffer - var err error - b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") } + }) + + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) + b.Run("extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } }) } diff --git a/django/django.go b/django/django.go index 188282b5..1cff3fc0 100644 --- a/django/django.go +++ b/django/django.go @@ -27,56 +27,39 @@ type Engine struct { Templates map[string]*pongo2.Template } -// New returns a Django render engine for Fiber -func New(directory, extension string) *Engine { - engine := &Engine{ - Engine: core.Engine{ - Left: "{{", - Right: "}}", - Directory: directory, - Extension: extension, - LayoutName: "embed", - Funcmap: make(map[string]interface{}), - }, - autoEscape: true, +// This helper function is used to avoid duplication in public constructors. +func (e *Engine) initialize(directory, extension string, fs http.FileSystem) { + e.Engine.Left = "{{" + e.Engine.Right = "}}" + e.Engine.Directory = directory + e.Engine.Extension = extension + e.Engine.LayoutName = "embed" + e.Engine.Funcmap = make(map[string]interface{}) + e.autoEscape = true + if fs != nil { + e.Engine.FileSystem = fs } +} + +// New creates a new Engine with a directory and extension. +func New(directory, extension string) *Engine { + engine := &Engine{} + engine.initialize(directory, extension, nil) return engine } -// NewFileSystem returns a Django render engine for Fiber with file system +// NewFileSystem creates a new Engine with a file system and extension. func NewFileSystem(fs http.FileSystem, extension string) *Engine { - engine := &Engine{ - Engine: core.Engine{ - Left: "{{", - Right: "}}", - Directory: "/", - FileSystem: fs, - Extension: extension, - LayoutName: "embed", - Funcmap: make(map[string]interface{}), - }, - autoEscape: true, - } + engine := &Engine{} + engine.initialize("/", extension, fs) return engine } -// NewPathForwardingFileSystem Passes "Directory" to the template engine where alternative functions don't. -// -// This fixes errors during resolution of templates when "{% extends 'parent.html' %}" is used. +// NewPathForwardingFileSystem creates a new Engine with path forwarding, +// using a file system, directory, and extension. func NewPathForwardingFileSystem(fs http.FileSystem, directory, extension string) *Engine { - engine := &Engine{ - Engine: core.Engine{ - Left: "{{", - Right: "}}", - Directory: directory, - FileSystem: fs, - Extension: extension, - LayoutName: "embed", - Funcmap: make(map[string]interface{}), - }, - autoEscape: true, - forwardPath: true, - } + engine := &Engine{forwardPath: true} + engine.initialize(directory, extension, fs) return engine } @@ -87,10 +70,9 @@ func (e *Engine) Load() error { defer e.Mutex.Unlock() e.Templates = make(map[string]*pongo2.Template) - baseDir := e.Directory - var pongoloader pongo2.TemplateLoader + if e.FileSystem != nil { // ensures creation of httpFileSystemLoader only when filesystem is defined if e.forwardPath { @@ -115,20 +97,24 @@ func (e *Engine) Load() error { if err != nil { return err } + // Skip file if it's a directory or has no file info if info == nil || info.IsDir() { return nil } + // Skip file if it does not equal the given template Extension if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension { return nil } + // Get the relative file path // ./views/html/index.tmpl -> index.tmpl rel, err := filepath.Rel(e.Directory, path) if err != nil { return err } + // Reverse slashes '\' -> '/' and // partials\footer.tmpl -> partials/footer.tmpl name := filepath.ToSlash(rel) @@ -141,20 +127,23 @@ func (e *Engine) Load() error { if err != nil { return err } + // Create new template associated with the current one tmpl, err := pongoset.FromBytes(buf) if err != nil { return err } e.Templates[name] = tmpl - // Debugging - if e.Verbose { + + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err } - // notify engine that we parsed all templates - e.Loaded = true + + // notify Engine that we parsed all templates + e.SetLoaded(true) + if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) } @@ -174,6 +163,7 @@ func getPongoBinding(binding interface{}) pongo2.Context { if binding == nil { return nil } + var bind pongo2.Context switch binds := binding.(type) { case pongo2.Context: @@ -220,24 +210,36 @@ func (e *Engine) SetAutoEscape(autoEscape bool) { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl, ok := e.Templates[name] + e.Mutex.RUnlock() + if !ok { return fmt.Errorf("template %s does not exist", name) } + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + bind := getPongoBinding(binding) parsed, err := tmpl.Execute(bind) if err != nil { return err } + if len(layout) > 0 && layout[0] != "" { if bind == nil { bind = make(map[string]interface{}, 1) diff --git a/django/django_test.go b/django/django_test.go index e4c228a2..0ee702ec 100644 --- a/django/django_test.go +++ b/django/django_test.go @@ -349,65 +349,250 @@ func Benchmark_Django(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == admin }) - - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, }, "layouts/main") } - - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) }) b.Run("simple_with_invalid_binding_keys", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", "Invalid_Key": "Don't return error from checkForValidIdentifiers!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended_with_invalid_binding_keys", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ "User": admin, "Invalid_Key": "Don't return error from checkForValidIdentifiers!", }, "layouts/main") } + }) + + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + + b.Run("extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) + + b.Run("simple_with_invalid_binding_keys_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + "Invalid_Key": "Don't return error from checkForValidIdentifiers!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) + b.Run("extended_with_invalid_binding_keys_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": admin, + "Invalid_Key": "Don't return error from checkForValidIdentifiers!", + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) +} +func Benchmark_Django_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Main

Header

Hello, Admin!

Footer

` + engine := New("./views", ".django") + engine.AddFunc("isAdmin", func(user string) bool { + return user == "admin" + }) + require.NoError(b, engine.Load()) + + b.Run("simple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + for pb.Next() { + buf.Reset() + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("extended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + for pb.Next() { + buf.Reset() + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + }, "layouts/main") + } + }) + }) + + b.Run("simple_with_invalid_binding_keys", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + for pb.Next() { + buf.Reset() + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + "Invalid_Key": "Don't return error from checkForValidIdentifiers!", + }) + } + }) + }) + + b.Run("extended_with_invalid_binding_keys", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + for pb.Next() { + buf.Reset() + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + "Invalid_Key": "Don't return error from checkForValidIdentifiers!", + }, "layouts/main") + } + }) + }) + + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) + }) + + b.Run("simple_with_invalid_binding_keys_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + "Invalid_Key": "Don't return error from checkForValidIdentifiers!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("extended_with_invalid_binding_keys_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + "Invalid_Key": "Don't return error from checkForValidIdentifiers!", + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) }) } diff --git a/handlebars/handlebars.go b/handlebars/handlebars.go index b6444af0..d1f1fa67 100644 --- a/handlebars/handlebars.go +++ b/handlebars/handlebars.go @@ -36,6 +36,11 @@ func New(directory, extension string) *Engine { Funcmap: make(map[string]interface{}), }, } + // Set template settings + engine.Templates = make(map[string]*raymond.Template) + engine.registerHelpersOnce.Do(func() { + raymond.RegisterHelpers(engine.Funcmap) + }) return engine } @@ -50,6 +55,11 @@ func NewFileSystem(fs http.FileSystem, extension string) *Engine { Funcmap: make(map[string]interface{}), }, } + // Set template settings + engine.Templates = make(map[string]*raymond.Template) + engine.registerHelpersOnce.Do(func() { + raymond.RegisterHelpers(engine.Funcmap) + }) return engine } @@ -59,25 +69,24 @@ func (e *Engine) Load() error { e.Mutex.Lock() defer e.Mutex.Unlock() var err error - // Set template settings - e.Templates = make(map[string]*raymond.Template) - e.registerHelpersOnce.Do(func() { - raymond.RegisterHelpers(e.Funcmap) - }) + // Loop trough each directory and register template files walkFn := func(path string, info os.FileInfo, err error) error { // Return error if exist if err != nil { return err } + // Skip file if it's a directory or has no file info if info == nil || info.IsDir() { return nil } + // Skip file if it does not equal the given template Extension if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension { return nil } + // Get the relative file path // ./views/html/index.tmpl -> index.tmpl rel, err := filepath.Rel(e.Directory, path) @@ -107,7 +116,7 @@ func (e *Engine) Load() error { e.Templates[name] = tmpl // Debugging - if e.Verbose { + if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } return err @@ -124,34 +133,49 @@ func (e *Engine) Load() error { } } // notify Engine that we parsed all templates - e.Loaded = true + e.SetLoaded(true) return err } // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { - if !e.Loaded || e.ShouldReload { - if e.ShouldReload { - e.Loaded = false + // Check if templates need to be loaded/reloaded + if !e.Loaded() || e.ShouldReload() { + if e.ShouldReload() { + e.LockAndSetLoaded(false) } + if err := e.Load(); err != nil { return err } } + + // Acquire read lock for accessing the template + e.Mutex.RLock() tmpl := e.Templates[name] + e.Mutex.RUnlock() + if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } + parsed, err := tmpl.Exec(binding) + if err != nil { return fmt.Errorf("render: %w", err) } + + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + if len(layout) > 0 && layout[0] != "" { + var bind map[string]interface{} lay := e.Templates[layout[0]] if lay == nil { return fmt.Errorf("render: LayoutName %s does not exist", layout[0]) } - var bind map[string]interface{} + switch binds := binding.(type) { case fiber.Map: bind = binds @@ -160,6 +184,7 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout default: bind = make(map[string]interface{}, 1) } + bind[e.LayoutName] = raymond.SafeString(parsed) parsed, err := lay.Exec(bind) if err != nil { @@ -170,6 +195,7 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout } return nil } + if _, err = out.Write([]byte(parsed)); err != nil { return fmt.Errorf("render: %w", err) } diff --git a/handlebars/handlebars_test.go b/handlebars/handlebars_test.go index 427610da..130e63fc 100644 --- a/handlebars/handlebars_test.go +++ b/handlebars/handlebars_test.go @@ -177,36 +177,131 @@ func Benchmark_Handlebars(b *testing.B) { engine.AddFunc("isAdmin", func(user string) bool { return user == "admin" }) - - var buf bytes.Buffer - var err error + require.NoError(b, engine.Load()) b.Run("simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "simple", map[string]interface{}{ + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ "Title": "Hello, World!", }) } - - require.NoError(b, err) - require.Equal(b, expectSimple, trim(buf.String())) }) b.Run("extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + "Title": "Main", + }, "layouts/main") + } + }) + + b.Run("simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer + for i := 0; i < bb.N; i++ { + buf.Reset() + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, World!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + + b.Run("extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + var buf bytes.Buffer for i := 0; i < bb.N; i++ { buf.Reset() - err = engine.Render(&buf, "extended", map[string]interface{}{ + err := engine.Render(&buf, "extended", map[string]interface{}{ "User": "admin", "Title": "Main", }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) } + }) +} + +func Benchmark_Handlebars_Concurrent(b *testing.B) { + expectSimple := `

Hello, Concurrent!

` + expectExtended := `Main

Header

Hello, Admin!

Footer

` + engine := New("./views", ".hbs") + engine.AddFunc("isAdmin", func(user string) bool { + return user == "admin" + }) + require.NoError(b, engine.Load()) + + b.Run("concurrent_simple", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + } + }) + }) + + b.Run("concurrent_extended", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + //nolint:gosec,errcheck // Return value not needed for benchmark + _ = engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + "Title": "Main", + }, "layouts/main") + } + }) + }) - require.NoError(b, err) - require.Equal(b, expectExtended, trim(buf.String())) + b.Run("concurrent_simple_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "simple", map[string]interface{}{ + "Title": "Hello, Concurrent!", + }) + require.NoError(bb, err) + require.Equal(bb, expectSimple, trim(buf.String())) + } + }) + }) + + b.Run("concurrent_extended_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + bb.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var buf bytes.Buffer + err := engine.Render(&buf, "extended", map[string]interface{}{ + "User": "admin", + "Title": "Main", + }, "layouts/main") + require.NoError(bb, err) + require.Equal(bb, expectExtended, trim(buf.String())) + } + }) }) } From 7d4fe73fac8b13dd4b760ed1bc277f7561cb67fb Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 19:35:20 -0500 Subject: [PATCH 14/30] Run code through gofumpt --- django/django_test.go | 1 + handlebars/handlebars.go | 1 - jet/jet.go | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/django/django_test.go b/django/django_test.go index 0ee702ec..505190bf 100644 --- a/django/django_test.go +++ b/django/django_test.go @@ -463,6 +463,7 @@ func Benchmark_Django(b *testing.B) { } }) } + func Benchmark_Django_Concurrent(b *testing.B) { expectSimple := `

Hello, Concurrent!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` diff --git a/handlebars/handlebars.go b/handlebars/handlebars.go index d1f1fa67..c6ca3a5d 100644 --- a/handlebars/handlebars.go +++ b/handlebars/handlebars.go @@ -160,7 +160,6 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout } parsed, err := tmpl.Exec(binding) - if err != nil { return fmt.Errorf("render: %w", err) } diff --git a/jet/jet.go b/jet/jet.go index 972998c9..c6bce575 100644 --- a/jet/jet.go +++ b/jet/jet.go @@ -76,7 +76,6 @@ func (e *Engine) Load() error { if e.FileSystem != nil { loader, err = httpfs.NewLoader(e.FileSystem) - if err != nil { return err } From f92c1846b8dd3afb4b08dc0509a1c7e652aff5fe Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 19:43:53 -0500 Subject: [PATCH 15/30] Update handlebars --- handlebars/handlebars.go | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/handlebars/handlebars.go b/handlebars/handlebars.go index c6ca3a5d..99b6f2cf 100644 --- a/handlebars/handlebars.go +++ b/handlebars/handlebars.go @@ -36,11 +36,6 @@ func New(directory, extension string) *Engine { Funcmap: make(map[string]interface{}), }, } - // Set template settings - engine.Templates = make(map[string]*raymond.Template) - engine.registerHelpersOnce.Do(func() { - raymond.RegisterHelpers(engine.Funcmap) - }) return engine } @@ -55,11 +50,6 @@ func NewFileSystem(fs http.FileSystem, extension string) *Engine { Funcmap: make(map[string]interface{}), }, } - // Set template settings - engine.Templates = make(map[string]*raymond.Template) - engine.registerHelpersOnce.Do(func() { - raymond.RegisterHelpers(engine.Funcmap) - }) return engine } @@ -69,24 +59,25 @@ func (e *Engine) Load() error { e.Mutex.Lock() defer e.Mutex.Unlock() var err error - + // Set template settings + e.Templates = make(map[string]*raymond.Template) + e.registerHelpersOnce.Do(func() { + raymond.RegisterHelpers(e.Funcmap) + }) // Loop trough each directory and register template files walkFn := func(path string, info os.FileInfo, err error) error { // Return error if exist if err != nil { return err } - // Skip file if it's a directory or has no file info if info == nil || info.IsDir() { return nil } - // Skip file if it does not equal the given template Extension if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension { return nil } - // Get the relative file path // ./views/html/index.tmpl -> index.tmpl rel, err := filepath.Rel(e.Directory, path) @@ -115,7 +106,6 @@ func (e *Engine) Load() error { // raymond.RegisterPartialTemplate(name, tmpl) e.Templates[name] = tmpl - // Debugging if e.Verbose() { log.Printf("views: parsed template: %s\n", name) } @@ -132,6 +122,7 @@ func (e *Engine) Load() error { e.Templates[j].RegisterPartialTemplate(n, template) } } + // notify Engine that we parsed all templates e.SetLoaded(true) return err @@ -150,11 +141,11 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout } } - // Acquire read lock for accessing the template - e.Mutex.RLock() - tmpl := e.Templates[name] - e.Mutex.RUnlock() + // Lock while executing layout + e.Mutex.Lock() + defer e.Mutex.Unlock() + tmpl := e.Templates[name] if tmpl == nil { return fmt.Errorf("render: template %s does not exist", name) } @@ -164,17 +155,12 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout return fmt.Errorf("render: %w", err) } - // Lock while executing layout - e.Mutex.Lock() - defer e.Mutex.Unlock() - if len(layout) > 0 && layout[0] != "" { - var bind map[string]interface{} lay := e.Templates[layout[0]] if lay == nil { return fmt.Errorf("render: LayoutName %s does not exist", layout[0]) } - + var bind map[string]interface{} switch binds := binding.(type) { case fiber.Map: bind = binds @@ -183,7 +169,6 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout default: bind = make(map[string]interface{}, 1) } - bind[e.LayoutName] = raymond.SafeString(parsed) parsed, err := lay.Exec(bind) if err != nil { @@ -194,7 +179,6 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout } return nil } - if _, err = out.Write([]byte(parsed)); err != nil { return fmt.Errorf("render: %w", err) } From e1fda5aa01225c455e872b5dc8b5dc9a6dc7f7a1 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 20:24:41 -0500 Subject: [PATCH 16/30] Migrate handlebars to mailgun fork. Add go1.22, remove older go versions from test matrix --- .github/workflows/benchmark.yml | 1 + .github/workflows/golangci-lint.yml | 3 +++ .github/workflows/test-ace.yml | 4 +--- .github/workflows/test-amber.yml | 4 +--- .github/workflows/test-django.yml | 3 +-- .github/workflows/test-handlebars.yml | 4 +--- .github/workflows/test-html.yml | 4 +--- .github/workflows/test-jet.yml | 4 +--- .github/workflows/test-mustache.yml | 4 +--- .github/workflows/test-pug.yml | 4 +--- .github/workflows/test-slim.yml | 4 +--- handlebars/go.mod | 4 ++-- handlebars/go.sum | 12 ++++++++++-- handlebars/handlebars.go | 3 ++- handlebars/handlebars_test.go | 4 +--- template.go | 5 +++++ 16 files changed, 33 insertions(+), 34 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 7580588e..ba38ee13 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -31,6 +31,7 @@ jobs: with: # NOTE: Keep this in sync with the version from go.mod go-version: "1.20.x" + cache: false - name: Run Benchmarks run: | diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 2e5482d3..3566ec42 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,6 +21,8 @@ on: permissions: contents: read + pull-requests: read + checks: write jobs: golangci-lint: @@ -33,6 +35,7 @@ jobs: with: # NOTE: Keep this in sync with the version from go.mod go-version: '1.20.x' + cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: diff --git a/.github/workflows/test-ace.yml b/.github/workflows/test-ace.yml index dfdf0f88..fa13a756 100644 --- a/.github/workflows/test-ace.yml +++ b/.github/workflows/test-ace.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-amber.yml b/.github/workflows/test-amber.yml index 9794c371..d809e98c 100644 --- a/.github/workflows/test-amber.yml +++ b/.github/workflows/test-amber.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-django.yml b/.github/workflows/test-django.yml index d1ae54e4..5cf1b2cf 100644 --- a/.github/workflows/test-django.yml +++ b/.github/workflows/test-django.yml @@ -16,10 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-handlebars.yml b/.github/workflows/test-handlebars.yml index f1f9ee3f..2fd56c13 100644 --- a/.github/workflows/test-handlebars.yml +++ b/.github/workflows/test-handlebars.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-html.yml b/.github/workflows/test-html.yml index 31ce0aae..0ce84e6e 100644 --- a/.github/workflows/test-html.yml +++ b/.github/workflows/test-html.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-jet.yml b/.github/workflows/test-jet.yml index 277d7f92..9f1f00f0 100644 --- a/.github/workflows/test-jet.yml +++ b/.github/workflows/test-jet.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-mustache.yml b/.github/workflows/test-mustache.yml index f15cb87a..22b47f6f 100644 --- a/.github/workflows/test-mustache.yml +++ b/.github/workflows/test-mustache.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-pug.yml b/.github/workflows/test-pug.yml index e9c35f9c..1d884aa6 100644 --- a/.github/workflows/test-pug.yml +++ b/.github/workflows/test-pug.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test-slim.yml b/.github/workflows/test-slim.yml index 90ce9a67..75545606 100644 --- a/.github/workflows/test-slim.yml +++ b/.github/workflows/test-slim.yml @@ -16,11 +16,9 @@ jobs: strategy: matrix: go-version: - - 1.17.x - - 1.18.x - - 1.19.x - 1.20.x - 1.21.x + - 1.22.x platform: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/handlebars/go.mod b/handlebars/go.mod index e2479e55..5074ed23 100644 --- a/handlebars/go.mod +++ b/handlebars/go.mod @@ -3,10 +3,10 @@ module github.com/gofiber/template/handlebars/v2 go 1.20 require ( - github.com/aymerick/raymond v2.0.2+incompatible github.com/gofiber/fiber/v2 v2.52.0 github.com/gofiber/template v1.8.2 github.com/gofiber/utils v1.1.0 + github.com/mailgun/raymond/v2 v2.0.48 github.com/stretchr/testify v1.8.4 ) @@ -20,11 +20,11 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.15.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/handlebars/go.sum b/handlebars/go.sum index 0df30afd..3d3ccdd7 100644 --- a/handlebars/go.sum +++ b/handlebars/go.sum @@ -1,7 +1,6 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= -github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE= @@ -12,6 +11,8 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= +github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -23,6 +24,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -31,6 +37,7 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= @@ -39,5 +46,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlebars/handlebars.go b/handlebars/handlebars.go index 99b6f2cf..e0aa8a5f 100644 --- a/handlebars/handlebars.go +++ b/handlebars/handlebars.go @@ -12,9 +12,9 @@ import ( "github.com/gofiber/fiber/v2" - "github.com/aymerick/raymond" core "github.com/gofiber/template" "github.com/gofiber/utils" + "github.com/mailgun/raymond/v2" ) // Engine struct @@ -59,6 +59,7 @@ func (e *Engine) Load() error { e.Mutex.Lock() defer e.Mutex.Unlock() var err error + // Set template settings e.Templates = make(map[string]*raymond.Template) e.registerHelpersOnce.Do(func() { diff --git a/handlebars/handlebars_test.go b/handlebars/handlebars_test.go index 130e63fc..8d36a8c6 100644 --- a/handlebars/handlebars_test.go +++ b/handlebars/handlebars_test.go @@ -240,9 +240,7 @@ func Benchmark_Handlebars_Concurrent(b *testing.B) { expectSimple := `

Hello, Concurrent!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".hbs") - engine.AddFunc("isAdmin", func(user string) bool { - return user == "admin" - }) + // Note we don't register isAdmin again because handlebars does not support re-registering helpers require.NoError(b, engine.Load()) b.Run("concurrent_simple", func(bb *testing.B) { diff --git a/template.go b/template.go index ab51a351..073297ae 100644 --- a/template.go +++ b/template.go @@ -115,29 +115,34 @@ func (e *Engine) Reload(enabled bool) IEngineCore { return e } +// ShouldReload returns true if the templates should be reloaded func (e *Engine) ShouldReload() bool { e.Mutex.RLock() defer e.Mutex.RUnlock() return e.shouldReload } +// Loaded returns true if templates are loaded func (e *Engine) Loaded() bool { e.Mutex.RLock() defer e.Mutex.RUnlock() return e.loaded } +// SetLoaded sets the loaded status func (e *Engine) SetLoaded(enabled bool) IEngineCore { e.loaded = enabled return e } +// LockAndSetLoaded locks and sets the loaded status func (e *Engine) LockAndSetLoaded(enabled bool) IEngineCore { e.Mutex.Lock() defer e.Mutex.Unlock() return e.SetLoaded(enabled) } +// Verbose returns the verbose status func (e *Engine) Verbose() bool { return e.verbose } From b3c666f0b94e2eab65c917ee5b7a325ce1c93732 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 20:42:24 -0500 Subject: [PATCH 17/30] Bump outdated actions --- .github/workflows/benchmark.yml | 2 +- .github/workflows/gosec.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ba38ee13..d7337893 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -44,7 +44,7 @@ jobs: shell: bash - name: Get Previous Benchmark Results - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./cache key: ${{ runner.os }}-benchmark diff --git a/.github/workflows/gosec.yml b/.github/workflows/gosec.yml index d34eba74..1ffa1411 100644 --- a/.github/workflows/gosec.yml +++ b/.github/workflows/gosec.yml @@ -29,7 +29,7 @@ jobs: with: fetch-depth: 0 - name: Changed Files - uses: tj-actions/changed-files@v41 + uses: tj-actions/changed-files@v42 id: changed-files with: files_ignore: | From 49979f776bd190541b983d528cc60432cc6fdde2 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 20:43:32 -0500 Subject: [PATCH 18/30] Run benchmarks if benchmark.yml changes --- .github/workflows/benchmark.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index d7337893..6ee1fb86 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -8,12 +8,14 @@ on: paths: - "**.go" - "**/go.mod" + - ".github/workflows/benchmark.yml" pull_request: branches: - "*" paths: - "**.go" - "**/go.mod" + - ".github/workflows/benchmark.yml" permissions: deployments: write From d99b60dd0bdd8725e4c5542e82b5614728b0b176 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 10 Feb 2024 20:54:13 -0500 Subject: [PATCH 19/30] Bump ace min to go1.18, update golangci-lint to v1.56.1 --- .github/workflows/golangci-lint.yml | 2 +- .golangci.yml | 162 ++++++++++------------------ ace/go.mod | 2 +- ace/go.sum | 7 -- template.go | 16 +-- 5 files changed, 64 insertions(+), 125 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3566ec42..3060e083 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -40,4 +40,4 @@ jobs: uses: golangci/golangci-lint-action@v3 with: # NOTE: Keep this in sync with the version from .golangci.yml - version: 'v1.52.2' + version: 'v1.56.1' diff --git a/.golangci.yml b/.golangci.yml index 19e18a55..d905a826 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,7 @@ -# Created based on v1.52.2 -# NOTE: Keep this in sync with the version in .github/workflows/lint_golangci-lint.yml +# Created based on v1.56.1 +# NOTE: Keep this in sync with the version in .github/workflows/linter.yml run: - timeout: 5m modules-download-mode: readonly skip-dirs-use-default: false skip-dirs: @@ -12,75 +11,61 @@ output: sort-results: true linters-settings: - depguard: - include-go-root: true - packages: - - flag - - io/ioutil - - reflect - - unsafe - packages-with-error-message: - - flag: '`flag` package is only allowed in main.go' - - io/ioutil: '`io/ioutil` package is deprecated, use the `io` and `os` package instead' - - reflect: '`reflect` package is dangerous to use' - - unsafe: '`unsafe` package is dangerous to use' - errcheck: - check-type-assertions: true - check-blank: true - disable-default-exclusions: true + check-type-assertions: true + check-blank: true + disable-default-exclusions: true + exclude-functions: + - '(*bytes.Buffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteString' # always returns nil error errchkjson: report-no-exported: true exhaustive: - check-generated: true default-signifies-exhaustive: true forbidigo: forbid: - ^(fmt\.Print(|f|ln)|print|println)$ - # - 'http\.Default(Client|Transport)' + - 'http\.Default(Client|Transport)' + # TODO: Eventually enable these patterns # - 'time\.Sleep' # - 'panic' - gci: - sections: - - standard - - prefix(github.com/gofiber/fiber) - - default - - blank - - dot - custom-order: true - - goconst: - numbers: true - gocritic: - enabled-tags: - - diagnostic - - style - - performance - - experimental - - opinionated disabled-checks: - - hugeParam - - rangeValCopy + - ifElseChain gofumpt: - module-path: github.com/gofiber/template + module-path: github.com/gofiber/fiber extra-rules: true gosec: + excludes: + - G104 # Provided by errcheck config: global: audit: true + + depguard: + rules: + main: + deny: + - pkg: flag + desc: '`flag` package is only allowed in main.go' + - pkg: io/ioutil + desc: '`io/ioutil` package is deprecated, use the `io` and `os` package instead' govet: + check-shadowing: true enable-all: true disable: - - fieldalignment - shadow + - fieldalignment + - loopclosure grouper: import-require-single-import: true @@ -102,10 +87,6 @@ linters-settings: promlinter: strict: true - reassign: - patterns: - - '.*' - revive: enable-all-rules: true rules: @@ -120,31 +101,35 @@ linters-settings: - name: cognitive-complexity disabled: true - name: comment-spacings - arguments: - - nolint - - msgp + disabled: true # TODO https://github.com/gofiber/fiber/issues/2816 - name: cyclomatic disabled: true + - name: early-return + severity: warning + disabled: true - name: exported disabled: true - name: file-header disabled: true - name: function-result-limit - arguments: [3] + disabled: true - name: function-length disabled: true - name: line-length-limit disabled: true - - name: nested-structs - disabled: true - name: max-public-structs disabled: true - name: modifies-parameter disabled: true + - name: nested-structs + disabled: true - name: package-comments disabled: true - - name: use-any - disabled: true # some tests still use go 1.17 + - name: unchecked-type-assertion + disabled: true # TODO https://github.com/gofiber/fiber/issues/2816 + # Provided by errcheck + - name: unhandled-error + disabled: true stylecheck: checks: @@ -171,13 +156,11 @@ linters-settings: - github.com/valyala/fasthttp issues: - exclude-use-default: false - exclude-rules: - - linters: - - goerr113 - text: 'do not define dynamic errors, use wrapped static errors instead*' + exclude-use-default: false linters: + disable: + - spancheck enable: - asasalint - asciicheck @@ -185,13 +168,8 @@ linters: - bodyclose - containedctx - contextcheck - # - cyclop - - deadcode - # - decorder - depguard - dogsled - # - dupl - - dupword - durationcheck - errcheck - errchkjson @@ -199,76 +177,46 @@ linters: - errorlint - execinquery - exhaustive - # - exhaustivestruct - # - exhaustruct - exportloopref - forbidigo - forcetypeassert - # - funlen - - gci - - ginkgolinter - - gocheckcompilerdirectives - - gochecknoglobals # Enabled - - gochecknoinits # Enabled - # - gocognit + - gochecksumtype - goconst - gocritic - # - gocyclo - # - godot - # - godox - - goerr113 - gofmt - gofumpt - # - goheader - # - goimports - # - golint - - gomnd # Enabled + - goimports - gomoddirectives - # - gomodguard - goprintffuncname - gosec - gosimple + - gosmopolitan - govet - grouper - # - ifshort - # - importas - - ineffassign - # - interfacebloat - # - interfacer - # - ireturn - # - lll + - inamedparam - loggercheck - # - maintidx - # - makezero - # - maligned + - mirror - misspell - - musttag - nakedret - # - nestif - nilerr - nilnil - # - nlreturn - noctx - nolintlint - nonamedreturns - - nosnakecase - nosprintfhostport - - paralleltest - # - prealloc + - perfsprint - predeclared - promlinter - reassign - revive - rowserrcheck - - scopelint - sqlclosecheck - staticcheck - - structcheck - stylecheck + - tagalign - tagliatelle - - tenv - - testableexamples - # - testpackage + - testifylint + - testpackage - thelper - tparallel - typecheck @@ -276,9 +224,7 @@ linters: - unparam - unused - usestdlibvars - - varcheck - # - varnamelen - wastedassign - whitespace - # - wrapcheck # disabled - # - wsl \ No newline at end of file + - wrapcheck + - tenv \ No newline at end of file diff --git a/ace/go.mod b/ace/go.mod index a57a3405..19e43843 100644 --- a/ace/go.mod +++ b/ace/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/template/ace/v2 -go 1.17 +go 1.18 require github.com/yosssi/ace v0.0.5 diff --git a/ace/go.sum b/ace/go.sum index fd21676a..79d2c190 100644 --- a/ace/go.sum +++ b/ace/go.sum @@ -1,21 +1,14 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/template.go b/template.go index 073297ae..eb7da6ca 100644 --- a/template.go +++ b/template.go @@ -10,16 +10,16 @@ import ( type IEngine interface { IEngineCore Load() error - Render(out io.Writer, template string, binding interface{}, layout ...string) error + Render(out io.Writer, template string, binding any, layout ...string) error } // IEngineCore interface type IEngineCore interface { - AddFunc(name string, fn interface{}) IEngineCore - AddFuncMap(m map[string]interface{}) IEngineCore + AddFunc(name string, fn any) IEngineCore + AddFuncMap(m map[string]any) IEngineCore Debug(enabled bool) IEngineCore Delims(left, right string) IEngineCore - FuncMap() map[string]interface{} + FuncMap() map[string]any Layout(key string) IEngineCore Reload(enabled bool) IEngineCore ShouldReload() bool @@ -45,7 +45,7 @@ type Engine struct { // lock for funcmap and templates Mutex sync.RWMutex // template funcmap - Funcmap map[string]interface{} + Funcmap map[string]any // debug prints the parsed templates verbose bool // determines if the engine parsed all templates @@ -56,7 +56,7 @@ type Engine struct { // AddFunc adds the function to the template's function map. // It is legal to overwrite elements of the default actions -func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { +func (e *Engine) AddFunc(name string, fn any) IEngineCore { e.Mutex.Lock() defer e.Mutex.Unlock() e.Funcmap[name] = fn @@ -65,7 +65,7 @@ func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { // AddFuncMap adds the functions from a map to the template's function map. // It is legal to overwrite elements of the default actions -func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore { +func (e *Engine) AddFuncMap(m map[string]any) IEngineCore { e.Mutex.Lock() defer e.Mutex.Unlock() for name, fn := range m { @@ -91,7 +91,7 @@ func (e *Engine) Delims(left, right string) IEngineCore { } // FuncMap returns the template's function map. -func (e *Engine) FuncMap() map[string]interface{} { +func (e *Engine) FuncMap() map[string]any { e.Mutex.RLock() defer e.Mutex.RUnlock() return e.Funcmap From 8520c1704798f069eba517671984a60a5d2c6cab Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 10 Feb 2024 23:54:42 -0500 Subject: [PATCH 20/30] Update .golangci.yml --- .golangci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d905a826..a91dd0d4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,7 +40,7 @@ linters-settings: - ifElseChain gofumpt: - module-path: github.com/gofiber/fiber + module-path: github.com/gofiber/template extra-rules: true gosec: @@ -227,4 +227,4 @@ linters: - wastedassign - whitespace - wrapcheck - - tenv \ No newline at end of file + - tenv From 82c500a90cfd5e124e2c46274c04f4f3a3730c15 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 10 Feb 2024 23:56:24 -0500 Subject: [PATCH 21/30] Update amber_test.go --- amber/amber_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amber/amber_test.go b/amber/amber_test.go index 466c1003..35f93df5 100644 --- a/amber/amber_test.go +++ b/amber/amber_test.go @@ -175,7 +175,7 @@ func Benchmark_Amber(b *testing.B) { expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".amber") engine.AddFunc("isAdmin", func(user string) bool { - return user == "admin" + return user == admin }) require.NoError(b, engine.Load()) From 30eaa1a22948fee1c64af0555aa73eb816b040eb Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 10 Feb 2024 23:59:03 -0500 Subject: [PATCH 22/30] Update django.go --- django/django.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/django/django.go b/django/django.go index 1cff3fc0..e71758aa 100644 --- a/django/django.go +++ b/django/django.go @@ -36,9 +36,7 @@ func (e *Engine) initialize(directory, extension string, fs http.FileSystem) { e.Engine.LayoutName = "embed" e.Engine.Funcmap = make(map[string]interface{}) e.autoEscape = true - if fs != nil { - e.Engine.FileSystem = fs - } + e.Engine.FileSystem = fs } // New creates a new Engine with a directory and extension. From 06ea1afa2d2c069c538d420e64ee0a61cf5acee7 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Thu, 15 Feb 2024 08:02:59 -0500 Subject: [PATCH 23/30] Update go.mod for Ace. Update README --- README.md | 2 +- ace/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f7bbaffc..4819c9ef 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This package provides universal methods to use multiple template engines with th - [slim](./slim/README.md) ### Installation -> Go version `1.17` or higher is required. +> Go version `1.20` or higher is required. ``` go get -u github.com/gofiber/fiber/v2 diff --git a/ace/go.mod b/ace/go.mod index 19e43843..af5f8427 100644 --- a/ace/go.mod +++ b/ace/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/template/ace/v2 -go 1.18 +go 1.20 require github.com/yosssi/ace v0.0.5 From b82f011454343c44ed08fe9140ca3a3ed1a349e2 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 17 Feb 2024 22:55:05 -0500 Subject: [PATCH 24/30] Remove all breaking changes --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/test-ace.yml | 3 ++ .github/workflows/test-amber.yml | 3 ++ .github/workflows/test-django.yml | 2 + .github/workflows/test-handlebars.yml | 3 ++ .github/workflows/test-html.yml | 3 ++ .github/workflows/test-jet.yml | 3 ++ .github/workflows/test-mustache.yml | 3 ++ .github/workflows/test-pug.yml | 3 ++ .github/workflows/test-slim.yml | 3 ++ .golangci.yml | 2 + README.md | 2 +- ace/ace.go | 10 ++-- amber/amber.go | 10 ++-- django/django.go | 10 ++-- handlebars/handlebars.go | 10 ++-- html/html.go | 10 ++-- jet/jet.go | 12 ++--- mustache/mustache.go | 10 ++-- pug/pug.go | 10 ++-- slim/slim.go | 10 ++-- template.go | 75 ++++++++++----------------- 22 files changed, 84 insertions(+), 115 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 90ef9128..127ba348 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -40,4 +40,4 @@ jobs: uses: golangci/golangci-lint-action@v4 with: # NOTE: Keep this in sync with the version from .golangci.yml - version: 'v1.56.1' + version: 'v1.56.2' diff --git a/.github/workflows/test-ace.yml b/.github/workflows/test-ace.yml index fa13a756..f75256a9 100644 --- a/.github/workflows/test-ace.yml +++ b/.github/workflows/test-ace.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-amber.yml b/.github/workflows/test-amber.yml index d809e98c..43858b5d 100644 --- a/.github/workflows/test-amber.yml +++ b/.github/workflows/test-amber.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-django.yml b/.github/workflows/test-django.yml index 5cf1b2cf..dd6a2ccd 100644 --- a/.github/workflows/test-django.yml +++ b/.github/workflows/test-django.yml @@ -16,6 +16,8 @@ jobs: strategy: matrix: go-version: + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-handlebars.yml b/.github/workflows/test-handlebars.yml index 2fd56c13..a2112313 100644 --- a/.github/workflows/test-handlebars.yml +++ b/.github/workflows/test-handlebars.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-html.yml b/.github/workflows/test-html.yml index 0ce84e6e..b67ab919 100644 --- a/.github/workflows/test-html.yml +++ b/.github/workflows/test-html.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-jet.yml b/.github/workflows/test-jet.yml index 9f1f00f0..7973ece5 100644 --- a/.github/workflows/test-jet.yml +++ b/.github/workflows/test-jet.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-mustache.yml b/.github/workflows/test-mustache.yml index 22b47f6f..460836b7 100644 --- a/.github/workflows/test-mustache.yml +++ b/.github/workflows/test-mustache.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-pug.yml b/.github/workflows/test-pug.yml index 1d884aa6..b4494e12 100644 --- a/.github/workflows/test-pug.yml +++ b/.github/workflows/test-pug.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.github/workflows/test-slim.yml b/.github/workflows/test-slim.yml index 75545606..25b66a8d 100644 --- a/.github/workflows/test-slim.yml +++ b/.github/workflows/test-slim.yml @@ -16,6 +16,9 @@ jobs: strategy: matrix: go-version: + - 1.17.x + - 1.18.x + - 1.19.x - 1.20.x - 1.21.x - 1.22.x diff --git a/.golangci.yml b/.golangci.yml index a91dd0d4..a5b91d75 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -130,6 +130,8 @@ linters-settings: # Provided by errcheck - name: unhandled-error disabled: true + - name: use-any # TODO Enable for v3 release + disabled: true stylecheck: checks: diff --git a/README.md b/README.md index 4819c9ef..f7bbaffc 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This package provides universal methods to use multiple template engines with th - [slim](./slim/README.md) ### Installation -> Go version `1.20` or higher is required. +> Go version `1.17` or higher is required. ``` go get -u github.com/gofiber/fiber/v2 diff --git a/ace/ace.go b/ace/ace.go index 469702aa..ff826674 100644 --- a/ace/ace.go +++ b/ace/ace.go @@ -125,14 +125,14 @@ func (e *Engine) Load() error { return err } - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -143,11 +143,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { ace.FlushCache() if err := e.Load(); err != nil { return err diff --git a/amber/amber.go b/amber/amber.go index 44b37f8a..2e686f76 100644 --- a/amber/amber.go +++ b/amber/amber.go @@ -121,14 +121,14 @@ func (e *Engine) Load() error { } e.Templates[name] = tmpl - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -139,11 +139,7 @@ func (e *Engine) Load() error { // Render will execute the template name along with the given values. func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/django/django.go b/django/django.go index e71758aa..bfd56635 100644 --- a/django/django.go +++ b/django/django.go @@ -133,14 +133,14 @@ func (e *Engine) Load() error { } e.Templates[name] = tmpl - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -209,11 +209,7 @@ func (e *Engine) SetAutoEscape(autoEscape bool) { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/handlebars/handlebars.go b/handlebars/handlebars.go index e0aa8a5f..ada312e0 100644 --- a/handlebars/handlebars.go +++ b/handlebars/handlebars.go @@ -107,7 +107,7 @@ func (e *Engine) Load() error { // raymond.RegisterPartialTemplate(name, tmpl) e.Templates[name] = tmpl - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err @@ -125,18 +125,14 @@ func (e *Engine) Load() error { } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true return err } // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/html/html.go b/html/html.go index 38b320f3..63073d35 100644 --- a/html/html.go +++ b/html/html.go @@ -111,14 +111,14 @@ func (e *Engine) Load() error { } // Debugging - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -129,11 +129,7 @@ func (e *Engine) Load() error { // Render will execute the template name along with the given values. func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/jet/jet.go b/jet/jet.go index c6bce575..b1a6b48f 100644 --- a/jet/jet.go +++ b/jet/jet.go @@ -83,7 +83,7 @@ func (e *Engine) Load() error { loader = jet.NewInMemLoader() } - if e.Verbose() { + if e.Verbose { e.Templates = jet.NewSet( loader, jet.WithDelims(e.Left, e.Right), @@ -132,7 +132,7 @@ func (e *Engine) Load() error { } l.Set(name, string(buf)) - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } @@ -140,7 +140,7 @@ func (e *Engine) Load() error { } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if _, ok := loader.(*jet.InMemLoader); ok { return filepath.Walk(e.Directory, walkFn) @@ -152,11 +152,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/mustache/mustache.go b/mustache/mustache.go index 1ebaf9c7..ce4a894a 100644 --- a/mustache/mustache.go +++ b/mustache/mustache.go @@ -129,14 +129,14 @@ func (e *Engine) Load() error { } e.Templates[name] = tmpl - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -147,11 +147,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/pug/pug.go b/pug/pug.go index d61cf6ad..686f8c26 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -116,14 +116,14 @@ func (e *Engine) Load() error { return err } - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -135,11 +135,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/slim/slim.go b/slim/slim.go index b7942f8f..20c1c98a 100644 --- a/slim/slim.go +++ b/slim/slim.go @@ -108,14 +108,14 @@ func (e *Engine) Load() error { tmpl.FuncMap(newFuncMap) e.Templates[name] = tmpl - if e.Verbose() { + if e.Verbose { log.Printf("views: parsed template: %s\n", name) } return err } // notify Engine that we parsed all templates - e.SetLoaded(true) + e.Loaded = true if e.FileSystem != nil { return utils.Walk(e.FileSystem, e.Directory, walkFn) @@ -126,11 +126,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if !e.Loaded() || e.ShouldReload() { - if e.ShouldReload() { - e.LockAndSetLoaded(false) - } - + if e.Check() { if err := e.Load(); err != nil { return err } diff --git a/template.go b/template.go index eb7da6ca..6ff25410 100644 --- a/template.go +++ b/template.go @@ -10,22 +10,18 @@ import ( type IEngine interface { IEngineCore Load() error - Render(out io.Writer, template string, binding any, layout ...string) error + Render(out io.Writer, template string, binding interface{}, layout ...string) error } // IEngineCore interface type IEngineCore interface { - AddFunc(name string, fn any) IEngineCore - AddFuncMap(m map[string]any) IEngineCore + AddFunc(name string, fn interface{}) IEngineCore + AddFuncMap(m map[string]interface{}) IEngineCore Debug(enabled bool) IEngineCore Delims(left, right string) IEngineCore - FuncMap() map[string]any + FuncMap() map[string]interface{} Layout(key string) IEngineCore Reload(enabled bool) IEngineCore - ShouldReload() bool - Loaded() bool - SetLoaded(enabled bool) IEngineCore - LockAndSetLoaded(enabled bool) IEngineCore } // Engine engine struct @@ -42,21 +38,21 @@ type Engine struct { Extension string // layout variable name that incapsulates the template LayoutName string + // determines if the engine parsed all templates + Loaded bool + // reload on each render + ShouldReload bool + // debug prints the parsed templates + Verbose bool // lock for funcmap and templates Mutex sync.RWMutex // template funcmap - Funcmap map[string]any - // debug prints the parsed templates - verbose bool - // determines if the engine parsed all templates - loaded bool - // reload on each render - shouldReload bool + Funcmap map[string]interface{} } // AddFunc adds the function to the template's function map. // It is legal to overwrite elements of the default actions -func (e *Engine) AddFunc(name string, fn any) IEngineCore { +func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { e.Mutex.Lock() defer e.Mutex.Unlock() e.Funcmap[name] = fn @@ -65,7 +61,7 @@ func (e *Engine) AddFunc(name string, fn any) IEngineCore { // AddFuncMap adds the functions from a map to the template's function map. // It is legal to overwrite elements of the default actions -func (e *Engine) AddFuncMap(m map[string]any) IEngineCore { +func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore { e.Mutex.Lock() defer e.Mutex.Unlock() for name, fn := range m { @@ -76,7 +72,9 @@ func (e *Engine) AddFuncMap(m map[string]any) IEngineCore { // Debug will print the parsed templates when Load is triggered. func (e *Engine) Debug(enabled bool) IEngineCore { - e.verbose = enabled + e.Mutex.Lock() + defer e.Mutex.Unlock() + e.Verbose = enabled return e } @@ -91,9 +89,7 @@ func (e *Engine) Delims(left, right string) IEngineCore { } // FuncMap returns the template's function map. -func (e *Engine) FuncMap() map[string]any { - e.Mutex.RLock() - defer e.Mutex.RUnlock() +func (e *Engine) FuncMap() map[string]interface{} { return e.Funcmap } @@ -111,38 +107,19 @@ func (e *Engine) Layout(key string) IEngineCore { func (e *Engine) Reload(enabled bool) IEngineCore { e.Mutex.Lock() defer e.Mutex.Unlock() - e.shouldReload = enabled + e.ShouldReload = enabled return e } -// ShouldReload returns true if the templates should be reloaded -func (e *Engine) ShouldReload() bool { - e.Mutex.RLock() - defer e.Mutex.RUnlock() - return e.shouldReload -} - -// Loaded returns true if templates are loaded -func (e *Engine) Loaded() bool { - e.Mutex.RLock() - defer e.Mutex.RUnlock() - return e.loaded -} - -// SetLoaded sets the loaded status -func (e *Engine) SetLoaded(enabled bool) IEngineCore { - e.loaded = enabled - return e -} - -// LockAndSetLoaded locks and sets the loaded status -func (e *Engine) LockAndSetLoaded(enabled bool) IEngineCore { +func (e *Engine) Check() bool { e.Mutex.Lock() defer e.Mutex.Unlock() - return e.SetLoaded(enabled) -} -// Verbose returns the verbose status -func (e *Engine) Verbose() bool { - return e.verbose + if !e.Loaded || e.ShouldReload { + if e.ShouldReload { + e.Loaded = false + } + return true + } + return false } From 3f5bad66f249452cbe01700fb884d2dfccd86635 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 17 Feb 2024 22:58:17 -0500 Subject: [PATCH 25/30] Fix issue with HTML engine --- html/html.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/html.go b/html/html.go index 63073d35..b99a223e 100644 --- a/html/html.go +++ b/html/html.go @@ -54,7 +54,7 @@ func newEngine(directory, extension string, fs http.FileSystem) *Engine { // Load parses the templates to the engine. func (e *Engine) Load() error { - if e.Loaded() { + if e.Loaded { return nil } From 4b18635997d32533844b1a60e1c70dcfd8618454 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sat, 17 Feb 2024 23:05:57 -0500 Subject: [PATCH 26/30] Rename Concurrent to Parallel --- ace/ace_test.go | 16 ++++++++-------- amber/amber_test.go | 16 ++++++++-------- django/django_test.go | 12 ++++++------ handlebars/handlebars_test.go | 16 ++++++++-------- html/html_test.go | 16 ++++++++-------- jet/jet_test.go | 16 ++++++++-------- pug/pug_test.go | 16 ++++++++-------- slim/slim_test.go | 12 ++++++------ 8 files changed, 60 insertions(+), 60 deletions(-) diff --git a/ace/ace_test.go b/ace/ace_test.go index 5fbf08df..b51cc079 100644 --- a/ace/ace_test.go +++ b/ace/ace_test.go @@ -253,8 +253,8 @@ func Benchmark_Ace(b *testing.B) { }) } -func Benchmark_Ace_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Ace_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".ace") engine.AddFunc("isAdmin", func(user string) bool { @@ -262,7 +262,7 @@ func Benchmark_Ace_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -270,13 +270,13 @@ func Benchmark_Ace_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_extended", func(bb *testing.B) { + b.Run("parallel_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -290,14 +290,14 @@ func Benchmark_Ace_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -305,7 +305,7 @@ func Benchmark_Ace_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_extended_asserted", func(bb *testing.B) { + b.Run("parallel_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/amber/amber_test.go b/amber/amber_test.go index 35f93df5..e869353a 100644 --- a/amber/amber_test.go +++ b/amber/amber_test.go @@ -234,8 +234,8 @@ func Benchmark_Amber(b *testing.B) { }) } -func Benchmark_Amber_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Amber_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".amber") engine.AddFunc("isAdmin", func(user string) bool { @@ -243,7 +243,7 @@ func Benchmark_Amber_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -251,13 +251,13 @@ func Benchmark_Amber_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_extended", func(bb *testing.B) { + b.Run("parallel_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -271,14 +271,14 @@ func Benchmark_Amber_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -286,7 +286,7 @@ func Benchmark_Amber_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_extended_asserted", func(bb *testing.B) { + b.Run("parallel_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/django/django_test.go b/django/django_test.go index 505190bf..79c5d019 100644 --- a/django/django_test.go +++ b/django/django_test.go @@ -464,8 +464,8 @@ func Benchmark_Django(b *testing.B) { }) } -func Benchmark_Django_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Django_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".django") engine.AddFunc("isAdmin", func(user string) bool { @@ -482,7 +482,7 @@ func Benchmark_Django_Concurrent(b *testing.B) { buf.Reset() //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) @@ -512,7 +512,7 @@ func Benchmark_Django_Concurrent(b *testing.B) { buf.Reset() //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", "Invalid_Key": "Don't return error from checkForValidIdentifiers!", }) } @@ -542,7 +542,7 @@ func Benchmark_Django_Concurrent(b *testing.B) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -572,7 +572,7 @@ func Benchmark_Django_Concurrent(b *testing.B) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", "Invalid_Key": "Don't return error from checkForValidIdentifiers!", }) require.NoError(bb, err) diff --git a/handlebars/handlebars_test.go b/handlebars/handlebars_test.go index 8d36a8c6..68a8afd5 100644 --- a/handlebars/handlebars_test.go +++ b/handlebars/handlebars_test.go @@ -236,14 +236,14 @@ func Benchmark_Handlebars(b *testing.B) { }) } -func Benchmark_Handlebars_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Handlebars_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".hbs") // Note we don't register isAdmin again because handlebars does not support re-registering helpers require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -251,13 +251,13 @@ func Benchmark_Handlebars_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_extended", func(bb *testing.B) { + b.Run("parallel_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -272,14 +272,14 @@ func Benchmark_Handlebars_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -287,7 +287,7 @@ func Benchmark_Handlebars_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_extended_asserted", func(bb *testing.B) { + b.Run("parallel_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/html/html_test.go b/html/html_test.go index 17e05de9..f1af6db7 100644 --- a/html/html_test.go +++ b/html/html_test.go @@ -326,8 +326,8 @@ func Benchmark_Html(b *testing.B) { }) } -func Benchmark_Html_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Html_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".html") engine.AddFunc("isAdmin", func(user string) bool { @@ -335,7 +335,7 @@ func Benchmark_Html_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -343,13 +343,13 @@ func Benchmark_Html_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_extended", func(bb *testing.B) { + b.Run("parallel_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -363,14 +363,14 @@ func Benchmark_Html_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -378,7 +378,7 @@ func Benchmark_Html_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_extended_asserted", func(bb *testing.B) { + b.Run("parallel_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/jet/jet_test.go b/jet/jet_test.go index eef2c91b..1a8ee108 100644 --- a/jet/jet_test.go +++ b/jet/jet_test.go @@ -226,8 +226,8 @@ func Benchmark_Jet(b *testing.B) { }) } -func Benchmark_Jet_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Jet_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Title

Header

Hello, Admin!

Footer

` engine := New("./views", ".jet") engine.AddFunc("isAdmin", func(user string) bool { @@ -235,7 +235,7 @@ func Benchmark_Jet_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -243,13 +243,13 @@ func Benchmark_Jet_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_extended", func(bb *testing.B) { + b.Run("parallel_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -263,14 +263,14 @@ func Benchmark_Jet_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -278,7 +278,7 @@ func Benchmark_Jet_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_extended_asserted", func(bb *testing.B) { + b.Run("parallel_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/pug/pug_test.go b/pug/pug_test.go index c6f5095f..97cfd5b4 100644 --- a/pug/pug_test.go +++ b/pug/pug_test.go @@ -247,8 +247,8 @@ func Benchmark_Pug(b *testing.B) { }) } -func Benchmark_Pug_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Pug_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` expectExtended := `Main

Header

Hello, Admin!

Footer

` engine := New("./views", ".pug") engine.AddFunc("isAdmin", func(user string) bool { @@ -256,7 +256,7 @@ func Benchmark_Pug_Concurrent(b *testing.B) { }) require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -264,13 +264,13 @@ func Benchmark_Pug_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_extended", func(bb *testing.B) { + b.Run("parallel_extended", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -284,14 +284,14 @@ func Benchmark_Pug_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) @@ -299,7 +299,7 @@ func Benchmark_Pug_Concurrent(b *testing.B) { }) }) - b.Run("concurrent_extended_asserted", func(bb *testing.B) { + b.Run("parallel_extended_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { diff --git a/slim/slim_test.go b/slim/slim_test.go index d1967611..62155c28 100644 --- a/slim/slim_test.go +++ b/slim/slim_test.go @@ -211,15 +211,15 @@ func Benchmark_Slim(b *testing.B) { }) } -func Benchmark_Slim_Concurrent(b *testing.B) { - expectSimple := `

Hello, Concurrent!

` +func Benchmark_Slim_Parallel(b *testing.B) { + expectSimple := `

Hello, Parallel!

` engine := New("./views", ".slim") engine.AddFunc("isAdmin", func(s ...slim.Value) (slim.Value, error) { return s[0].(string) == "admin", nil }) require.NoError(b, engine.Load()) - b.Run("concurrent_simple", func(bb *testing.B) { + b.Run("parallel_simple", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { @@ -227,20 +227,20 @@ func Benchmark_Slim_Concurrent(b *testing.B) { var buf bytes.Buffer //nolint:gosec,errcheck // Return value not needed for benchmark _ = engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) } }) }) - b.Run("concurrent_simple_asserted", func(bb *testing.B) { + b.Run("parallel_simple_asserted", func(bb *testing.B) { bb.ReportAllocs() bb.ResetTimer() bb.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf bytes.Buffer err := engine.Render(&buf, "simple", map[string]interface{}{ - "Title": "Hello, Concurrent!", + "Title": "Hello, Parallel!", }) require.NoError(bb, err) require.Equal(bb, expectSimple, trim(buf.String())) From 3e12de78c309af6fcffade5bd59c2450016aa771 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 18 Feb 2024 00:32:14 -0500 Subject: [PATCH 27/30] Rename function and add comment --- ace/ace.go | 2 +- amber/amber.go | 2 +- django/django.go | 2 +- handlebars/handlebars.go | 2 +- html/html.go | 2 +- jet/jet.go | 2 +- mustache/mustache.go | 2 +- pug/pug.go | 2 +- slim/slim.go | 2 +- template.go | 3 ++- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ace/ace.go b/ace/ace.go index ff826674..3771f7dd 100644 --- a/ace/ace.go +++ b/ace/ace.go @@ -143,7 +143,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { ace.FlushCache() if err := e.Load(); err != nil { return err diff --git a/amber/amber.go b/amber/amber.go index 2e686f76..220b52d4 100644 --- a/amber/amber.go +++ b/amber/amber.go @@ -139,7 +139,7 @@ func (e *Engine) Load() error { // Render will execute the template name along with the given values. func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/django/django.go b/django/django.go index bfd56635..77125df9 100644 --- a/django/django.go +++ b/django/django.go @@ -209,7 +209,7 @@ func (e *Engine) SetAutoEscape(autoEscape bool) { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/handlebars/handlebars.go b/handlebars/handlebars.go index ada312e0..fb64f322 100644 --- a/handlebars/handlebars.go +++ b/handlebars/handlebars.go @@ -132,7 +132,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/html/html.go b/html/html.go index b99a223e..4c1c4c27 100644 --- a/html/html.go +++ b/html/html.go @@ -129,7 +129,7 @@ func (e *Engine) Load() error { // Render will execute the template name along with the given values. func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/jet/jet.go b/jet/jet.go index b1a6b48f..3d60e804 100644 --- a/jet/jet.go +++ b/jet/jet.go @@ -152,7 +152,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/mustache/mustache.go b/mustache/mustache.go index ce4a894a..a2c15ff4 100644 --- a/mustache/mustache.go +++ b/mustache/mustache.go @@ -147,7 +147,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/pug/pug.go b/pug/pug.go index 686f8c26..d30068fb 100644 --- a/pug/pug.go +++ b/pug/pug.go @@ -135,7 +135,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/slim/slim.go b/slim/slim.go index 20c1c98a..e10ab9bc 100644 --- a/slim/slim.go +++ b/slim/slim.go @@ -126,7 +126,7 @@ func (e *Engine) Load() error { // Render will render the template by name func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error { // Check if templates need to be loaded/reloaded - if e.Check() { + if e.PreRenderCheck() { if err := e.Load(); err != nil { return err } diff --git a/template.go b/template.go index 6ff25410..9888f4ad 100644 --- a/template.go +++ b/template.go @@ -111,7 +111,8 @@ func (e *Engine) Reload(enabled bool) IEngineCore { return e } -func (e *Engine) Check() bool { +// Check if the engine should reload the templates before rendering +func (e *Engine) PreRenderCheck() bool { e.Mutex.Lock() defer e.Mutex.Unlock() From caf252ac5216946565085b89c07b9ecba3c306d2 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 18 Feb 2024 14:16:27 -0500 Subject: [PATCH 28/30] Fix ace go.mod file --- ace/go.mod | 2 +- ace/go.sum | 7 +++++++ template.go | 13 +++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ace/go.mod b/ace/go.mod index af5f8427..a57a3405 100644 --- a/ace/go.mod +++ b/ace/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/template/ace/v2 -go 1.20 +go 1.17 require github.com/yosssi/ace v0.0.5 diff --git a/ace/go.sum b/ace/go.sum index 79d2c190..fd21676a 100644 --- a/ace/go.sum +++ b/ace/go.sum @@ -1,14 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/template.go b/template.go index 9888f4ad..09a3a034 100644 --- a/template.go +++ b/template.go @@ -22,6 +22,7 @@ type IEngineCore interface { FuncMap() map[string]interface{} Layout(key string) IEngineCore Reload(enabled bool) IEngineCore + PreRenderCheck() bool } // Engine engine struct @@ -54,8 +55,8 @@ type Engine struct { // It is legal to overwrite elements of the default actions func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { e.Mutex.Lock() - defer e.Mutex.Unlock() e.Funcmap[name] = fn + e.Mutex.Unlock() return e } @@ -63,18 +64,18 @@ func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore { // It is legal to overwrite elements of the default actions func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore { e.Mutex.Lock() - defer e.Mutex.Unlock() for name, fn := range m { e.Funcmap[name] = fn } + e.Mutex.Unlock() return e } // Debug will print the parsed templates when Load is triggered. func (e *Engine) Debug(enabled bool) IEngineCore { e.Mutex.Lock() - defer e.Mutex.Unlock() e.Verbose = enabled + e.Mutex.Unlock() return e } @@ -83,8 +84,8 @@ func (e *Engine) Debug(enabled bool) IEngineCore { // corresponding default: "{{" and "}}". func (e *Engine) Delims(left, right string) IEngineCore { e.Mutex.Lock() - defer e.Mutex.Unlock() e.Left, e.Right = left, right + e.Mutex.Unlock() return e } @@ -96,8 +97,8 @@ func (e *Engine) FuncMap() map[string]interface{} { // Layout defines the variable name that will incapsulate the template func (e *Engine) Layout(key string) IEngineCore { e.Mutex.Lock() - defer e.Mutex.Unlock() e.LayoutName = key + e.Mutex.Unlock() return e } @@ -106,8 +107,8 @@ func (e *Engine) Layout(key string) IEngineCore { // the application when you edit a template file. func (e *Engine) Reload(enabled bool) IEngineCore { e.Mutex.Lock() - defer e.Mutex.Unlock() e.ShouldReload = enabled + e.Mutex.Unlock() return e } From 826af69b84ff71258413b2331b095ce330a6d27e Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 18 Feb 2024 14:18:18 -0500 Subject: [PATCH 29/30] Remove defer --- template.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template.go b/template.go index 09a3a034..7d3e8398 100644 --- a/template.go +++ b/template.go @@ -115,13 +115,14 @@ func (e *Engine) Reload(enabled bool) IEngineCore { // Check if the engine should reload the templates before rendering func (e *Engine) PreRenderCheck() bool { e.Mutex.Lock() - defer e.Mutex.Unlock() if !e.Loaded || e.ShouldReload { if e.ShouldReload { e.Loaded = false } + e.Mutex.Unlock() return true } + e.Mutex.Unlock() return false } From 14219360a638e084abb42b7205b1f1fda3e233f6 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 18 Feb 2024 14:25:34 -0500 Subject: [PATCH 30/30] Add comment about mutex.unlock() vs defer --- template.go | 1 + 1 file changed, 1 insertion(+) diff --git a/template.go b/template.go index 7d3e8398..ab443f67 100644 --- a/template.go +++ b/template.go @@ -113,6 +113,7 @@ func (e *Engine) Reload(enabled bool) IEngineCore { } // Check if the engine should reload the templates before rendering +// Explicit Mute Unlock vs defer offers better performance func (e *Engine) PreRenderCheck() bool { e.Mutex.Lock()