Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

✨ feature: add initial support for hooks #1777

Merged
merged 30 commits into from Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b0933d1
Add initial support for hooks.
efectn Feb 14, 2022
b43788d
release ctx, mutex.
efectn Feb 17, 2022
76a2e45
Add unit tests.
efectn Feb 18, 2022
c61eac9
add comment lines.
efectn Feb 18, 2022
124671b
update
efectn Feb 18, 2022
7292511
update
efectn Feb 18, 2022
649bb6c
Merge branch 'gofiber:master' into add-hooks-support
efectn Feb 19, 2022
2faecaa
remove unnecessary code.
efectn Feb 19, 2022
2e18ea0
Merge branch 'add-hooks-support' of https://github.com/efectn/fiber i…
efectn Feb 19, 2022
429ee79
fix race condition.
efectn Feb 19, 2022
8a04452
fix gosec.
efectn Feb 20, 2022
936a260
skip error handling for onshutdown and onresponse.
efectn Feb 20, 2022
495e722
update
efectn Feb 20, 2022
2d8dce2
separate hooks from app.go
efectn Feb 21, 2022
ca72c2c
make hooks field private, hook struct public and Hooks() func.
efectn Feb 23, 2022
7dd783b
remove onreq and onres because of they can be done by middlewares.
efectn Feb 25, 2022
5b6073a
OnGroupName method.
efectn Feb 25, 2022
a9605c3
Update hooks.go
efectn Feb 26, 2022
57de317
handle errors for name and groupname
efectn Feb 28, 2022
401e4c6
Merge branch 'add-hooks-support' of https://github.com/efectn/fiber i…
efectn Feb 28, 2022
bf99b34
fix tests.
efectn Feb 28, 2022
02fa494
Update app.go
hi019 Mar 2, 2022
1e4138e
use struct fields instead of map
efectn Mar 2, 2022
8efce6f
Merge branch 'add-hooks-support' of https://github.com/efectn/fiber i…
efectn Mar 2, 2022
4bb277e
add multi-handler.
efectn Mar 2, 2022
fecaa73
add onGroup, make prefix field public on Group struct.
efectn Mar 2, 2022
3dbf49b
Update hooks.go
efectn Mar 5, 2022
0223ed1
add newhooks method.
efectn Mar 7, 2022
8d9f0dc
✨ feature: add initial support for hooks
ReneWerner87 Mar 8, 2022
34592bc
remove ctx from hooks.
efectn Mar 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 111 additions & 0 deletions app.go
Expand Up @@ -41,6 +41,9 @@ const Version = "2.27.0"
// Handler defines a function to serve HTTP requests.
type Handler = func(*Ctx) error

// Handler defines a function to create hooks for Fibe.
type HookHandler = func(*Ctx, Map) error

// Map is a shortcut for map[string]interface{}, useful for JSON returns
type Map map[string]interface{}

Expand Down Expand Up @@ -114,6 +117,9 @@ type App struct {

// Mounted and main apps
appList map[string]*App

// hooks
hookList map[string][]HookHandler
}

// Config is a struct holding the server settings.
Expand Down Expand Up @@ -465,6 +471,7 @@ func New(config ...Config) *App {
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
appList: make(map[string]*App),
hookList: make(map[string][]HookHandler),
}
// Override config if provided
if len(config) > 0 {
Expand Down Expand Up @@ -575,7 +582,9 @@ func (app *App) Name(name string) Router {
} else {
latestRoute.route.Name = name
}
app.executeOnNameHooks(*latestRoute.route)
latestRoute.mu.Unlock()

return app
}

