mirror of https://github.com/gofiber/fiber.git
🐛 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
parent
569511eb78
commit
c450072f4a
|
@ -0,0 +1 @@
|
|||
<h1>Hello {{ .Name }}!</h1>
|
34
app.go
34
app.go
|
@ -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
31
ctx.go
|
@ -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
|
||||
|
|
54
ctx_test.go
54
ctx_test.go
|
@ -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{
|
||||
|
|
10
group.go
10
group.go
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue