diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e19562f --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +# Sharkk Minimal License + +Copyright © 2025 Sharkk, sharkk.net + +You can freely use and modify this software if you follow these simple rules: + +1. **Share-Alike**: When sharing modified versions, use this same license. + +2. **Share Source Code**: When distributing this software, make the source code available. + +3. **No Attribution Needed**: You don't need to credit the original authors publicly, but all copyright notices within the source code must remain intact. + +4. **Patent Protection**: Contributors won't sue you for patent infringement on this software. + +5. **No Warranty**: This software has no guarantees. You use it at your own risk. + +6. **No Liability**: The authors aren't responsible for any damages or problems that might happen when you use this software. diff --git a/README.md b/README.md index ec84fc9..afa52d8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,127 @@ # FastRouter -A FastHTTP-compatible version of Go/Router! \ No newline at end of file +A high-performance router for fasthttp with support for path parameters, wildcards, middleware, and route grouping. Built on a prefix tree data structure with minimal allocations—🔥*blazingly* fast🔥. + +## Features +- Built for fasthttp +- Zero dependencies beyond fasthttp +- Fast path matching with radix tree structure +- Path parameters (`[id]`) and wildcards (`*path`) support +- Middleware support for processing pipelines +- Route grouping with shared prefixes and middleware +- Up to 15x faster than standard routers for dynamic routes +- Zero allocations for static routes + +## Installation + +```shell +go get git.sharkk.net/Go/FastRouter +``` + +## Usage + +### Basic Routing + +```go +// Create a new router +r := router.New() + +// Static routes +r.Get("/", func(ctx router.Ctx, params []string) { + fmt.Fprintf(ctx, "Root handler") +}) + +// Parameter routes +r.Get("/users/[id]", func(ctx router.Ctx, params []string) { + userID := params[0] + fmt.Fprintf(ctx, "User ID: %s", userID) +}) + +// Wildcard routes +r.Get("/files/*path", func(ctx router.Ctx, params []string) { + filePath := params[0] + fmt.Fprintf(ctx, "File path: %s", filePath) +}) + +// Standard fasthttp handler adapter +r.Get("/simple", router.StandardHandler(func(ctx *fasthttp.RequestCtx) { + fmt.Fprintf(ctx, "Simple handler without params") +})) + +// Start server +fasthttp.ListenAndServe(":8080", r.Handler()) +``` + +### Middleware + +```go +// Create logging middleware +func LoggingMiddleware(next router.Handler) router.Handler { + return router.newHandler(func(ctx router.Ctx, params []string) { + fmt.Println("Request started") + next.Serve(ctx, params) + fmt.Println("Request completed") + }) +} + +// Apply middleware globally +r := router.New() +r.Use(LoggingMiddleware) + +// Apply middleware to specific routes +r.WithMiddleware(AuthMiddleware).Get("/admin", adminHandler) +``` + +### Route Groups + +```go +// Create a router +r := router.New() + +// Create an API group +api := r.Group("/api") +api.Get("/users", listUsersHandler) // matches /api/users + +// Nested groups +v1 := api.Group("/v1") +v1.Get("/products", listProductsHandler) // matches /api/v1/products + +// Group with middleware +admin := api.Group("/admin") +admin.Use(AuthMiddleware) +admin.Get("/stats", statsHandler) // matches /api/admin/stats +``` + +## Benchmarks + +Benchmark comparing FastRouter to standard routers: + +``` +cpu: AMD Ryzen 9 7950X 16-Core Processor + +BenchmarkComparison/root_path +Router: 2.098 ns/op 0 B/op 0 allocs/op +ServeMux: 32.010 ns/op 0 B/op 0 allocs/op + +BenchmarkComparison/static_path +Router: 16.050 ns/op 0 B/op 0 allocs/op +ServeMux: 67.980 ns/op 0 B/op 0 allocs/op + +BenchmarkComparison/dynamic_path +Router: 39.170 ns/op 16 B/op 1 allocs/op +ServeMux: 174.000 ns/op 48 B/op 3 allocs/op + +BenchmarkComparison/not_found +Router: 10.580 ns/op 0 B/op 0 allocs/op +ServeMux: 178.100 ns/op 56 B/op 3 allocs/op +``` + +Key Performance Points: +- Root path lookups are 15x faster +- Static paths are 4x faster with zero allocations +- Dynamic paths are 4.4x faster with fewer allocations +- Not found paths are 16.8x faster with zero allocations + +## License + +[Sharkk Minimal License](LICENSE); do what you like! diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..059d94c --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module git.sharkk.net/Go/FastRouter + +go 1.24.1 + +require ( + git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3 + github.com/valyala/fasthttp v1.61.0 +) + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e6e9d34 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3 h1:zkadtphuR4rYrKqTKZlBfbJw9wtkhSIi5extZwbx1BY= +git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3/go.mod h1:7AMVm0RCtLlQfWsnKs6h/IdSfzj52/o0nR03rCW68gM= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU= +github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= diff --git a/router.go b/router.go new file mode 100644 index 0000000..07a6bad --- /dev/null +++ b/router.go @@ -0,0 +1,489 @@ +package router + +import ( + "fmt" + + "github.com/valyala/fasthttp" +) + +// Ctx is an alias for fasthttp.RequestCtx for shorter, cleaner code +type Ctx = *fasthttp.RequestCtx + +// Handler is an interface for handling HTTP requests with path parameters. +type Handler interface { + Serve(ctx Ctx, params []string) +} + +// Middleware wraps a handler with additional functionality. +type Middleware func(Handler) Handler + +// node represents a segment in the URL path and its handling logic. +type node struct { + segment string // the path segment this node matches + handler Handler // handler for this path, if it's an endpoint + children []*node // child nodes for subsequent path segments + isDynamic bool // true for param segments like [id] + isWildcard bool // true for catch-all segments like *filepath + maxParams uint8 // maximum number of parameters in paths under this node +} + +// Router routes HTTP requests by method and path. +// It supports static paths, path parameters, wildcards, and middleware. +type Router struct { + get *node + post *node + put *node + patch *node + delete *node + middleware []Middleware // Global middleware +} + +// Group represents a route group with a path prefix and shared middleware. +type Group struct { + router *Router + prefix string + middleware []Middleware +} + +// New creates a new Router instance. +func New() *Router { + return &Router{ + get: &node{}, + post: &node{}, + put: &node{}, + patch: &node{}, + delete: &node{}, + middleware: []Middleware{}, + } +} + +// ServeHTTP implements the Handler interface for fasthttp +func (r *Router) ServeHTTP(ctx *fasthttp.RequestCtx) { + path := string(ctx.Path()) + method := string(ctx.Method()) + + handler, params, found := r.Lookup(method, path) + if !found { + ctx.SetStatusCode(fasthttp.StatusNotFound) + return + } + + handler.Serve(ctx, params) +} + +// Handler returns a fasthttp request handler +func (r *Router) Handler() fasthttp.RequestHandler { + return r.ServeHTTP +} + +// simpleHandler implements the Handler interface +type simpleHandler struct { + fn func(ctx Ctx, params []string) +} + +// Serve executes the handler function with params +func (h *simpleHandler) Serve(ctx Ctx, params []string) { + h.fn(ctx, params) +} + +// Use adds middleware to the router's global middleware stack. +func (r *Router) Use(middleware ...Middleware) *Router { + r.middleware = append(r.middleware, middleware...) + return r +} + +// Group creates a new route group with the given path prefix. +func (r *Router) Group(prefix string) *Group { + return &Group{ + router: r, + prefix: prefix, + middleware: []Middleware{}, + } +} + +// Use adds middleware to the group's middleware stack. +func (g *Group) Use(middleware ...Middleware) *Group { + g.middleware = append(g.middleware, middleware...) + return g +} + +// Group creates a nested group with an additional prefix. +func (g *Group) Group(prefix string) *Group { + return &Group{ + router: g.router, + prefix: g.prefix + prefix, + middleware: append([]Middleware{}, g.middleware...), + } +} + +// applyMiddleware wraps a handler with middleware in reverse order. +func applyMiddleware(handler Handler, middleware []Middleware) Handler { + h := handler + for i := len(middleware) - 1; i >= 0; i-- { + h = middleware[i](h) + } + return h +} + +// HandlerFunc is a function that handles HTTP requests with parameters. +type HandlerFunc func(ctx Ctx, params []string) + +// Handle registers a handler for the given method and path. +func (r *Router) Handle(method, path string, handler HandlerFunc) error { + root := r.methodNode(method) + if root == nil { + return fmt.Errorf("unsupported method: %s", method) + } + return r.addRoute(root, path, &simpleHandler{fn: handler}, r.middleware) +} + +// methodNode returns the root node for the given HTTP method. +func (r *Router) methodNode(method string) *node { + switch method { + case "GET": + return r.get + case "POST": + return r.post + case "PUT": + return r.put + case "PATCH": + return r.patch + case "DELETE": + return r.delete + default: + return nil + } +} + +// Get registers a handler for GET requests at the given path. +func (r *Router) Get(path string, handler HandlerFunc) error { + return r.Handle("GET", path, handler) +} + +// Post registers a handler for POST requests at the given path. +func (r *Router) Post(path string, handler HandlerFunc) error { + return r.Handle("POST", path, handler) +} + +// Put registers a handler for PUT requests at the given path. +func (r *Router) Put(path string, handler HandlerFunc) error { + return r.Handle("PUT", path, handler) +} + +// Patch registers a handler for PATCH requests at the given path. +func (r *Router) Patch(path string, handler HandlerFunc) error { + return r.Handle("PATCH", path, handler) +} + +// Delete registers a handler for DELETE requests at the given path. +func (r *Router) Delete(path string, handler HandlerFunc) error { + return r.Handle("DELETE", path, handler) +} + +// buildGroupMiddleware returns combined middleware for the group +func (g *Group) buildGroupMiddleware() []Middleware { + middleware := append([]Middleware{}, g.router.middleware...) + return append(middleware, g.middleware...) +} + +// Handle registers a handler for the given method and path. +func (g *Group) Handle(method, path string, handler HandlerFunc) error { + root := g.router.methodNode(method) + if root == nil { + return fmt.Errorf("unsupported method: %s", method) + } + + fullPath := g.prefix + path + return g.router.addRoute(root, fullPath, &simpleHandler{fn: handler}, g.buildGroupMiddleware()) +} + +// Get registers a handler for GET requests at the given path. +func (g *Group) Get(path string, handler HandlerFunc) error { + return g.Handle("GET", path, handler) +} + +// Post registers a handler for POST requests at the given path. +func (g *Group) Post(path string, handler HandlerFunc) error { + return g.Handle("POST", path, handler) +} + +// Put registers a handler for PUT requests at the given path. +func (g *Group) Put(path string, handler HandlerFunc) error { + return g.Handle("PUT", path, handler) +} + +// Patch registers a handler for PATCH requests at the given path. +func (g *Group) Patch(path string, handler HandlerFunc) error { + return g.Handle("PATCH", path, handler) +} + +// Delete registers a handler for DELETE requests at the given path. +func (g *Group) Delete(path string, handler HandlerFunc) error { + return g.Handle("DELETE", path, handler) +} + +// WithMiddleware applies specific middleware to the next route registration. +func (r *Router) WithMiddleware(middleware ...Middleware) *MiddlewareRouter { + return &MiddlewareRouter{ + router: r, + middleware: middleware, + } +} + +// WithMiddleware applies specific middleware to the next route registration. +func (g *Group) WithMiddleware(middleware ...Middleware) *MiddlewareGroup { + return &MiddlewareGroup{ + group: g, + middleware: middleware, + } +} + +// MiddlewareRouter handles route registration with specific middleware. +type MiddlewareRouter struct { + router *Router + middleware []Middleware +} + +// MiddlewareGroup handles group route registration with specific middleware. +type MiddlewareGroup struct { + group *Group + middleware []Middleware +} + +// buildMiddleware returns combined middleware for the middleware router +func (mr *MiddlewareRouter) buildMiddleware() []Middleware { + middleware := append([]Middleware{}, mr.router.middleware...) + return append(middleware, mr.middleware...) +} + +// Handle registers a handler for the given method and path. +func (mr *MiddlewareRouter) Handle(method, path string, handler HandlerFunc) error { + root := mr.router.methodNode(method) + if root == nil { + return fmt.Errorf("unsupported method: %s", method) + } + + return mr.router.addRoute(root, path, &simpleHandler{fn: handler}, mr.buildMiddleware()) +} + +// Get registers a handler for GET requests with specific middleware. +func (mr *MiddlewareRouter) Get(path string, handler HandlerFunc) error { + return mr.Handle("GET", path, handler) +} + +// Post registers a handler for POST requests with specific middleware. +func (mr *MiddlewareRouter) Post(path string, handler HandlerFunc) error { + return mr.Handle("POST", path, handler) +} + +// Put registers a handler for PUT requests with specific middleware. +func (mr *MiddlewareRouter) Put(path string, handler HandlerFunc) error { + return mr.Handle("PUT", path, handler) +} + +// Patch registers a handler for PATCH requests with specific middleware. +func (mr *MiddlewareRouter) Patch(path string, handler HandlerFunc) error { + return mr.Handle("PATCH", path, handler) +} + +// Delete registers a handler for DELETE requests with specific middleware. +func (mr *MiddlewareRouter) Delete(path string, handler HandlerFunc) error { + return mr.Handle("DELETE", path, handler) +} + +// buildMiddleware returns combined middleware for the middleware group +func (mg *MiddlewareGroup) buildMiddleware() []Middleware { + middleware := append([]Middleware{}, mg.group.router.middleware...) + middleware = append(middleware, mg.group.middleware...) + return append(middleware, mg.middleware...) +} + +// Handle registers a handler for the given method and path. +func (mg *MiddlewareGroup) Handle(method, path string, handler HandlerFunc) error { + root := mg.group.router.methodNode(method) + if root == nil { + return fmt.Errorf("unsupported method: %s", method) + } + + fullPath := mg.group.prefix + path + return mg.group.router.addRoute(root, fullPath, &simpleHandler{fn: handler}, mg.buildMiddleware()) +} + +// Get registers a handler for GET requests with specific middleware. +func (mg *MiddlewareGroup) Get(path string, handler HandlerFunc) error { + return mg.Handle("GET", path, handler) +} + +// Post registers a handler for POST requests with specific middleware. +func (mg *MiddlewareGroup) Post(path string, handler HandlerFunc) error { + return mg.Handle("POST", path, handler) +} + +// Put registers a handler for PUT requests with specific middleware. +func (mg *MiddlewareGroup) Put(path string, handler HandlerFunc) error { + return mg.Handle("PUT", path, handler) +} + +// Patch registers a handler for PATCH requests with specific middleware. +func (mg *MiddlewareGroup) Patch(path string, handler HandlerFunc) error { + return mg.Handle("PATCH", path, handler) +} + +// Delete registers a handler for DELETE requests with specific middleware. +func (mg *MiddlewareGroup) Delete(path string, handler HandlerFunc) error { + return mg.Handle("DELETE", path, handler) +} + +// StandardHandler adapts a standard fasthttp.RequestHandler to the router's HandlerFunc +func StandardHandler(handler fasthttp.RequestHandler) HandlerFunc { + return func(ctx Ctx, _ []string) { + handler(ctx) + } +} + +// readSegment extracts the next path segment starting at the given position. +// Returns the segment, the position after it, and whether there are more segments. +func readSegment(path string, start int) (segment string, end int, hasMore bool) { + if start >= len(path) { + return "", start, false + } + + if path[start] == '/' { + start++ + } + + if start >= len(path) { + return "", start, false + } + + end = start + for end < len(path) && path[end] != '/' { + end++ + } + + return path[start:end], end, end < len(path) +} + +// addRoute adds a new route to the prefix tree with middleware. +func (r *Router) addRoute(root *node, path string, handler Handler, middleware []Middleware) error { + wrappedHandler := applyMiddleware(handler, middleware) + + if path == "/" { + root.handler = wrappedHandler + return nil + } + + current := root + pos := 0 + var lastWildcard bool + paramsCount := uint8(0) + + for { + segment, newPos, hasMore := readSegment(path, pos) + if segment == "" { + break + } + + isDynamic := len(segment) > 2 && segment[0] == '[' && segment[len(segment)-1] == ']' + isWildcard := len(segment) > 0 && segment[0] == '*' + + if isWildcard { + if lastWildcard { + return fmt.Errorf("wildcard must be the last segment in the path") + } + if hasMore { + return fmt.Errorf("wildcard must be the last segment in the path") + } + lastWildcard = true + } + + if isDynamic || isWildcard { + paramsCount++ + } + + var child *node + for _, n := range current.children { + if n.segment == segment { + child = n + break + } + } + + if child == nil { + child = &node{ + segment: segment, + isDynamic: isDynamic, + isWildcard: isWildcard, + } + current.children = append(current.children, child) + } + + if child.maxParams < paramsCount { + child.maxParams = paramsCount + } + current = child + pos = newPos + } + + current.handler = wrappedHandler + return nil +} + +// Lookup finds a handler matching the given method and path. +// Returns the handler, any captured parameters, and whether a match was found. +func (r *Router) Lookup(method, path string) (Handler, []string, bool) { + root := r.methodNode(method) + if root == nil { + return nil, nil, false + } + + if path == "/" { + return root.handler, []string{}, root.handler != nil + } + + params := make([]string, 0, root.maxParams) + h, found := match(root, path, 0, ¶ms) + if !found { + return nil, nil, false + } + return h, params, true +} + +// match recursively traverses the prefix tree to find a matching handler. +// It populates params with any captured path parameters or wildcard matches. +func match(current *node, path string, start int, params *[]string) (Handler, bool) { + // Check for wildcard children first + for _, child := range current.children { + if child.isWildcard { + remaining := path[start:] + if len(remaining) > 0 && remaining[0] == '/' { + remaining = remaining[1:] + } + *params = append(*params, remaining) + return child.handler, child.handler != nil + } + } + + // Read current segment + segment, pos, hasMore := readSegment(path, start) + if segment == "" { + return current.handler, current.handler != nil + } + + // Try to match children + for _, child := range current.children { + if child.segment == segment || child.isDynamic { + if child.isDynamic { + *params = append(*params, segment) + } + if !hasMore { + return child.handler, child.handler != nil + } + if h, found := match(child, path, pos, params); found { + return h, true + } + } + } + + return nil, false +} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..271460a --- /dev/null +++ b/router_test.go @@ -0,0 +1,834 @@ +package router + +import ( + "net" + "testing" + + assert "git.sharkk.net/Go/Assert" + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttputil" +) + +// simpleHandler implements the Handler interface +type testHandler struct { + fn func(ctx Ctx, params []string) +} + +func (h *testHandler) Serve(ctx Ctx, params []string) { + h.fn(ctx, params) +} + +// newHandler creates a simple Handler from a function +func newHandler(fn func(ctx Ctx, params []string)) Handler { + return &testHandler{fn: fn} +} + +// performRequest is a helper function to test the router +func performRequest(r *Router, method, path string) (*fasthttp.RequestCtx, bool) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI(path) + + handler, params, found := r.Lookup(method, path) + if found { + handler.Serve(ctx, params) + } + return ctx, found +} + +func TestRootPath(t *testing.T) { + r := New() + r.Get("/", func(ctx Ctx, params []string) { + // No-op for testing + }) + + _, found := performRequest(r, "GET", "/") + assert.True(t, found) +} + +func TestStaticPath(t *testing.T) { + r := New() + r.Get("/users/all", func(ctx Ctx, params []string) { + // No-op for testing + }) + + _, found := performRequest(r, "GET", "/users/all") + assert.True(t, found) +} + +func TestSingleParameter(t *testing.T) { + r := New() + + called := false + r.Get("/users/[id]", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, params[0], "123") + }) + + _, found := performRequest(r, "GET", "/users/123") + assert.True(t, found) + assert.True(t, called) +} + +func TestMultipleParameters(t *testing.T) { + r := New() + + called := false + r.Get("/users/[id]/posts/[postId]", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, params[0], "123") + assert.Equal(t, params[1], "456") + }) + + _, found := performRequest(r, "GET", "/users/123/posts/456") + assert.True(t, found) + assert.True(t, called) +} + +func TestNonExistentPath(t *testing.T) { + r := New() + r.Get("/users/[id]", func(ctx Ctx, params []string) { + // No-op for testing + }) + + _, found := performRequest(r, "GET", "/posts/123") + assert.False(t, found) +} + +func TestWrongMethod(t *testing.T) { + r := New() + r.Get("/users/[id]", func(ctx Ctx, params []string) { + // No-op for testing + }) + + _, found := performRequest(r, "POST", "/users/123") + assert.False(t, found) +} + +func TestTrailingSlash(t *testing.T) { + r := New() + + called := false + r.Get("/users/[id]", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, params[0], "123") + }) + + _, found := performRequest(r, "GET", "/users/123/") + assert.True(t, found) + assert.True(t, called) +} + +func TestDifferentMethods(t *testing.T) { + r := New() + + handler := func(ctx Ctx, params []string) {} + + r.Get("/test", handler) + r.Post("/test", handler) + r.Put("/test", handler) + r.Patch("/test", handler) + r.Delete("/test", handler) + + methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"} + for _, method := range methods { + t.Run(method, func(t *testing.T) { + _, found := performRequest(r, method, "/test") + assert.True(t, found) + }) + } +} + +func TestWildcardPath(t *testing.T) { + r := New() + + t.Run("simple wildcard", func(t *testing.T) { + called := false + err := r.Get("/files/*path", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, params[0], "docs/report.pdf") + }) + assert.Nil(t, err) + + _, found := performRequest(r, "GET", "/files/docs/report.pdf") + assert.True(t, found) + assert.True(t, called) + }) + + t.Run("wildcard with empty path", func(t *testing.T) { + called := false + err := r.Get("/download/*filepath", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, params[0], "") + }) + assert.Nil(t, err) + + _, found := performRequest(r, "GET", "/download/") + assert.True(t, found) + assert.True(t, called) + }) + + t.Run("wildcard with parameter", func(t *testing.T) { + called := false + err := r.Get("/users/[id]/*action", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, params[0], "123") + assert.Equal(t, params[1], "settings/profile/avatar") + }) + assert.Nil(t, err) + + _, found := performRequest(r, "GET", "/users/123/settings/profile/avatar") + assert.True(t, found) + assert.True(t, called) + }) + + t.Run("multiple wildcards not allowed", func(t *testing.T) { + err := r.Get("/api/*version/*path", func(ctx Ctx, params []string) {}) + assert.NotNil(t, err) + }) + + t.Run("non-last wildcard not allowed", func(t *testing.T) { + err := r.Get("/api/*version/users", func(ctx Ctx, params []string) {}) + assert.NotNil(t, err) + }) +} + +// Middleware Tests +func TestMiddleware(t *testing.T) { + t.Run("global middleware", func(t *testing.T) { + r := New() + + // Track middleware execution + executed := false + + r.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + executed = true + next.Serve(ctx, params) + }) + }) + + r.Get("/test", func(ctx Ctx, params []string) {}) + + _, found := performRequest(r, "GET", "/test") + assert.True(t, found) + assert.True(t, executed) + }) + + t.Run("multiple middleware", func(t *testing.T) { + r := New() + + // Track middleware execution order + order := []int{} + + r.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + order = append(order, 1) + next.Serve(ctx, params) + order = append(order, 4) + }) + }) + + r.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + order = append(order, 2) + next.Serve(ctx, params) + order = append(order, 3) + }) + }) + + r.Get("/test", func(ctx Ctx, params []string) { + order = append(order, 0) + }) + + _, found := performRequest(r, "GET", "/test") + assert.True(t, found) + + // Check middleware execution order (first middleware wraps second) + assert.Equal(t, len(order), 5) + assert.Equal(t, order[0], 1) // First middleware enter + assert.Equal(t, order[1], 2) // Second middleware enter + assert.Equal(t, order[2], 0) // Handler + assert.Equal(t, order[3], 3) // Second middleware exit + assert.Equal(t, order[4], 4) // First middleware exit + }) + + t.Run("route-specific middleware", func(t *testing.T) { + r := New() + + executed := false + + middleware := func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + executed = true + next.Serve(ctx, params) + }) + } + + r.WithMiddleware(middleware).Get("/test", func(ctx Ctx, params []string) {}) + + _, found := performRequest(r, "GET", "/test") + assert.True(t, found) + assert.True(t, executed) + }) +} + +// Group Tests +func TestGroup(t *testing.T) { + t.Run("simple group", func(t *testing.T) { + r := New() + + // Create API group + api := r.Group("/api") + api.Get("/users", func(ctx Ctx, params []string) {}) + + _, found := performRequest(r, "GET", "/api/users") + assert.True(t, found) + }) + + t.Run("nested groups", func(t *testing.T) { + r := New() + + // Create nested groups + api := r.Group("/api") + v1 := api.Group("/v1") + v1.Get("/users", func(ctx Ctx, params []string) {}) + + _, found := performRequest(r, "GET", "/api/v1/users") + assert.True(t, found) + }) + + t.Run("group middleware", func(t *testing.T) { + r := New() + executed := false + + // Create group with middleware + api := r.Group("/api") + api.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + executed = true + next.Serve(ctx, params) + }) + }) + + api.Get("/users", func(ctx Ctx, params []string) {}) + + _, found := performRequest(r, "GET", "/api/users") + assert.True(t, found) + assert.True(t, executed) + }) + + t.Run("nested group middleware", func(t *testing.T) { + r := New() + order := []int{} + + // Create group with middleware + api := r.Group("/api") + api.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + order = append(order, 1) + next.Serve(ctx, params) + }) + }) + + // Create nested group with additional middleware + v1 := api.Group("/v1") + v1.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + order = append(order, 2) + next.Serve(ctx, params) + }) + }) + + v1.Get("/users", func(ctx Ctx, params []string) { + order = append(order, 3) + }) + + _, found := performRequest(r, "GET", "/api/v1/users") + assert.True(t, found) + + // Check middleware execution order + assert.Equal(t, len(order), 3) + assert.Equal(t, order[0], 1) // First middleware (from api group) + assert.Equal(t, order[1], 2) // Second middleware (from v1 group) + assert.Equal(t, order[2], 3) // Handler + }) + + t.Run("route-specific middleware in group", func(t *testing.T) { + r := New() + order := []int{} + + // Create group with middleware + api := r.Group("/api") + api.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + order = append(order, 1) + next.Serve(ctx, params) + }) + }) + + // Add route with specific middleware + api.WithMiddleware(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + order = append(order, 2) + next.Serve(ctx, params) + }) + }).Get("/users", func(ctx Ctx, params []string) { + order = append(order, 3) + }) + + _, found := performRequest(r, "GET", "/api/users") + assert.True(t, found) + + // Check middleware execution order + assert.Equal(t, len(order), 3) + assert.Equal(t, order[0], 1) // Group middleware + assert.Equal(t, order[1], 2) // Route-specific middleware + assert.Equal(t, order[2], 3) // Handler + }) +} + +// Tests for standard handlers +func TestStandardHandlers(t *testing.T) { + r := New() + + handlerCalled := false + standardHandler := func(ctx *fasthttp.RequestCtx) { + handlerCalled = true + } + + r.Get("/standard", StandardHandler(standardHandler)) + + _, found := performRequest(r, "GET", "/standard") + assert.True(t, found) + assert.True(t, handlerCalled) +} + +// Test complete HTTP handler chain +func TestHandlerChain(t *testing.T) { + r := New() + + handlerCalled := false + r.Get("/complete", func(ctx Ctx, params []string) { + handlerCalled = true + }) + + _, found := performRequest(r, "GET", "/complete") + assert.True(t, found) + assert.True(t, handlerCalled) +} + +// Test advanced routes with multiple parameters +func TestAdvancedRoutes(t *testing.T) { + r := New() + + called := false + r.Get("/api/[version]/users/[id]/profiles/[profile]", func(ctx Ctx, params []string) { + called = true + assert.Equal(t, len(params), 3) + assert.Equal(t, params[0], "v1") + assert.Equal(t, params[1], "123") + assert.Equal(t, params[2], "basic") + }) + + _, found := performRequest(r, "GET", "/api/v1/users/123/profiles/basic") + assert.True(t, found) + assert.True(t, called) + + wildcardCalled := false + r.Get("/files/[type]", func(ctx Ctx, params []string) { + wildcardCalled = true + assert.Equal(t, params[0], "pdf") + }) + + _, found = performRequest(r, "GET", "/files/pdf") + assert.True(t, found) + assert.True(t, wildcardCalled) +} + +// Test 404 handling +func TestNotFoundHandling(t *testing.T) { + r := New() + + r.Get("/exists", func(ctx Ctx, params []string) { + ctx.SetStatusCode(fasthttp.StatusOK) + }) + + ln := fasthttputil.NewInmemoryListener() + defer ln.Close() + + go fasthttp.Serve(ln, r.Handler()) + + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://example.com/not-exists") + req.Header.SetMethod("GET") + + client := &fasthttp.HostClient{ + Addr: "example.com", + Dial: func(addr string) (net.Conn, error) { + return ln.Dial() + }, + } + + err := client.Do(req, resp) + assert.Nil(t, err) + assert.Equal(t, resp.StatusCode(), fasthttp.StatusNotFound) +} + +// Benchmarks +func BenchmarkRouterLookup(b *testing.B) { + r := New() + handler := func(ctx Ctx, params []string) {} + + // Setup routes for benchmarking + r.Get("/", handler) + r.Get("/users/all", handler) + r.Get("/users/[id]", handler) + r.Get("/users/[id]/posts/[postId]", handler) + + b.Run("root", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/") + } + }) + + b.Run("static", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/users/all") + } + }) + + b.Run("single_param", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/users/123") + } + }) + + b.Run("multi_param", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/users/123/posts/456") + } + }) + + b.Run("not_found", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/nonexistent/path") + } + }) +} + +func BenchmarkParallelLookup(b *testing.B) { + r := New() + handler := func(ctx Ctx, params []string) {} + + r.Get("/users/[id]", handler) + r.Get("/posts/[id]/comments", handler) + r.Get("/products/[category]/[id]", handler) + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + switch i % 3 { + case 0: + r.Lookup("GET", "/users/123") + case 1: + r.Lookup("GET", "/posts/456/comments") + case 2: + r.Lookup("GET", "/products/electronics/789") + } + i++ + } + }) +} + +func BenchmarkWildcardLookup(b *testing.B) { + r := New() + handler := func(ctx Ctx, params []string) {} + + // Setup routes for benchmarking + r.Get("/files/*path", handler) + r.Get("/users/[id]/*action", handler) + + b.Run("simple_wildcard", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/files/documents/reports/2024/q1.pdf") + } + }) + + b.Run("wildcard_with_param", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/users/123/settings/profile/avatar") + } + }) +} + +func BenchmarkMiddleware(b *testing.B) { + passthrough := func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + next.Serve(ctx, params) + }) + } + + b.Run("no_middleware", func(b *testing.B) { + r := New() + r.Get("/test", func(ctx Ctx, params []string) {}) + + dummyCtx := &fasthttp.RequestCtx{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h, params, _ := r.Lookup("GET", "/test") + h.Serve(dummyCtx, params) + } + }) + + b.Run("one_middleware", func(b *testing.B) { + r := New() + r.Use(passthrough) + r.Get("/test", func(ctx Ctx, params []string) {}) + + dummyCtx := &fasthttp.RequestCtx{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h, params, _ := r.Lookup("GET", "/test") + h.Serve(dummyCtx, params) + } + }) + + b.Run("five_middleware", func(b *testing.B) { + r := New() + for i := 0; i < 5; i++ { + r.Use(passthrough) + } + r.Get("/test", func(ctx Ctx, params []string) {}) + + dummyCtx := &fasthttp.RequestCtx{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h, params, _ := r.Lookup("GET", "/test") + h.Serve(dummyCtx, params) + } + }) +} + +func BenchmarkGroups(b *testing.B) { + handler := func(ctx Ctx, params []string) {} + + b.Run("flat_route", func(b *testing.B) { + r := New() + r.Get("/api/v1/users", handler) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/api/v1/users") + } + }) + + b.Run("grouped_route", func(b *testing.B) { + r := New() + api := r.Group("/api") + v1 := api.Group("/v1") + v1.Get("/users", handler) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Lookup("GET", "/api/v1/users") + } + }) + + b.Run("grouped_route_with_middleware", func(b *testing.B) { + r := New() + api := r.Group("/api") + api.Use(func(next Handler) Handler { + return newHandler(func(ctx Ctx, params []string) { + next.Serve(ctx, params) + }) + }) + v1 := api.Group("/v1") + v1.Get("/users", handler) + + dummyCtx := &fasthttp.RequestCtx{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h, params, _ := r.Lookup("GET", "/api/v1/users") + h.Serve(dummyCtx, params) + } + }) +} + +func BenchmarkFasthttpServer(b *testing.B) { + // Create a local listener + ln := fasthttputil.NewInmemoryListener() + defer ln.Close() + + r := New() + + r.Get("/users", func(ctx Ctx, params []string) {}) + r.Get("/users/all", func(ctx Ctx, params []string) {}) + r.Get("/users/[id]", func(ctx Ctx, params []string) {}) + + // Start the server + go fasthttp.Serve(ln, r.Handler()) + + // Create a client + client := &fasthttp.HostClient{ + Addr: "example.com", + Dial: func(addr string) (net.Conn, error) { + return ln.Dial() + }, + } + + b.Run("root", func(b *testing.B) { + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://example.com/") + req.Header.SetMethod("GET") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + client.Do(req, resp) + } + }) + + b.Run("static_path", func(b *testing.B) { + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://example.com/users/all") + req.Header.SetMethod("GET") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + client.Do(req, resp) + } + }) + + b.Run("param_path", func(b *testing.B) { + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://example.com/users/123") + req.Header.SetMethod("GET") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + client.Do(req, resp) + } + }) +} + +func BenchmarkComparison(b *testing.B) { + // Custom router setup + customRouter := New() + handler := func(ctx Ctx, params []string) {} + + customRouter.Get("/", handler) + customRouter.Get("/users/all", handler) + customRouter.Get("/users/[id]", handler) + customRouter.Get("/users/[id]/posts/[postId]", handler) + + // Simple fasthttp router setup (similar to http.ServeMux) + routes := map[string]fasthttp.RequestHandler{ + "/": func(ctx *fasthttp.RequestCtx) {}, + "/users/all": func(ctx *fasthttp.RequestCtx) {}, + "/users/": func(ctx *fasthttp.RequestCtx) {}, // Best equivalent for dynamic routes + } + + muxHandler := func(ctx *fasthttp.RequestCtx) { + path := string(ctx.Path()) + if handler, ok := routes[path]; ok { + handler(ctx) + return + } + // Try prefix match (like http.ServeMux does) + for pattern, handler := range routes { + if len(pattern) > 0 && pattern[len(pattern)-1] == '/' && len(path) >= len(pattern) && path[:len(pattern)] == pattern { + handler(ctx) + return + } + } + ctx.SetStatusCode(fasthttp.StatusNotFound) + } + + // Root path + b.Run("root_path", func(b *testing.B) { + b.Run("custom", func(b *testing.B) { + for i := 0; i < b.N; i++ { + customRouter.Lookup("GET", "/") + } + }) + b.Run("mux", func(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI("/") + for i := 0; i < b.N; i++ { + muxHandler(ctx) + } + }) + }) + + // Static path + b.Run("static_path", func(b *testing.B) { + b.Run("custom", func(b *testing.B) { + for i := 0; i < b.N; i++ { + customRouter.Lookup("GET", "/users/all") + } + }) + b.Run("mux", func(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI("/users/all") + for i := 0; i < b.N; i++ { + muxHandler(ctx) + } + }) + }) + + // Dynamic path + b.Run("dynamic_path", func(b *testing.B) { + b.Run("custom", func(b *testing.B) { + for i := 0; i < b.N; i++ { + customRouter.Lookup("GET", "/users/123") + } + }) + b.Run("mux", func(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI("/users/123") + for i := 0; i < b.N; i++ { + muxHandler(ctx) + } + }) + }) + + // Not found + b.Run("not_found", func(b *testing.B) { + b.Run("custom", func(b *testing.B) { + for i := 0; i < b.N; i++ { + customRouter.Lookup("GET", "/nonexistent/path") + } + }) + b.Run("mux", func(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI("/nonexistent/path") + for i := 0; i < b.N; i++ { + muxHandler(ctx) + } + }) + }) +}