From 3de2f6e74cddceee33219677ca59842ecae0f337 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 23 Jan 2024 12:28:06 +0300 Subject: [PATCH 1/6] :sparkles: v3 (feature): add support for custom constraints --- app.go | 7 ++++++ app_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++ docs/api/app.md | 10 ++++++++ docs/guide/routing.md | 50 ++++++++++++++++++++++++++++++++++++++ go.sum | 18 +++----------- path.go | 44 +++++++++++++++++++++++++--------- router.go | 6 ++--- 7 files changed, 162 insertions(+), 29 deletions(-) diff --git a/app.go b/app.go index 5f0007a35f..d7bb6abd4c 100644 --- a/app.go +++ b/app.go @@ -121,6 +121,8 @@ type App struct { mountFields *mountFields // Indicates if the value was explicitly configured configured Config + // customConstraints is a list of external constraints + customConstraints []CustomConstraint } // Config is a struct holding the server settings. @@ -588,6 +590,11 @@ func (app *App) NewCtxFunc(function func(app *App) CustomCtx) { app.newCtxFunc = function } +// RegisterCustomConstraint allows to register custom constraint. +func (app *App) RegisterCustomConstraint(constraint CustomConstraint) { + app.customConstraints = append(app.customConstraints, constraint) +} + // You can register custom binders to use as Bind().Custom("name"). // They should be compatible with CustomBinder interface. func (app *App) RegisterCustomBinder(binder CustomBinder) { diff --git a/app_test.go b/app_test.go index e61d23a95d..78ada401c0 100644 --- a/app_test.go +++ b/app_test.go @@ -178,6 +178,62 @@ func Test_App_Errors(t *testing.T) { } } +type customConstraint struct{} + +func (c *customConstraint) Name() string { + return "test" +} + +func (c *customConstraint) Execute(param string, args ...string) bool { + if param == "test" && len(args) == 1 && args[0] == "test" { + return true + } + + if len(args) == 0 && param == "c" { + return true + } + + return false +} + +func Test_App_CustomConstraint(t *testing.T) { + app := New() + app.RegisterCustomConstraint(&customConstraint{}) + + app.Get("/test/:param", func(c Ctx) error { + return c.SendString("test") + }) + + app.Get("/test2/:param", func(c Ctx) error { + return c.SendString("test") + }) + + app.Get("/test3/:param", func(c Ctx) error { + return c.SendString("test") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/test", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test/test2", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/c", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/cc", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/cc", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + +} + func Test_App_ErrorHandler_Custom(t *testing.T) { t.Parallel() app := New(Config{ diff --git a/docs/api/app.md b/docs/api/app.md index 29022a3104..2e58a426d1 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -617,6 +617,16 @@ ln = tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{cer}}) app.Listener(ln) ``` +## RegisterCustomConstraint + +RegisterCustomConstraint allows to register custom constraint. + +```go title="Signature" +func (app *App) RegisterCustomConstraint(constraint CustomConstraint) +``` + +See [Custom Constraint](../guide/routing.md#custom-constraint) section for more information. + ## Test Testing your application is done with the **Test** method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s` if you want to disable a timeout altogether, pass `-1` as a second argument. diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 31d187aeb5..130c00cb57 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -240,6 +240,56 @@ app.Get("/:test?", func(c fiber.Ctx) error { // Cannot GET /7.0 ``` +**Custom Constraint Example** + +Custom constraints can be added to Fiber using the `app.RegisterCustomConstraint` method. Your constraints have to be compatible with the `CustomConstraint` interface. + +```go +// CustomConstraint is an interface for custom constraints +type CustomConstraint interface { + // Name returns the name of the constraint. + // This name is used in the constraint matching. + Name() string + + // Execute executes the constraint. + // It returns true if the constraint is matched and right. + // param is the parameter value to check. + // args are the constraint arguments. + Execute(param string, args ...string) bool +} +``` + +You can check the example below: + +```go +type UlidConstraint struct { + fiber.CustomConstraint +} + +func (*UlidConstraint) Name() string { + return "ulid" +} + +func (*UlidConstraint) Execute(param string, args ...string) bool { + _, err := ulid.Parse(param) + return err == nil +} + +func main() { + app := fiber.New() + app.RegisterCustomConstraint(&UlidConstraint{}) + + app.Get("/login/:id", func(c fiber.Ctx) error { + return c.SendString("...") + }) + + app.Listen(":3000") + + // /login/01HK7H9ZE5BFMK348CPYP14S0Z -> 200 + // /login/12345 -> 404 +} +``` + ## Middleware Functions that are designed to make changes to the request or response are called **middleware functions**. The [Next](../api/ctx.md#next) is a **Fiber** router function, when called, executes the **next** function that **matches** the current route. diff --git a/go.sum b/go.sum index babb349db9..47a38d262f 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,16 @@ -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/utils/v2 v2.0.0-beta.3 h1:pfOhUDDVjBJpkWv6C5jaDyYLvpui7zQ97zpyFFsUOKw= github.com/gofiber/utils/v2 v2.0.0-beta.3/go.mod h1:jsl17+MsKfwJjM3ONCE9Rzji/j8XNbwjhUVTjzgfDCo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= -github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= @@ -31,8 +23,6 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= -github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= @@ -57,8 +47,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -76,4 +64,4 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/path.go b/path.go index bfcfe68e6f..a51b31562e 100644 --- a/path.go +++ b/path.go @@ -65,9 +65,24 @@ const ( type TypeConstraint int16 type Constraint struct { - ID TypeConstraint - RegexCompiler *regexp.Regexp - Data []string + ID TypeConstraint + RegexCompiler *regexp.Regexp + Data []string + Name string + customConstraints []CustomConstraint +} + +// CustomConstraint is an interface for custom constraints +type CustomConstraint interface { + // Name returns the name of the constraint. + // This name is used in the constraint matching. + Name() string + + // Execute executes the constraint. + // It returns true if the constraint is matched and right. + // param is the parameter value to check. + // args are the constraint arguments. + Execute(param string, args ...string) bool } const ( @@ -175,15 +190,14 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { // parseRoute analyzes the route and divides it into segments for constant areas and parameters, // this information is needed later when assigning the requests to the declared routes -func parseRoute(pattern string) routeParser { +func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { parser := routeParser{} - part := "" for len(pattern) > 0 { nextParamPosition := findNextParamPosition(pattern) // handle the parameter part if nextParamPosition == 0 { - processedPart, seg := parser.analyseParameterPart(pattern) + processedPart, seg := parser.analyseParameterPart(pattern, customConstraints...) parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart } else { processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition) @@ -284,7 +298,7 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) ( } // analyseParameterPart find the parameter end and create the route segment -func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) { +func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (string, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam @@ -332,7 +346,9 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r // Assign constraint if start != -1 && end != -1 { constraint := &Constraint{ - ID: getParamConstraintType(c[:start]), + ID: getParamConstraintType(c[:start]), + Name: c[:start], + customConstraints: customConstraints, } // remove escapes from data @@ -355,8 +371,10 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r constraints = append(constraints, constraint) } else { constraints = append(constraints, &Constraint{ - ID: getParamConstraintType(c), - Data: []string{}, + ID: getParamConstraintType(c), + Data: []string{}, + Name: c, + customConstraints: customConstraints, }) } } @@ -666,7 +684,11 @@ func (c *Constraint) CheckConstraint(param string) bool { // check constraints switch c.ID { case noConstraint: - // Nothing to check + for _, cc := range c.customConstraints { + if cc.Name() == c.Name { + return cc.Execute(param, c.Data...) + } + } case intConstraint: _, err = strconv.Atoi(param) case boolConstraint: diff --git a/router.go b/router.go index f25e98c565..b210b5a56d 100644 --- a/router.go +++ b/router.go @@ -260,7 +260,7 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { route.Path = prefixedPath route.path = RemoveEscapeChar(prettyPath) - route.routeParser = parseRoute(prettyPath) + route.routeParser = parseRoute(prettyPath, app.customConstraints...) route.root = false route.star = false @@ -335,8 +335,8 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler // Is path a root slash? isRoot := pathPretty == "/" // Parse path parameters - parsedRaw := parseRoute(pathRaw) - parsedPretty := parseRoute(pathPretty) + parsedRaw := parseRoute(pathRaw, app.customConstraints...) + parsedPretty := parseRoute(pathPretty, app.customConstraints...) // Create route metadata without pointer route := Route{ From f03e293fe9816439b13ef6221c437cd1873a6835 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 23 Jan 2024 12:34:33 +0300 Subject: [PATCH 2/6] fix linter --- app_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app_test.go b/app_test.go index 78ada401c0..1e1e239373 100644 --- a/app_test.go +++ b/app_test.go @@ -180,7 +180,7 @@ func Test_App_Errors(t *testing.T) { type customConstraint struct{} -func (c *customConstraint) Name() string { +func (*customConstraint) Name() string { return "test" } @@ -231,7 +231,6 @@ func Test_App_CustomConstraint(t *testing.T) { resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/cc", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") - } func Test_App_ErrorHandler_Custom(t *testing.T) { From cf8d0713cce0d2d0dfd1bfc45dbd83aef315da15 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 23 Jan 2024 12:40:41 +0300 Subject: [PATCH 3/6] fix --- app_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app_test.go b/app_test.go index 1e1e239373..dee1d3eebb 100644 --- a/app_test.go +++ b/app_test.go @@ -184,7 +184,7 @@ func (*customConstraint) Name() string { return "test" } -func (c *customConstraint) Execute(param string, args ...string) bool { +func (*customConstraint) Execute(param string, args ...string) bool { if param == "test" && len(args) == 1 && args[0] == "test" { return true } @@ -230,7 +230,7 @@ func Test_App_CustomConstraint(t *testing.T) { resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/cc", nil)) require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, 404, resp.StatusCode, "Status code") } func Test_App_ErrorHandler_Custom(t *testing.T) { From e6cf3eabe5a7e38c03ac846166a20d9cfbc3bb42 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 23 Jan 2024 12:46:31 +0300 Subject: [PATCH 4/6] disable goconst linter for tests --- app_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app_test.go b/app_test.go index dee1d3eebb..a6144883e9 100644 --- a/app_test.go +++ b/app_test.go @@ -3,6 +3,7 @@ // 📌 API Documentation: https://docs.gofiber.io //nolint:bodyclose // Much easier to just ignore memory leaks in tests +//nolint:goconst // No need to create a constant for a test package fiber import ( From 9afcebc6b32ef26aba5b93a7f0914dac35396f0b Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 25 Jan 2024 18:05:48 +0300 Subject: [PATCH 5/6] update docs --- docs/guide/routing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 130c00cb57..70d746a317 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -244,6 +244,9 @@ app.Get("/:test?", func(c fiber.Ctx) error { Custom constraints can be added to Fiber using the `app.RegisterCustomConstraint` method. Your constraints have to be compatible with the `CustomConstraint` interface. +It is a good idea to add external constraints to your project once you want to add more specific rules to your routes. +For example, you can add a constraint to check if a parameter is a valid ULID. + ```go // CustomConstraint is an interface for custom constraints type CustomConstraint interface { From 6a336dcf26f3274b4830fe9723bb3adcb9c985f8 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 25 Jan 2024 18:11:08 +0300 Subject: [PATCH 6/6] update --- app_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app_test.go b/app_test.go index a6144883e9..18016f1a8f 100644 --- a/app_test.go +++ b/app_test.go @@ -2,8 +2,7 @@ // 🤖 Github Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io -//nolint:bodyclose // Much easier to just ignore memory leaks in tests -//nolint:goconst // No need to create a constant for a test +//nolint:bodyclose, goconst // Much easier to just ignore memory leaks in tests package fiber import (