🐛 bug: improve mounting behavior (#2120)

* 🐛 bug: fix mounting doesn't work if when to declare it before routes

* 🐛 bug: fix mounting doesn't work if when to declare it before routes

* 🐛 bug: fix mounting doesn't work if when to declare it before routes

* 🐛 bug: fix mounting doesn't work if when to declare it before routes

* 🐛 bug: fix mounting doesn't work if when to declare it before routes

* add onMount hooks, mountPath like express.js, better behavior for onName, onRoute, onGroup, onGroupName hooks on mounted apps

* add comment

* use once

* fix views when both app and sub-app have view engine, better behavior for mount path

* fix tests

* fix tests

* make some tasks

* make some tasks
pull/2173/head
M. Efe Çetin 2022-10-25 08:51:44 +03:00 committed by GitHub
parent 5b1885a44c
commit 156b81c768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 649 additions and 302 deletions

1
.github/testdata2/bruh.tmpl vendored Normal file
View File

@ -0,0 +1 @@
<h1>I'm Bruh</h1>

56
app.go
View File

@ -18,7 +18,6 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"encoding/json"
@ -106,8 +105,6 @@ type App struct {
getBytes func(s string) (b []byte)
// Converts byte slice to a string
getString func(b []byte) string
// Mounted and main apps
appList map[string]*App
// Hooks
hooks *Hooks
// Latest route & group
@ -115,6 +112,8 @@ type App struct {
latestGroup *Group
// TLS handler
tlsHandler *TLSHandler
// Mount fields
mountFields *mountFields
}
// Config is a struct holding the server settings.
@ -485,7 +484,6 @@ func New(config ...Config) *App {
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
appList: make(map[string]*App),
latestRoute: &Route{},
latestGroup: &Group{},
}
@ -493,6 +491,9 @@ func New(config ...Config) *App {
// Define hooks
app.hooks = newHooks(app)
// Define mountFields
app.mountFields = newMountFields(app)
// Override config if provided
if len(config) > 0 {
app.config = config[0]
@ -549,9 +550,6 @@ func New(config ...Config) *App {
// Override colors
app.config.ColorScheme = defaultColors(app.config.ColorScheme)
// Init appList
app.appList[""] = app
// Init app
app.init()
@ -582,36 +580,6 @@ func (app *App) SetTLSHandler(tlsHandler *TLSHandler) {
app.mutex.Unlock()
}
// Mount attaches another app instance as a sub-router along a routing path.
// It's very useful to split up a large API as many independent routers and
// compose them as a single service using Mount. The fiber's error handler and
// any of the fiber's sub apps are added to the application's error handlers
// 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, "/")
if prefix == "" {
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 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)
return app
}
// Name Assign name to specific route.
func (app *App) Name(name string) Router {
app.mutex.Lock()
@ -991,7 +959,7 @@ func (app *App) ErrorHandler(ctx *Ctx, err error) error {
mountedPrefixParts int
)
for prefix, subApp := range app.appList {
for prefix, subApp := range app.mountFields.appList {
if prefix != "" && strings.HasPrefix(ctx.path, prefix) {
parts := len(strings.Split(prefix, "/"))
if mountedPrefixParts <= parts {
@ -1041,7 +1009,17 @@ func (app *App) startupProcess() *App {
}
app.mutex.Lock()
defer app.mutex.Unlock()
// add routes of sub-apps
app.mountFields.subAppsRoutesAdded.Do(func() {
app.appendSubAppLists(app.mountFields.appList)
app.addSubAppsRoutes(app.mountFields.appList)
app.generateAppListKeys()
})
// build route tree stack
app.buildTree()
app.mutex.Unlock()
return app
}

View File

@ -246,44 +246,6 @@ func Test_App_ErrorHandler_RouteStack(t *testing.T) {
utils.AssertEqual(t, "1: USE error", string(body))
}
func Test_App_ErrorHandler_GroupMount(t *testing.T) {
micro := New(Config{
ErrorHandler: func(c *Ctx, err error) error {
utils.AssertEqual(t, "0: GET error", err.Error())
return c.Status(500).SendString("1: custom error")
},
})
micro.Get("/doe", func(c *Ctx) error {
return errors.New("0: GET error")
})
app := New()
v1 := app.Group("/v1")
v1.Mount("/john", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
testErrorResponse(t, err, resp, "1: custom error")
}
func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) {
micro := New(Config{
ErrorHandler: func(c *Ctx, err error) error {
utils.AssertEqual(t, "0: GET error", err.Error())
return c.Status(500).SendString("1: custom error")
},
})
micro.Get("/john/doe", func(c *Ctx) error {
return errors.New("0: GET error")
})
app := New()
v1 := app.Group("/v1")
v1.Mount("/", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
testErrorResponse(t, err, resp, "1: custom error")
}
func Test_App_Nested_Params(t *testing.T) {
app := New()
@ -307,22 +269,6 @@ func Test_App_Nested_Params(t *testing.T) {
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
}
// go test -run Test_App_Mount
func Test_App_Mount(t *testing.T) {
micro := New()
micro.Get("/doe", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
app := New()
app.Mount("/john", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
utils.AssertEqual(t, uint32(2), app.handlersCount)
}
func Test_App_Use_Params(t *testing.T) {
app := New()
@ -1046,23 +992,6 @@ func Test_App_Group_Invalid(t *testing.T) {
New().Group("/").Use(1)
}
// go test -run Test_App_Group_Mount
func Test_App_Group_Mount(t *testing.T) {
micro := New()
micro.Get("/doe", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
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")
utils.AssertEqual(t, uint32(2), app.handlersCount)
}
func Test_App_Group(t *testing.T) {
dummyHandler := testEmptyHandler
@ -1538,96 +1467,6 @@ func Test_App_DisablePreParseMultipartForm(t *testing.T) {
utils.AssertEqual(t, testString, string(body))
}
func Test_App_UseMountedErrorHandler(t *testing.T) {
app := New()
fiber := New(Config{
ErrorHandler: func(ctx *Ctx, err error) error {
return ctx.Status(500).SendString("hi, i'm a custom error")
},
})
fiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
app.Mount("/api", fiber)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
testErrorResponse(t, err, resp, "hi, i'm a custom error")
}
func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) {
app := New()
fiber := New(Config{
ErrorHandler: func(ctx *Ctx, err error) error {
return ctx.Status(500).SendString("hi, i'm a custom error")
},
})
fiber.Get("/api", func(c *Ctx) error {
return errors.New("something happened")
})
app.Mount("/", fiber)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
testErrorResponse(t, err, resp, "hi, i'm a custom error")
}
func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) {
app := New()
tsf := func(ctx *Ctx, err error) error {
return ctx.Status(200).SendString("hi, i'm a custom sub sub fiber error")
}
tripleSubFiber := New(Config{
ErrorHandler: tsf,
})
tripleSubFiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
sf := func(ctx *Ctx, err error) error {
return ctx.Status(200).SendString("hi, i'm a custom sub fiber error")
}
subfiber := New(Config{
ErrorHandler: sf,
})
subfiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
subfiber.Mount("/third", tripleSubFiber)
f := func(ctx *Ctx, err error) error {
return ctx.Status(200).SendString("hi, i'm a custom error")
}
fiber := New(Config{
ErrorHandler: f,
})
fiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
fiber.Mount("/sub", subfiber)
app.Mount("/api", fiber)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", nil))
utils.AssertEqual(t, nil, err, "/api/sub req")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
b, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
utils.AssertEqual(t, "hi, i'm a custom sub fiber error", string(b), "Response body")
resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", nil))
utils.AssertEqual(t, nil, err, "/api/sub/third req")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
b, err = ioutil.ReadAll(resp2.Body)
utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
utils.AssertEqual(t, "hi, i'm a custom sub sub fiber error", string(b), "Third fiber Response body")
}
func Test_App_Test_no_timeout_infinitely(t *testing.T) {
var err error
c := make(chan int)

12
ctx.go
View File

@ -1384,11 +1384,13 @@ func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
// Pass-locals-to-views & bind
// Pass-locals-to-views, bind, appListKeys
c.renderExtensions(bind)
rendered := false
for prefix, app := range c.app.appList {
var rendered bool
for i := len(c.app.mountFields.appListKeys) - 1; i >= 0; i-- {
prefix := c.app.mountFields.appListKeys[i]
app := c.app.mountFields.appList[prefix]
if prefix == "" || strings.Contains(c.OriginalURL(), prefix) {
if len(layouts) == 0 && app.config.ViewsLayout != "" {
layouts = []string{
@ -1454,6 +1456,10 @@ func (c *Ctx) renderExtensions(bind interface{}) {
})
}
}
if len(c.app.mountFields.appListKeys) == 0 {
c.app.generateAppListKeys()
}
}
// Route returns the matched Route struct.

View File

@ -32,7 +32,6 @@ 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"
)
@ -2792,58 +2791,6 @@ func Test_Ctx_Render(t *testing.T) {
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{
@ -3149,6 +3096,7 @@ func Test_Ctx_RestartRoutingWithChangedPathAndCatchAll(t *testing.T) {
type testTemplateEngine struct {
templates *template.Template
path string
}
func (t *testTemplateEngine) Render(w io.Writer, name string, bind interface{}, layout ...string) error {
@ -3160,7 +3108,10 @@ func (t *testTemplateEngine) Render(w io.Writer, name string, bind interface{},
}
func (t *testTemplateEngine) Load() error {
t.templates = template.Must(template.ParseGlob("./.github/testdata/*.tmpl"))
if t.path == "" {
t.path = "testdata"
}
t.templates = template.Must(template.ParseGlob("./.github/" + t.path + "/*.tmpl"))
return nil
}

View File

@ -8,7 +8,6 @@ import (
"fmt"
"reflect"
"strings"
"sync/atomic"
)
// Group struct
@ -19,35 +18,6 @@ type Group struct {
Prefix string
}
// Mount attaches another app instance as a sub-router along a routing path.
// It's very useful to split up a large API as many independent routers and
// compose them as a single service using Mount.
func (grp *Group) Mount(prefix string, fiber *App) Router {
stack := fiber.Stack()
groupPath := getGroupPath(grp.Prefix, prefix)
groupPath = strings.TrimRight(groupPath, "/")
if groupPath == "" {
groupPath = "/"
}
for m := range stack {
for r := range stack[m] {
route := grp.app.copyRoute(stack[m][r])
grp.app.addRoute(route.Method, grp.app.addPrefixToRoute(groupPath, route))
}
}
// Support for configs of mounted-apps and sub-mounted-apps
for mountedPrefixes, subApp := range fiber.appList {
grp.app.appList[groupPath+mountedPrefixes] = subApp
subApp.init()
}
atomic.AddUint32(&grp.app.handlersCount, fiber.handlersCount)
return grp
}
// Name Assign name to specific route.
func (grp *Group) Name(name string) Router {
grp.app.mutex.Lock()

View File

@ -8,6 +8,7 @@ type OnGroupNameHandler = OnGroupHandler
type OnListenHandler = func() error
type OnShutdownHandler = OnListenHandler
type OnForkHandler = func(int) error
type OnMountHandler = func(*App) error
// Hooks is a struct to use it with App.
type Hooks struct {
@ -22,6 +23,7 @@ type Hooks struct {
onListen []OnListenHandler
onShutdown []OnShutdownHandler
onFork []OnForkHandler
onMount []OnMountHandler
}
func newHooks(app *App) *Hooks {
@ -34,6 +36,7 @@ func newHooks(app *App) *Hooks {
onListen: make([]OnListenHandler, 0),
onShutdown: make([]OnShutdownHandler, 0),
onFork: make([]OnForkHandler, 0),
onMount: make([]OnMountHandler, 0),
}
}
@ -94,7 +97,22 @@ func (h *Hooks) OnFork(handler ...OnForkHandler) {
h.app.mutex.Unlock()
}
// OnMount is a hook to execute user function after mounting process.
// The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter.
// It works for app and group mounting.
func (h *Hooks) OnMount(handler ...OnMountHandler) {
h.app.mutex.Lock()
h.onMount = append(h.onMount, handler...)
h.app.mutex.Unlock()
}
func (h *Hooks) executeOnRouteHooks(route Route) error {
// Check mounting
if h.app.mountFields.mountPath != "" {
route.path = h.app.mountFields.mountPath + route.path
route.Path = route.path
}
for _, v := range h.onRoute {
if err := v(route); err != nil {
return err
@ -105,6 +123,12 @@ func (h *Hooks) executeOnRouteHooks(route Route) error {
}
func (h *Hooks) executeOnNameHooks(route Route) error {
// Check mounting
if h.app.mountFields.mountPath != "" {
route.path = h.app.mountFields.mountPath + route.path
route.Path = route.path
}
for _, v := range h.onName {
if err := v(route); err != nil {
return err
@ -115,6 +139,11 @@ func (h *Hooks) executeOnNameHooks(route Route) error {
}
func (h *Hooks) executeOnGroupHooks(group Group) error {
// Check mounting
if h.app.mountFields.mountPath != "" {
group.Prefix = h.app.mountFields.mountPath + group.Prefix
}
for _, v := range h.onGroup {
if err := v(group); err != nil {
return err
@ -125,6 +154,11 @@ func (h *Hooks) executeOnGroupHooks(group Group) error {
}
func (h *Hooks) executeOnGroupNameHooks(group Group) error {
// Check mounting
if h.app.mountFields.mountPath != "" {
group.Prefix = h.app.mountFields.mountPath + group.Prefix
}
for _, v := range h.onGroupName {
if err := v(group); err != nil {
return err
@ -155,3 +189,13 @@ func (h *Hooks) executeOnForkHooks(pid int) {
_ = v(pid)
}
}
func (h *Hooks) executeOnMountHooks(app *App) error {
for _, v := range h.onMount {
if err := v(app); err != nil {
return err
}
}
return nil
}

View File

@ -33,6 +33,29 @@ func Test_Hook_OnRoute(t *testing.T) {
app.Mount("/sub", subApp)
}
func Test_Hook_OnRoute_Mount(t *testing.T) {
t.Parallel()
app := New()
subApp := New()
app.Mount("/sub", subApp)
subApp.Hooks().OnRoute(func(r Route) error {
utils.AssertEqual(t, "/sub/test", r.Path)
return nil
})
app.Hooks().OnRoute(func(r Route) error {
utils.AssertEqual(t, "/", r.Path)
return nil
})
app.Get("/", testSimpleHandler).Name("x")
subApp.Get("/test", testSimpleHandler)
}
func Test_Hook_OnName(t *testing.T) {
t.Parallel()
@ -96,6 +119,24 @@ func Test_Hook_OnGroup(t *testing.T) {
utils.AssertEqual(t, "/x/x/a", buf.String())
}
func Test_Hook_OnGroup_Mount(t *testing.T) {
t.Parallel()
app := New()
micro := New()
micro.Mount("/john", app)
app.Hooks().OnGroup(func(g Group) error {
utils.AssertEqual(t, "/john/v1", g.Prefix)
return nil
})
v1 := app.Group("/v1")
v1.Get("/doe", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
}
func Test_Hook_OnGroupName(t *testing.T) {
t.Parallel()
@ -200,3 +241,21 @@ func Test_Hook_OnHook(t *testing.T) {
utils.AssertEqual(t, nil, app.prefork(NetworkTCP4, ":3000", nil))
}
func Test_Hook_OnMount(t *testing.T) {
t.Parallel()
app := New()
app.Get("/", testSimpleHandler).Name("x")
subApp := New()
subApp.Get("/test", testSimpleHandler)
subApp.Hooks().OnMount(func(parent *App) error {
utils.AssertEqual(t, parent.mountFields.mountPath, "")
return nil
})
app.Mount("/sub", subApp)
}

View File

@ -183,15 +183,6 @@ func (e *Engine) Load() error {
// 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)

146
mount.go Normal file
View File

@ -0,0 +1,146 @@
// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 🤖 Github Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
package fiber
import (
"sort"
"strings"
"sync"
"sync/atomic"
)
// Put fields related to mounting.
type mountFields struct {
// Mounted and main apps
appList map[string]*App
// Ordered keys of apps (sorted by key length for Render)
appListKeys []string
// check added routes of sub-apps
subAppsRoutesAdded sync.Once
// Prefix of app if it was mounted
mountPath string
}
// Create empty mountFields instance
func newMountFields(app *App) *mountFields {
return &mountFields{
appList: map[string]*App{"": app},
appListKeys: make([]string, 0),
}
}
// Mount attaches another app instance as a sub-router along a routing path.
// It's very useful to split up a large API as many independent routers and
// compose them as a single service using Mount. The fiber's error handler and
// any of the fiber's sub apps are added to the application's error handlers
// to be invoked on errors that happen within the prefix route.
func (app *App) Mount(prefix string, fiber *App) Router {
prefix = strings.TrimRight(prefix, "/")
if prefix == "" {
prefix = "/"
}
// Support for configs of mounted-apps and sub-mounted-apps
for mountedPrefixes, subApp := range fiber.mountFields.appList {
subApp.mountFields.mountPath = prefix + mountedPrefixes
app.mountFields.appList[prefix+mountedPrefixes] = subApp
}
// Execute onMount hooks
if err := fiber.hooks.executeOnMountHooks(app); err != nil {
panic(err)
}
return app
}
// Mount attaches another app instance as a sub-router along a routing path.
// It's very useful to split up a large API as many independent routers and
// compose them as a single service using Mount.
func (grp *Group) Mount(prefix string, fiber *App) Router {
groupPath := getGroupPath(grp.Prefix, prefix)
groupPath = strings.TrimRight(groupPath, "/")
if groupPath == "" {
groupPath = "/"
}
// Support for configs of mounted-apps and sub-mounted-apps
for mountedPrefixes, subApp := range fiber.mountFields.appList {
subApp.mountFields.mountPath = groupPath + mountedPrefixes
grp.app.mountFields.appList[groupPath+mountedPrefixes] = subApp
}
// Execute onMount hooks
if err := fiber.hooks.executeOnMountHooks(grp.app); err != nil {
panic(err)
}
return grp
}
// The MountPath property contains one or more path patterns on which a sub-app was mounted.
func (app *App) MountPath() string {
return app.mountFields.mountPath
}
// generateAppListKeys generates app list keys for Render, should work after appendSubAppLists
func (app *App) generateAppListKeys() {
for key := range app.mountFields.appList {
app.mountFields.appListKeys = append(app.mountFields.appListKeys, key)
}
sort.Slice(app.mountFields.appListKeys, func(i, j int) bool {
return len(app.mountFields.appListKeys[i]) < len(app.mountFields.appListKeys[j])
})
}
// appendSubAppLists supports nested for sub apps
func (app *App) appendSubAppLists(appList map[string]*App, parent ...string) {
for prefix, subApp := range appList {
// skip real app
if prefix == "" {
continue
}
if len(parent) > 0 {
prefix = parent[0] + prefix
}
if _, ok := app.mountFields.appList[prefix]; !ok {
app.mountFields.appList[prefix] = subApp
}
// The first element of appList is always the app itself. If there are no other sub apps, we should skip appending nested apps.
if len(subApp.mountFields.appList) > 1 {
app.appendSubAppLists(subApp.mountFields.appList, prefix)
}
}
}
// addSubAppsRoutes adds routes of sub apps nestedly when to start the server
func (app *App) addSubAppsRoutes(appList map[string]*App, parent ...string) {
for prefix, subApp := range appList {
// skip real app
if prefix == "" {
continue
}
if len(parent) > 0 {
prefix = parent[0] + prefix
}
// add routes
stack := subApp.stack
for m := range stack {
for r := range stack[m] {
route := app.copyRoute(stack[m][r])
app.addRoute(route.Method, app.addPrefixToRoute(prefix, route), true)
}
}
atomic.AddUint32(&app.handlersCount, subApp.handlersCount)
}
}

351
mount_test.go Normal file
View File

@ -0,0 +1,351 @@
// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 🤖 Github Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
package fiber
import (
"errors"
"io/ioutil"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2/internal/template/html"
"github.com/gofiber/fiber/v2/utils"
)
// go test -run Test_App_Mount
func Test_App_Mount(t *testing.T) {
micro := New()
micro.Get("/doe", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
app := New()
app.Mount("/john", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
utils.AssertEqual(t, uint32(2), app.handlersCount)
}
// go test -run Test_App_Mount_Nested
func Test_App_Mount_Nested(t *testing.T) {
app := New()
one := New()
two := New()
three := New()
two.Mount("/three", three)
app.Mount("/one", one)
one.Mount("/two", two)
one.Get("/doe", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
two.Get("/nested", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
three.Get("/test", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/one/doe", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/nested", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/three/test", nil))
utils.AssertEqual(t, nil, err, "app.Test(req)")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
utils.AssertEqual(t, uint32(6), app.handlersCount)
}
// go test -run Test_App_MountPath
func Test_App_MountPath(t *testing.T) {
app := New()
one := New()
two := New()
three := New()
two.Mount("/three", three)
one.Mount("/two", two)
app.Mount("/one", one)
utils.AssertEqual(t, "/one", one.MountPath())
utils.AssertEqual(t, "/one/two", two.MountPath())
utils.AssertEqual(t, "/one/two/three", three.MountPath())
utils.AssertEqual(t, "", app.MountPath())
}
func Test_App_ErrorHandler_GroupMount(t *testing.T) {
micro := New(Config{
ErrorHandler: func(c *Ctx, err error) error {
utils.AssertEqual(t, "0: GET error", err.Error())
return c.Status(500).SendString("1: custom error")
},
})
micro.Get("/doe", func(c *Ctx) error {
return errors.New("0: GET error")
})
app := New()
v1 := app.Group("/v1")
v1.Mount("/john", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
testErrorResponse(t, err, resp, "1: custom error")
}
func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) {
micro := New(Config{
ErrorHandler: func(c *Ctx, err error) error {
utils.AssertEqual(t, "0: GET error", err.Error())
return c.Status(500).SendString("1: custom error")
},
})
micro.Get("/john/doe", func(c *Ctx) error {
return errors.New("0: GET error")
})
app := New()
v1 := app.Group("/v1")
v1.Mount("/", micro)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
testErrorResponse(t, err, resp, "1: custom error")
}
// go test -run Test_App_Group_Mount
func Test_App_Group_Mount(t *testing.T) {
micro := New()
micro.Get("/doe", func(c *Ctx) error {
return c.SendStatus(StatusOK)
})
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")
utils.AssertEqual(t, uint32(2), app.handlersCount)
}
func Test_App_UseMountedErrorHandler(t *testing.T) {
app := New()
fiber := New(Config{
ErrorHandler: func(ctx *Ctx, err error) error {
return ctx.Status(500).SendString("hi, i'm a custom error")
},
})
fiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
app.Mount("/api", fiber)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
testErrorResponse(t, err, resp, "hi, i'm a custom error")
}
func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) {
app := New()
fiber := New(Config{
ErrorHandler: func(ctx *Ctx, err error) error {
return ctx.Status(500).SendString("hi, i'm a custom error")
},
})
fiber.Get("/api", func(c *Ctx) error {
return errors.New("something happened")
})
app.Mount("/", fiber)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
testErrorResponse(t, err, resp, "hi, i'm a custom error")
}
func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) {
app := New()
tsf := func(ctx *Ctx, err error) error {
return ctx.Status(200).SendString("hi, i'm a custom sub sub fiber error")
}
tripleSubFiber := New(Config{
ErrorHandler: tsf,
})
tripleSubFiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
sf := func(ctx *Ctx, err error) error {
return ctx.Status(200).SendString("hi, i'm a custom sub fiber error")
}
subfiber := New(Config{
ErrorHandler: sf,
})
subfiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
subfiber.Mount("/third", tripleSubFiber)
f := func(ctx *Ctx, err error) error {
return ctx.Status(200).SendString("hi, i'm a custom error")
}
fiber := New(Config{
ErrorHandler: f,
})
fiber.Get("/", func(c *Ctx) error {
return errors.New("something happened")
})
fiber.Mount("/sub", subfiber)
app.Mount("/api", fiber)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", nil))
utils.AssertEqual(t, nil, err, "/api/sub req")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
b, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
utils.AssertEqual(t, "hi, i'm a custom sub fiber error", string(b), "Response body")
resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", nil))
utils.AssertEqual(t, nil, err, "/api/sub/third req")
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
b, err = ioutil.ReadAll(resp2.Body)
utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
utils.AssertEqual(t, "hi, i'm a custom sub sub fiber error", string(b), "Third fiber Response body")
}
// 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))
}
// go test -run Test_Ctx_Render_Mount_ParentOrSubHasViews
func Test_Ctx_Render_Mount_ParentOrSubHasViews(t *testing.T) {
t.Parallel()
engine := &testTemplateEngine{}
err := engine.Load()
utils.AssertEqual(t, nil, err)
engine2 := &testTemplateEngine{path: "testdata2"}
err = engine2.Load()
utils.AssertEqual(t, nil, err)
sub := New(Config{
Views: html.New("./.github/testdata/template", ".gohtml"),
})
sub2 := New(Config{
Views: engine2,
})
app := New(Config{
Views: engine,
})
app.Get("/test", func(c *Ctx) error {
return c.Render("index.tmpl", Map{
"Title": "Hello, World!",
})
})
sub.Get("/world/:name", func(c *Ctx) error {
return c.Render("hello_world", Map{
"Name": c.Params("name"),
})
})
sub2.Get("/moment", func(c *Ctx) error {
return c.Render("bruh.tmpl", Map{})
})
sub.Mount("/bruh", sub2)
app.Mount("/hello", sub)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/world/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))
resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", 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, World!</h1>", string(body))
resp, err = app.Test(httptest.NewRequest(MethodGet, "/hello/bruh/moment", 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>I'm Bruh</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))
}

View File

@ -423,7 +423,13 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router {
return app
}
func (app *App) addRoute(method string, route *Route) {
func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
// Check mounted routes
var mounted bool
if len(isMounted) > 0 {
mounted = isMounted[0]
}
// Get unique HTTP method identifier
m := methodInt(method)
@ -441,12 +447,15 @@ func (app *App) addRoute(method string, route *Route) {
app.routesRefreshed = true
}
// Execute onRoute hooks & change latestRoute if not adding mounted route
if !mounted {
app.mutex.Lock()
app.latestRoute = route
if err := app.hooks.executeOnRouteHooks(*route); err != nil {
panic(err)
}
app.mutex.Unlock()
}
}
// buildTree build the prefix tree from the previously registered routes
@ -454,6 +463,7 @@ func (app *App) buildTree() *App {
if !app.routesRefreshed {
return app
}
// loop all the methods and stacks and create the prefix tree
for m := range intMethod {
tsMap := make(map[string][]*Route)
@ -467,6 +477,7 @@ func (app *App) buildTree() *App {
}
app.treeStack[m] = tsMap
}
// loop the methods and tree stacks and add global stack and sort everything
for m := range intMethod {
tsMap := app.treeStack[m]