🐛 fix: mounted app views (#1749)

* Fix mounted app views.

* Cleaner structure.

Co-authored-by: RW <rene@gofiber.io>

* remove unnecessary lines.

* Add test case for group-with-mount, remove unnecessary lines.

Co-authored-by: RW <rene@gofiber.io>
pull/1755/head
M. Efe Çetin 2022-02-06 16:58:45 +03:00 committed by GitHub
parent 569511eb78
commit c450072f4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 422 additions and 32 deletions

View File

@ -0,0 +1 @@
<h1>Hello {{ .Name }}!</h1>

34
app.go
View File

@ -111,8 +111,9 @@ type App struct {
getBytes func(s string) (b []byte)
// Converts byte slice to a string
getString func(b []byte) string
// mount prefix -> error handler
errorHandlers map[string]ErrorHandler
// Mounted and main apps
appList map[string]*App
}
// Config is a struct holding the server settings.
@ -460,10 +461,10 @@ func New(config ...Config) *App {
},
},
// Create config
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
errorHandlers: make(map[string]ErrorHandler),
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
appList: make(map[string]*App),
}
// Override config if provided
if len(config) > 0 {
@ -515,6 +516,9 @@ func New(config ...Config) *App {
app.handleTrustedProxy(ipAddress)
}
// Init appList
app.appList[""] = app
// Init app
app.init()
@ -544,6 +548,7 @@ 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])
@ -551,13 +556,10 @@ func (app *App) Mount(prefix string, fiber *App) Router {
}
}
// 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
}
for mountedPrefixes, errHandler := range fiber.errorHandlers {
app.errorHandlers[prefix+mountedPrefixes] = errHandler
// Support for configs of mounted-apps and sub-mounted-apps
for mountedPrefixes, subApp := range fiber.appList {
app.appList[prefix+mountedPrefixes] = subApp
subApp.init()
}
atomic.AddUint32(&app.handlersCount, fiber.handlersCount)
@ -1002,11 +1004,11 @@ func (app *App) ErrorHandler(ctx *Ctx, err error) error {
mountedPrefixParts int
)
for prefix, errHandler := range app.errorHandlers {
if strings.HasPrefix(ctx.path, prefix) {
for prefix, subApp := range app.appList {
if strings.HasPrefix(ctx.path, prefix) && prefix != "" {
parts := len(strings.Split(prefix, "/"))
if mountedPrefixParts <= parts {
mountedErrHandler = errHandler
mountedErrHandler = subApp.config.ErrorHandler
mountedPrefixParts = parts
}
}

31
ctx.go
View File

@ -1082,18 +1082,28 @@ func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error {
}
if c.app.config.Views != nil {
// Render template based on global layout if exists
if len(layouts) == 0 && c.app.config.ViewsLayout != "" {
layouts = []string{
c.app.config.ViewsLayout,
rendered := false
for prefix, app := range c.app.appList {
if prefix == "" || strings.Contains(c.OriginalURL(), prefix) {
if len(layouts) == 0 && app.config.ViewsLayout != "" {
layouts = []string{
app.config.ViewsLayout,
}
}
// Render template from Views
if app.config.Views != nil {
if err := app.config.Views.Render(buf, name, bind, layouts...); err != nil {
return err
}
rendered = true
break
}
}
// Render template from Views
if err := c.app.config.Views.Render(buf, name, bind, layouts...); err != nil {
return err
}
} else {
}
if !rendered {
// Render raw template using 'name' as filepath if no engine is set
var tmpl *template.Template
if _, err = readContent(buf, name); err != nil {
@ -1109,6 +1119,7 @@ func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error {
return err
}
}
// Set Content-Type to text/html
c.fasthttp.Response.Header.SetContentType(MIMETextHTMLCharsetUTF8)
// Set rendered template to body

View File

@ -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"
)
@ -2049,6 +2050,59 @@ 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_Render_MountGroup(t *testing.T) {
t.Parallel()
micro := New(Config{
Views: html.New("./.github/testdata/template", ".gohtml"),
})
micro.Get("/doe", func(c *Ctx) error {
return c.Render("hello_world", Map{
"Name": "doe",
})
})
app := New()
v1 := app.Group("/v1")
v1.Mount("/john", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
body, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "<h1>Hello doe!</h1>", string(body))
}
func Test_Ctx_RenderWithoutLocals(t *testing.T) {
t.Parallel()
app := New(Config{

View File

@ -32,13 +32,11 @@ func (grp *Group) Mount(prefix string, fiber *App) Router {
}
}
// Save the fiber's error handler and its sub apps
// Support for configs of mounted-apps and sub-mounted-apps
groupPath = strings.TrimRight(groupPath, "/")
if fiber.config.ErrorHandler != nil {
grp.app.errorHandlers[groupPath] = fiber.config.ErrorHandler
}
for mountedPrefixes, errHandler := range fiber.errorHandlers {
grp.app.errorHandlers[groupPath+mountedPrefixes] = errHandler
for mountedPrefixes, subApp := range fiber.appList {
grp.app.appList[groupPath+mountedPrefixes] = subApp
subApp.init()
}
atomic.AddUint32(&grp.app.handlersCount, fiber.handlersCount)

View File

@ -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)
}

View File

@ -0,0 +1,110 @@
package utils
import (
"io/ioutil"
"net/http"
"os"
pathpkg "path"
"path/filepath"
"sort"
)
// Walk walks the filesystem rooted at root, calling walkFn for each file or
// directory in the filesystem, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order.
func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error {
info, err := stat(fs, root)
if err != nil {
return walkFn(root, nil, err)
}
return walk(fs, root, info, walkFn)
}
// #nosec G304
// ReadFile returns the raw content of a file
func ReadFile(path string, fs http.FileSystem) ([]byte, error) {
if fs != nil {
file, err := fs.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return ioutil.ReadAll(file)
}
return ioutil.ReadFile(path)
}
// readDirNames reads the directory named by dirname and returns
// a sorted list of directory entries.
func readDirNames(fs http.FileSystem, dirname string) ([]string, error) {
fis, err := readDir(fs, dirname)
if err != nil {
return nil, err
}
names := make([]string, len(fis))
for i := range fis {
names[i] = fis[i].Name()
}
sort.Strings(names)
return names, nil
}
// walk recursively descends path, calling walkFn.
func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
err := walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
names, err := readDirNames(fs, path)
if err != nil {
return walkFn(path, info, err)
}
for _, name := range names {
filename := pathpkg.Join(path, name)
fileInfo, err := stat(fs, filename)
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = walk(fs, filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}
// readDir reads the contents of the directory associated with file and
// returns a slice of FileInfo values in directory order.
func readDir(fs http.FileSystem, name string) ([]os.FileInfo, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return f.Readdir(0)
}
// stat returns the FileInfo structure describing file.
func stat(fs http.FileSystem, name string) (os.FileInfo, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return f.Stat()
}