diff --git a/app.go b/app.go index 84059e79..d4d89d92 100644 --- a/app.go +++ b/app.go @@ -109,7 +109,7 @@ type App struct { // Route stack divided by HTTP methods stack [][]*Route // Route stack divided by HTTP methods and route prefixes - treeStack []map[string][]*Route + treeStack []map[int][]*Route // custom binders customBinders []CustomBinder // customConstraints is a list of external constraints @@ -581,7 +581,7 @@ func New(config ...Config) *App { // Create router stack app.stack = make([][]*Route, len(app.config.RequestMethods)) - app.treeStack = make([]map[string][]*Route, len(app.config.RequestMethods)) + app.treeStack = make([]map[int][]*Route, len(app.config.RequestMethods)) // Override colors app.config.ColorScheme = defaultColors(app.config.ColorScheme) diff --git a/ctx.go b/ctx.go index b2d13c0a..b959a48f 100644 --- a/ctx.go +++ b/ctx.go @@ -33,8 +33,11 @@ const ( schemeHTTPS = "https" ) -// maxParams defines the maximum number of parameters per route. -const maxParams = 30 +const ( + // maxParams defines the maximum number of parameters per route. + maxParams = 30 + maxDetectionPaths = 3 +) // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. @@ -49,28 +52,26 @@ const userContextKey contextKey = 0 // __local_user_context__ // //go:generate ifacemaker --file ctx.go --struct DefaultCtx --iface Ctx --pkg fiber --output ctx_interface_gen.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on." type DefaultCtx struct { - app *App // Reference to *App - route *Route // Reference to *Route - fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx - bind *Bind // Default bind reference - redirect *Redirect // Default redirect reference - req *DefaultReq // Default request api reference - res *DefaultRes // Default response api reference - values [maxParams]string // Route parameter values - viewBindMap sync.Map // Default view map to bind template engine - method string // HTTP method - baseURI string // HTTP base uri - path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer - detectionPath string // Route detection path -> string copy from detectionPathBuffer - treePath string // Path for the search in the tree - pathOriginal string // Original HTTP path - pathBuffer []byte // HTTP path buffer - detectionPathBuffer []byte // HTTP detectionPath buffer - flashMessages redirectionMsgs // Flash messages - indexRoute int // Index of the current route - indexHandler int // Index of the current handler - methodINT int // HTTP method INT equivalent - matched bool // Non use route matched + app *App // Reference to *App + route *Route // Reference to *Route + fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx + bind *Bind // Default bind reference + redirect *Redirect // Default redirect reference + req *DefaultReq // Default request api reference + res *DefaultRes // Default response api reference + values [maxParams]string // Route parameter values + viewBindMap sync.Map // Default view map to bind template engine + method string // HTTP method + baseURI string // HTTP base uri + pathOriginal string // Original HTTP path + flashMessages redirectionMsgs // Flash messages + path []byte // HTTP path with the modifications by the configuration + detectionPath []byte // Route detection path + treePathHash int // Hash of the path for the search in the tree + indexRoute int // Index of the current route + indexHandler int // Index of the current handler + methodINT int // HTTP method INT equivalent + matched bool // Non use route matched } // SendFile defines configuration options when to transfer file with SendFile. @@ -1123,8 +1124,9 @@ func Params[V GenericType](c Ctx, key string, defaultValue ...V) V { // Path returns the path part of the request URL. // Optionally, you could override the path. +// Make copies or use the Immutable setting to use the value outside the Handler. func (c *DefaultCtx) Path(override ...string) string { - if len(override) != 0 && c.path != override[0] { + if len(override) != 0 && string(c.path) != override[0] { // Set new path to context c.pathOriginal = override[0] @@ -1133,7 +1135,7 @@ func (c *DefaultCtx) Path(override ...string) string { // Prettify path c.configDependentPaths() } - return c.path + return c.app.getString(c.path) } // Scheme contains the request protocol string: http or https for TLS requests. @@ -1832,32 +1834,31 @@ func (c *DefaultCtx) XHR() bool { // configDependentPaths set paths for route recognition and prepared paths for the user, // here the features for caseSensitive, decoded paths, strict paths are evaluated func (c *DefaultCtx) configDependentPaths() { - c.pathBuffer = append(c.pathBuffer[0:0], c.pathOriginal...) + c.path = append(c.path[:0], c.pathOriginal...) // If UnescapePath enabled, we decode the path and save it for the framework user if c.app.config.UnescapePath { - c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer) + c.path = fasthttp.AppendUnquotedArg(c.path[:0], c.path) } - c.path = c.app.getString(c.pathBuffer) // another path is specified which is for routing recognition only // use the path that was changed by the previous configuration flags - c.detectionPathBuffer = append(c.detectionPathBuffer[0:0], c.pathBuffer...) + c.detectionPath = append(c.detectionPath[:0], c.path...) // If CaseSensitive is disabled, we lowercase the original path if !c.app.config.CaseSensitive { - c.detectionPathBuffer = utils.ToLowerBytes(c.detectionPathBuffer) + c.detectionPath = utils.ToLowerBytes(c.detectionPath) } // If StrictRouting is disabled, we strip all trailing slashes - if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' { - c.detectionPathBuffer = utils.TrimRight(c.detectionPathBuffer, '/') + if !c.app.config.StrictRouting && len(c.detectionPath) > 1 && c.detectionPath[len(c.detectionPath)-1] == '/' { + c.detectionPath = utils.TrimRight(c.detectionPath, '/') } - c.detectionPath = c.app.getString(c.detectionPathBuffer) // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, // since the first three characters area select a list of routes - c.treePath = c.treePath[0:0] - const maxDetectionPaths = 3 + c.treePathHash = 0 if len(c.detectionPath) >= maxDetectionPaths { - c.treePath = c.detectionPath[:maxDetectionPaths] + c.treePathHash = int(c.detectionPath[0])<<16 | + int(c.detectionPath[1])<<8 | + int(c.detectionPath[2]) } } @@ -1958,12 +1959,12 @@ func (c *DefaultCtx) getIndexRoute() int { return c.indexRoute } -func (c *DefaultCtx) getTreePath() string { - return c.treePath +func (c *DefaultCtx) getTreePathHash() int { + return c.treePathHash } func (c *DefaultCtx) getDetectionPath() string { - return c.detectionPath + return c.app.getString(c.detectionPath) } func (c *DefaultCtx) getPathOriginal() string { diff --git a/ctx_interface.go b/ctx_interface.go index 32e8ee39..6ef33847 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -19,7 +19,7 @@ type CustomCtx interface { // Methods to use with next stack. getMethodINT() int getIndexRoute() int - getTreePath() string + getTreePathHash() int getDetectionPath() string getPathOriginal() string getValues() *[maxParams]string diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index fffe218d..a4d7db3d 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -347,7 +347,7 @@ type Ctx interface { // Methods to use with next stack. getMethodINT() int getIndexRoute() int - getTreePath() string + getTreePathHash() int getDetectionPath() string getPathOriginal() string getValues() *[maxParams]string diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 7226347f..fd007a62 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -8,8 +8,7 @@ sidebar_position: 5 Fiber provides the [Bind](../api/bind.md#validation) function to validate and bind [request data](../api/bind.md#binders) to a struct. -```go title="Example" - +```go title="Basic Example" import "github.com/go-playground/validator/v10" type structValidator struct { @@ -42,3 +41,71 @@ app.Post("/", func(c fiber.Ctx) error { return c.JSON(user) }) ``` + +```go title="Advanced Validation Example" +type User struct { + Name string `json:"name" validate:"required,min=3,max=32"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"gte=0,lte=100"` + Password string `json:"password" validate:"required,min=8"` + Website string `json:"website" validate:"url"` +} + +// Custom validation error messages +type UserWithCustomMessages struct { + Name string `json:"name" validate:"required,min=3,max=32" message:"Name is required and must be between 3 and 32 characters"` + Email string `json:"email" validate:"required,email" message:"Valid email is required"` + Age int `json:"age" validate:"gte=0,lte=100" message:"Age must be between 0 and 100"` +} + +app.Post("/user", func(c fiber.Ctx) error { + user := new(User) + + if err := c.Bind().Body(user); err != nil { + // Handle validation errors + if validationErrors, ok := err.(validator.ValidationErrors); ok { + for _, e := range validationErrors { + // e.Field() - field name + // e.Tag() - validation tag + // e.Value() - invalid value + // e.Param() - validation parameter + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "field": e.Field(), + "error": e.Error(), + }) + } + } + return err + } + + return c.JSON(user) +}) +``` + +```go title="Custom Validator Example" +// Custom validator for password strength +type PasswordValidator struct { + validate *validator.Validate +} + +func (v *PasswordValidator) Validate(out any) error { + if err := v.validate.Struct(out); err != nil { + return err + } + + // Custom password validation logic + if user, ok := out.(*User); ok { + if len(user.Password) < 8 { + return errors.New("password must be at least 8 characters") + } + // Add more password validation rules here + } + + return nil +} + +// Usage +app := fiber.New(fiber.Config{ + StructValidator: &PasswordValidator{validate: validator.New()}, +}) +``` diff --git a/helpers.go b/helpers.go index 584553a7..3f1685b1 100644 --- a/helpers.go +++ b/helpers.go @@ -113,9 +113,9 @@ func (app *App) methodExist(c *DefaultCtx) bool { // Reset stack index c.setIndexRoute(-1) - tree, ok := c.App().treeStack[i][c.getTreePath()] + tree, ok := c.App().treeStack[i][c.treePathHash] if !ok { - tree = c.App().treeStack[i][""] + tree = c.App().treeStack[i][0] } // Get stack length lenr := len(tree) - 1 @@ -157,9 +157,9 @@ func (app *App) methodExistCustom(c CustomCtx) bool { // Reset stack index c.setIndexRoute(-1) - tree, ok := c.App().treeStack[i][c.getTreePath()] + tree, ok := c.App().treeStack[i][c.getTreePathHash()] if !ok { - tree = c.App().treeStack[i][""] + tree = c.App().treeStack[i][0] } // Get stack length lenr := len(tree) - 1 diff --git a/router.go b/router.go index c247480e..4f3a391a 100644 --- a/router.go +++ b/router.go @@ -110,9 +110,9 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing // Get stack length - tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()] + tree, ok := app.treeStack[c.getMethodINT()][c.getTreePathHash()] if !ok { - tree = app.treeStack[c.getMethodINT()][""] + tree = app.treeStack[c.getMethodINT()][0] } lenr := len(tree) - 1 @@ -158,9 +158,9 @@ func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool func (app *App) next(c *DefaultCtx) (bool, error) { // Get stack length - tree, ok := app.treeStack[c.methodINT][c.treePath] + tree, ok := app.treeStack[c.methodINT][c.treePathHash] if !ok { - tree = app.treeStack[c.methodINT][""] + tree = app.treeStack[c.methodINT][0] } lenTree := len(tree) - 1 @@ -180,7 +180,7 @@ func (app *App) next(c *DefaultCtx) (bool, error) { } // Check if it matches the request path - match = route.match(c.detectionPath, c.path, &c.values) + match = route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) if !match { // No match, next route continue @@ -552,30 +552,28 @@ func (app *App) buildTree() *App { // loop all the methods and stacks and create the prefix tree for m := range app.config.RequestMethods { - tsMap := make(map[string][]*Route) + tsMap := make(map[int][]*Route) for _, route := range app.stack[m] { - treePath := "" - if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { - treePath = route.routeParser.segs[0].Const[:3] + treePathHash := 0 + if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= maxDetectionPaths { + treePathHash = int(route.routeParser.segs[0].Const[0])<<16 | + int(route.routeParser.segs[0].Const[1])<<8 | + int(route.routeParser.segs[0].Const[2]) } // create tree stack - tsMap[treePath] = append(tsMap[treePath], route) + tsMap[treePathHash] = append(tsMap[treePathHash], route) } - app.treeStack[m] = tsMap - } - // loop the methods and tree stacks and add global stack and sort everything - for m := range app.config.RequestMethods { - tsMap := app.treeStack[m] for treePart := range tsMap { - if treePart != "" { + if treePart != 0 { // merge global tree routes in current tree stack - tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...)) + tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[0]...)) } // sort tree slices with the positions slc := tsMap[treePart] sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) } + app.treeStack[m] = tsMap } app.routesRefreshed = false