Expand Down Expand Up @@ -859,6 +868,8 @@ func (app *App) HandlersCount() uint32 {
//
// Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.
func (app *App) Shutdown() error {
defer app.executeOnShutdownHooks()

app.mutex.Lock()
defer app.mutex.Unlock()
if app.server == nil {
Expand Down Expand Up @@ -1038,6 +1049,8 @@ func (app *App) serverErrorHandler(fctx *fasthttp.RequestCtx, err error) {

// startupProcess Is the method which executes all the necessary processes just before the start of the server.
func (app *App) startupProcess() *App {
app.executeOnListenHooks()

app.mutex.Lock()
app.buildTree()
app.mutex.Unlock()
Expand Down Expand Up @@ -1302,3 +1315,101 @@ func (app *App) printRoutesMessage() {

_ = w.Flush()
}

// OnRoute is a hook to execute user functions on each route registeration.
// Also you can get route properties by "route" key of map.
func (app *App) OnRoute(handler ...HookHandler) {
app.mutex.Lock()
app.hookList["onRoute"] = append(app.hookList["onRoute"], handler...)
app.mutex.Unlock()
}

// OnName is a hook to execute user functions on each route naming.
// Also you can get route properties by "route" key of map.
//
// WARN: OnName only works with naming routes, not groups.
func (app *App) OnName(handler ...HookHandler) {
app.mutex.Lock()
app.hookList["onName"] = append(app.hookList["onName"], handler...)
app.mutex.Unlock()
}

// OnListen is a hook to execute user functions on Listen, ListenTLS, Listener.
func (app *App) OnListen(handler ...HookHandler) {
app.mutex.Lock()
app.hookList["onListen"] = append(app.hookList["onListen"], handler...)
app.mutex.Unlock()
}

// OnShutdown is a hook to execute user functions after Shutdown.
func (app *App) OnShutdown(handler ...HookHandler) {
app.mutex.Lock()
app.hookList["onShutdown"] = append(app.hookList["onShutdown"], handler...)
app.mutex.Unlock()
}

// OnResponse is a hook to execute user functions after a response.
//
// WARN: You can't edit response with OnResponse hook.
func (app *App) OnResponse(handler ...HookHandler) {
app.mutex.Lock()
app.hookList["onResponse"] = append(app.hookList["onResponse"], handler...)
app.mutex.Unlock()
}

// OnRequest is a hook to execute user functions after a request.
//
// WARN: You can edit response with OnRequest hook.
func (app *App) OnRequest(handler ...HookHandler) {
app.mutex.Lock()
app.hookList["onRequest"] = append(app.hookList["onRequest"], handler...)
app.mutex.Unlock()
}

func (app *App) executeOnRouteHooks(route Route) {
for _, v := range app.hookList["onRoute"] {
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)

v(ctx, Map{"route": route})
efectn marked this conversation as resolved.
Show resolved Hide resolved
}
}

func (app *App) executeOnNameHooks(route Route) {
for _, v := range app.hookList["onName"] {
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)

v(ctx, Map{"route": route})
}
}

func (app *App) executeOnListenHooks() {
for _, v := range app.hookList["onListen"] {
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)

v(ctx, Map{})
}
}

func (app *App) executeOnShutdownHooks() {
for _, v := range app.hookList["onShutdown"] {
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)

v(ctx, Map{})
}
}

func (app *App) executeOnRequestHooks(c *Ctx) {
for _, v := range app.hookList["onRequest"] {
v(c, Map{})
}
}

func (app *App) executeOnResponseHooks(c *Ctx) {
for _, v := range app.hookList["onResponse"] {
v(c, Map{})
}
}
134 changes: 134 additions & 0 deletions app_test.go
Expand Up @@ -24,6 +24,7 @@ import (
"testing"
"time"

"github.com/gofiber/fiber/v2/internal/bytebufferpool"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttputil"
Expand All @@ -33,6 +34,10 @@ var testEmptyHandler = func(c *Ctx) error {
return nil
}

var testSimpleHandler = func(c *Ctx) error {
return c.SendString("simple")
}

func testStatus200(t *testing.T, app *App, url string, method string) {
t.Helper()

Expand Down Expand Up @@ -1682,3 +1687,132 @@ func Test_App_print_Route_with_group(t *testing.T) {
utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "PUT"))
utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "/v1/test/fiber/*"))
}

func Test_Hook_OnRoute(t *testing.T) {
app := New()

app.OnRoute(func(c *Ctx, m Map) error {
utils.AssertEqual(t, "", m["route"].(Route).Name)

return nil
})

app.Get("/", testSimpleHandler).Name("x")

subApp := New()
subApp.Get("/test", testSimpleHandler)

app.Mount("/sub", subApp)
}

func Test_Hook_OnName(t *testing.T) {
app := New()

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

app.OnName(func(c *Ctx, m Map) error {
buf.WriteString(m["route"].(Route).Name)

return nil
})

app.Get("/", testSimpleHandler).Name("index")

subApp := New()
subApp.Get("/test", testSimpleHandler)
subApp.Get("/test2", testSimpleHandler)

app.Mount("/sub", subApp)

utils.AssertEqual(t, "index", buf.String())
}

func Test_Hook_OnShutdown(t *testing.T) {
app := New()

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

app.OnShutdown(func(c *Ctx, m Map) error {
buf.WriteString("shutdowning")

return nil
})

utils.AssertEqual(t, nil, app.Shutdown())
utils.AssertEqual(t, "shutdowning", buf.String())
}

