Rewrite for fasthttp
This commit is contained in:
parent
d2e380e8a6
commit
21fe890260
17
LICENSE
Normal file
17
LICENSE
Normal file
@ -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.
|
126
README.md
126
README.md
@ -1,3 +1,127 @@
|
||||
# FastRouter
|
||||
|
||||
A FastHTTP-compatible version of Go/Router!
|
||||
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!
|
||||
|
14
go.mod
Normal file
14
go.mod
Normal file
@ -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
|
||||
)
|
12
go.sum
Normal file
12
go.sum
Normal file
@ -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=
|
489
router.go
Normal file
489
router.go
Normal file
@ -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
|
||||
}
|
834
router_test.go
Normal file
834
router_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user