From 156b81c768ec3e9e8b4cfdf6a37040fa8daa0e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Tue, 25 Oct 2022 08:51:44 +0300 Subject: [PATCH] :bug: bug: improve mounting behavior (#2120) * :bug: bug: fix mounting doesn't work if when to declare it before routes * :bug: bug: fix mounting doesn't work if when to declare it before routes * :bug: bug: fix mounting doesn't work if when to declare it before routes * :bug: bug: fix mounting doesn't work if when to declare it before routes * :bug: 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 --- .github/testdata2/bruh.tmpl | 1 + app.go | 56 ++---- app_test.go | 161 --------------- ctx.go | 12 +- ctx_test.go | 59 +----- group.go | 30 --- hooks.go | 44 +++++ hooks_test.go | 59 ++++++ internal/template/html/html.go | 9 - mount.go | 146 ++++++++++++++ mount_test.go | 351 +++++++++++++++++++++++++++++++++ router.go | 23 ++- 12 files changed, 649 insertions(+), 302 deletions(-) create mode 100644 .github/testdata2/bruh.tmpl create mode 100644 mount.go create mode 100644 mount_test.go diff --git a/.github/testdata2/bruh.tmpl b/.github/testdata2/bruh.tmpl new file mode 100644 index 00000000..28d75b49 --- /dev/null +++ b/.github/testdata2/bruh.tmpl @@ -0,0 +1 @@ +

I'm Bruh

\ No newline at end of file diff --git a/app.go b/app.go index ace88eb8..f725424e 100644 --- a/app.go +++ b/app.go @@ -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 } diff --git a/app_test.go b/app_test.go index 6bbd8fcb..acfa8f9b 100644 --- a/app_test.go +++ b/app_test.go @@ -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) diff --git a/ctx.go b/ctx.go index 327b3193..0fa466e5 100644 --- a/ctx.go +++ b/ctx.go @@ -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. diff --git a/ctx_test.go b/ctx_test.go index ad975de9..f1ba1f6c 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -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, "

Hello a!

", 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, "

Hello doe!

", 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 } diff --git a/group.go b/group.go index ab74b80e..a59eaed8 100644 --- a/group.go +++ b/group.go @@ -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() diff --git a/hooks.go b/hooks.go index f614ef13..b2766a8c 100644 --- a/hooks.go +++ b/hooks.go @@ -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 +} diff --git a/hooks_test.go b/hooks_test.go index 5ed9d821..e39d269c 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -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) +} diff --git a/internal/template/html/html.go b/internal/template/html/html.go index da9d8c52..71f6f02c 100644 --- a/internal/template/html/html.go +++ b/internal/template/html/html.go @@ -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) diff --git a/mount.go b/mount.go new file mode 100644 index 00000000..f7df3a97 --- /dev/null +++ b/mount.go @@ -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) + } +} diff --git a/mount_test.go b/mount_test.go new file mode 100644 index 00000000..1fa91359 --- /dev/null +++ b/mount_test.go @@ -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, "

Hello a!

", 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, "

Hello a!

", 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, "

Hello, World!

", 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, "

I'm Bruh

", 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, "

Hello doe!

", string(body)) +} diff --git a/router.go b/router.go index ff51bb0d..642149d5 100644 --- a/router.go +++ b/router.go @@ -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 } - app.mutex.Lock() - app.latestRoute = route - if err := app.hooks.executeOnRouteHooks(*route); err != nil { - panic(err) + // 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() } - 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]