func Test_Hook_OnRequest(t *testing.T) {
app := New()

app.OnRequest(func(c *Ctx, m Map) error {
return c.SendString("-")
})

app.Get("/", testSimpleHandler)

subApp := New()
subApp.Get("/test", testSimpleHandler)

app.Mount("/sub", subApp)

resp, err := app.Test(httptest.NewRequest("GET", "/sub/test", nil))
utils.AssertEqual(t, nil, err)

body, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "-", string(body))
}

func Test_Hook_OnResponse(t *testing.T) {
app := New()

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

app.OnResponse(func(c *Ctx, m Map) error {
buf.WriteString(c.Path() + "-")

return nil
})

app.Get("/", testSimpleHandler)

subApp := New()
subApp.Get("/test", testSimpleHandler)

app.Mount("/sub", subApp)

_, err := app.Test(httptest.NewRequest("GET", "/sub/test", nil))
utils.AssertEqual(t, nil, err)

_, err = app.Test(httptest.NewRequest("GET", "/", nil))
utils.AssertEqual(t, nil, err)

utils.AssertEqual(t, "/sub/test-/-", buf.String())
}

func Test_Hook_OnListen(t *testing.T) {
app := New(Config{
DisableStartupMessage: true,
})

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

app.OnListen(func(c *Ctx, m Map) error {
buf.WriteString("ready")

return nil
})

go func() {
time.Sleep(1000 * time.Millisecond)
utils.AssertEqual(t, nil, app.Shutdown())
}()
utils.AssertEqual(t, nil, app.Listen(":9000"))

utils.AssertEqual(t, "ready", buf.String())
}
6 changes: 6 additions & 0 deletions ctx.go
Expand Up @@ -507,6 +507,7 @@ func (c *Ctx) Format(body interface{}) error {
case "txt":
return c.SendString(b)
case "xml":
defer c.app.executeOnResponseHooks(c)
raw, err := xml.Marshal(body)
if err != nil {
return fmt.Errorf("error serializing xml: %v", body)
Expand Down Expand Up @@ -692,6 +693,7 @@ func (c *Ctx) Is(extension string) bool {
// and a nil slice encodes as the null JSON value.
// This method also sets the content header to application/json.
func (c *Ctx) JSON(data interface{}) error {
defer c.app.executeOnResponseHooks(c)
raw, err := c.app.config.JSONEncoder(data)
if err != nil {
return err
Expand Down Expand Up @@ -1127,6 +1129,7 @@ func (c *Ctx) RedirectBack(fallback string, status ...int) error {
// Render a template with data and sends a text/html response.
// We support the following engines: html, amber, handlebars, mustache, pug
func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error {
defer c.app.executeOnResponseHooks(c)
var err error
// Get new buffer from pool
buf := bytebufferpool.Get()
Expand Down Expand Up @@ -1247,6 +1250,7 @@ func (c *Ctx) Secure() bool {
// Send sets the HTTP response body without copying it.
// From this point onward the body argument must not be changed.
func (c *Ctx) Send(body []byte) error {
defer c.app.executeOnResponseHooks(c)
// Write response body
c.fasthttp.Response.SetBodyRaw(body)
return nil
Expand Down Expand Up @@ -1338,13 +1342,15 @@ func (c *Ctx) SendStatus(status int) error {
// SendString sets the HTTP response body for string types.
// This means no type assertion, recommended for faster performance
func (c *Ctx) SendString(body string) error {
defer c.app.executeOnResponseHooks(c)
c.fasthttp.Response.SetBodyString(body)

return nil
}

// SendStream sets response body stream and optional body size.
func (c *Ctx) SendStream(stream io.Reader, size ...int) error {
defer c.app.executeOnResponseHooks(c)
if len(size) > 0 && size[0] >= 0 {
c.fasthttp.Response.SetBodyStream(stream, size[0])
} else {
Expand Down
4 changes: 4 additions & 0 deletions router.go
Expand Up @@ -167,6 +167,9 @@ func (app *App) handler(rctx *fasthttp.RequestCtx) {
if match && app.config.ETag {
setETag(c, false)
}

app.executeOnRequestHooks(c)

// Release Ctx
app.ReleaseCtx(c)
}
Expand Down Expand Up @@ -437,6 +440,7 @@ func (app *App) addRoute(method string, route *Route) {

latestRoute.mu.Lock()
latestRoute.route = route
app.executeOnRouteHooks(*route)
latestRoute.mu.Unlock()
}

Expand Down