Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 fix: mounted app views #1749

Merged
merged 4 commits into from Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/testdata/template/hello_world.gohtml
@@ -0,0 +1 @@
<h1>Hello {{ .Name }}!</h1>
10 changes: 9 additions & 1 deletion app.go
Expand Up @@ -113,6 +113,8 @@ type App struct {
getString func(b []byte) string
// mount prefix -> error handler
errorHandlers map[string]ErrorHandler
// mount prefix -> sub-views
mountedViews map[string]Views
}

// Config is a struct holding the server settings.
Expand Down Expand Up @@ -464,6 +466,7 @@ func New(config ...Config) *App {
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
errorHandlers: make(map[string]ErrorHandler),
mountedViews: make(map[string]Views),
}
// Override config if provided
if len(config) > 0 {
Expand Down Expand Up @@ -544,15 +547,20 @@ func (app *App) handleTrustedProxy(ipAddress string) {
// to be invoked on errors that happen within the prefix route.
func (app *App) Mount(prefix string, fiber *App) Router {
stack := fiber.Stack()
prefix = strings.TrimRight(prefix, "/")
for m := range stack {
for r := range stack[m] {
route := app.copyRoute(stack[m][r])
app.addRoute(route.Method, app.addPrefixToRoute(prefix, route))
}
}

// Support for mounted-views
if fiber.config.Views != nil {
app.mountedViews[prefix] = fiber.config.Views
}

// Save the fiber's error handler and its sub apps
prefix = strings.TrimRight(prefix, "/")
if fiber.config.ErrorHandler != nil {
app.errorHandlers[prefix] = fiber.config.ErrorHandler
}
Expand Down
14 changes: 14 additions & 0 deletions ctx.go
Expand Up @@ -1093,6 +1093,20 @@ func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error {
if err := c.app.config.Views.Render(buf, name, bind, layouts...); err != nil {
return err
}
} else if len(c.app.mountedViews) != 0 {
for prefix, view := range c.app.mountedViews {
if strings.Contains(c.OriginalURL(), prefix) {
// Load
if err := view.Load(); err != nil {
return err
}

// Render
if err := c.app.mountedViews[prefix].Render(buf, name, bind, layouts...); err != nil {
return err
}
}
}
} else {
// Render raw template using 'name' as filepath if no engine is set
var tmpl *template.Template
Expand Down
28 changes: 28 additions & 0 deletions ctx_test.go
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/gofiber/fiber/v2/internal/bytebufferpool"
"github.com/gofiber/fiber/v2/internal/storage/memory"
"github.com/gofiber/fiber/v2/internal/template/html"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
)
Expand Down Expand Up @@ -2049,6 +2050,33 @@ func Test_Ctx_Render(t *testing.T) {
err = c.Render("./.github/testdata/template-invalid.html", nil)
utils.AssertEqual(t, false, err == nil)
}

// go test -run Test_Ctx_Render_Mount
func Test_Ctx_Render_Mount(t *testing.T) {
t.Parallel()

sub := New(Config{
Views: html.New("./.github/testdata/template", ".gohtml"),
})

sub.Get("/:name", func(ctx *Ctx) error {
return ctx.Render("hello_world", Map{
"Name": ctx.Params("name"),
})
})

app := New()
app.Mount("/hello", sub)

resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/a", nil))
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
utils.AssertEqual(t, nil, err, "app.Test(req)")

body, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "<h1>Hello a!</h1>", string(body))
}

func Test_Ctx_RenderWithoutLocals(t *testing.T) {
t.Parallel()
app := New(Config{
Expand Down
214 changes: 214 additions & 0 deletions internal/template/html/html.go
@@ -0,0 +1,214 @@
package html

import (
"fmt"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"

"github.com/gofiber/fiber/v2/internal/template/utils"
)

// Engine struct
type Engine struct {
// delimiters
left string
right string
// views folder
directory string
// http.FileSystem supports embedded files
fileSystem http.FileSystem
// views extension
extension string
// layout variable name that incapsulates the template
layout string
// determines if the engine parsed all templates
loaded bool
// reload on each render
reload bool
// debug prints the parsed templates
debug bool
// lock for funcmap and templates
mutex sync.RWMutex
// template funcmap
funcmap map[string]interface{}
// templates
Templates *template.Template
}

// New returns a HTML render engine for Fiber
func New(directory, extension string) *Engine {
engine := &Engine{
left: "{{",
right: "}}",
directory: directory,
extension: extension,
layout: "embed",
funcmap: make(map[string]interface{}),
}
engine.AddFunc(engine.layout, func() error {
return fmt.Errorf("layout called unexpectedly.")
})
return engine
}

//NewFileSystem ...
func NewFileSystem(fs http.FileSystem, extension string) *Engine {
engine := &Engine{
left: "{{",
right: "}}",
directory: "/",
fileSystem: fs,
extension: extension,
layout: "embed",
funcmap: make(map[string]interface{}),
}
engine.AddFunc(engine.layout, func() error {
return fmt.Errorf("layout called unexpectedly.")
})
return engine
}

// Layout defines the variable name that will incapsulate the template
func (e *Engine) Layout(key string) *Engine {
e.layout = key
return e
}

// Delims sets the action delimiters to the specified strings, to be used in
// templates. An empty delimiter stands for the
// corresponding default: {{ or }}.
func (e *Engine) Delims(left, right string) *Engine {
e.left, e.right = left, right
return e
}

// 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{}) *Engine {
e.mutex.Lock()
e.funcmap[name] = fn
e.mutex.Unlock()
return e
}

// Reload if set to true the templates are reloading on each render,
// 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) *Engine {
e.reload = enabled
return e
}

// Debug will print the parsed templates when Load is triggered.
func (e *Engine) Debug(enabled bool) *Engine {
e.debug = enabled
return e
}

// Parse is deprecated, please use Load() instead
func (e *Engine) Parse() error {
fmt.Println("Parse() is deprecated, please use Load() instead.")
return e.Load()
}

// Load parses the templates to the engine.
func (e *Engine) Load() error {
if e.loaded {
return nil
}
// race safe
e.mutex.Lock()
defer e.mutex.Unlock()
e.Templates = template.New(e.directory)

// Set template settings
e.Templates.Delims(e.left, e.right)
e.Templates.Funcs(e.funcmap)

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)
if err != nil {
return err
}
// Reverse slashes '\' -> '/' and
// partials\footer.tmpl -> partials/footer.tmpl
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 .. }}
_, err = e.Templates.New(name).Parse(string(buf))
if err != nil {
return err
}
// Debugging
if e.debug {
fmt.Printf("views: parsed template: %s\n", name)
}
return err
}
// notify engine that we parsed all templates
e.loaded = true
if e.fileSystem != nil {
return utils.Walk(e.fileSystem, e.directory, walkFn)
}
return filepath.Walk(e.directory, walkFn)
}

// Render will execute the template name along with the given values.
func (e *Engine) Render(out io.Writer, template string, binding interface{}, layout ...string) error {
if !e.loaded || e.reload {
if e.reload {
e.loaded = false
}
if err := e.Load(); err != nil {
return err
}
}

tmpl := e.Templates.Lookup(template)
if tmpl == nil {
return fmt.Errorf("render: template %s does not exist", template)
}
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])
}
e.mutex.Lock()
defer e.mutex.Unlock()
lay.Funcs(map[string]interface{}{
e.layout: func() error {
return tmpl.Execute(out, binding)
},
})
return lay.Execute(out, binding)
}
return tmpl.Execute(out, binding)
}