diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..6823c7a --- /dev/null +++ b/DOCS.md @@ -0,0 +1,155 @@ +# Router Package Documentation + +A fast, lightweight HTTP router for Go with support for middleware, route groups, and path parameters. + +## Core Types + +### Router + +Main router that implements `http.Handler`. + +```go +router := router.New() +``` + +### Handler + +Request handler function type. + +```go +type Handler func(w http.ResponseWriter, r *http.Request, params []string) +``` + +### Middleware + +Function type for middleware. + +```go +type Middleware func(Handler) Handler +``` + +### Group + +Route group with a prefix. + +```go +group := router.Group("/api") +``` + +## Router Methods + +### New() + +Creates a new router. + +```go +router := router.New() +``` + +### ServeHTTP(w, r) + +Implements `http.Handler` interface. + +### Use(mw ...Middleware) + +Adds global middleware. + +```go +router.Use(loggingMiddleware, authMiddleware) +``` + +### Handle(method, path, handler) + +Registers a handler for the given method and path. + +```go +router.Handle("GET", "/users", listUsersHandler) +``` + +### HTTP Method Shortcuts + +```go +router.Get("/users", listUsersHandler) +router.Post("/users", createUserHandler) +router.Put("/users/[id]", updateUserHandler) +router.Patch("/users/[id]", patchUserHandler) +router.Delete("/users/[id]", deleteUserHandler) +``` + +### Group(prefix) + +Creates a route group with prefix. + +```go +api := router.Group("/api") +``` + +### WithMiddleware(mw ...Middleware) + +Applies middleware to the next route registration. + +```go +router.WithMiddleware(authMiddleware).Get("/admin", adminHandler) +``` + +## Group Methods + +### Use(mw ...Middleware) + +Adds middleware to the group. + +```go +api.Use(apiKeyMiddleware) +``` + +### Group(prefix) + +Creates a nested group. + +```go +v1 := api.Group("/v1") +``` + +### HTTP Method Shortcuts + +```go +api.Get("/users", listUsersHandler) +api.Post("/users", createUserHandler) +api.Put("/users/[id]", updateUserHandler) +api.Patch("/users/[id]", patchUserHandler) +api.Delete("/users/[id]", deleteUserHandler) +``` + +### WithMiddleware(mw ...Middleware) + +Applies middleware to the next route registration in this group. + +```go +api.WithMiddleware(authMiddleware).Get("/admin", adminHandler) +``` + +## Path Parameters + +Dynamic segments in paths are defined using square brackets. + +```go +router.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + id := params[0] + // ... +}) +``` + +## Wildcards + +Wildcard segments capture all remaining path segments. + +```go +router.Get("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) { + path := params[0] + // ... +}) +``` + +Notes: +- Wildcards must be the last segment in a path +- Only one wildcard is allowed per path diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..26935c2 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,275 @@ +# Router Usage Examples + +## Basic Usage + +```go +package main + +import ( + "fmt" + "net/http" + "github.com/yourusername/router" +) + +func main() { + r := router.New() + + r.Get("/", func(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "Hello World!") + }) + + r.Get("/about", func(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "About page") + }) + + http.ListenAndServe(":8080", r) +} +``` + +## Path Parameters + +```go +r := router.New() + +// Single parameter +r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + id := params[0] + fmt.Fprintf(w, "User ID: %s", id) +}) + +// Multiple parameters +r.Get("/posts/[category]/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + category := params[0] + id := params[1] + fmt.Fprintf(w, "Category: %s, Post ID: %s", category, id) +}) + +// Wildcard +r.Get("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) { + path := params[0] + fmt.Fprintf(w, "File path: %s", path) +}) +``` + +## Middleware + +```go +// Logging middleware +func LoggingMiddleware(next router.Handler) router.Handler { + return func(w http.ResponseWriter, r *http.Request, params []string) { + fmt.Printf("[%s] %s\n", r.Method, r.URL.Path) + next(w, r, params) + } +} + +// Auth middleware +func AuthMiddleware(next router.Handler) router.Handler { + return func(w http.ResponseWriter, r *http.Request, params []string) { + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next(w, r, params) + } +} + +// Global middleware +r := router.New() +r.Use(LoggingMiddleware) + +// Route-specific middleware +r.WithMiddleware(AuthMiddleware).Get("/admin", adminHandler) +``` + +## Route Groups + +```go +r := router.New() + +// API group +api := r.Group("/api") +api.Get("/status", statusHandler) + +// Versioned API +v1 := api.Group("/v1") +v1.Get("/users", listUsersHandler) +v1.Post("/users", createUserHandler) + +v2 := api.Group("/v2") +v2.Get("/users", listUsersV2Handler) +``` + +## Combining Features + +```go +r := router.New() + +// Global middleware +r.Use(LoggingMiddleware) + +// API group with middleware +api := r.Group("/api") +api.Use(ApiKeyMiddleware) + +// Admin group with auth middleware +admin := r.Group("/admin") +admin.Use(AuthMiddleware) + +// Users endpoints with versioning +users := api.Group("/v1/users") +users.Get("/", listUsersHandler) +users.Post("/", createUserHandler) +users.Get("/[id]", getUserHandler) +users.Put("/[id]", updateUserHandler) +users.Delete("/[id]", deleteUserHandler) + +// Special case with route-specific middleware +api.WithMiddleware(CacheMiddleware).Get("/cached-resource", cachedResourceHandler) +``` + +## Error Handling + +```go +r := router.New() + +err := r.Get("/users/[id]", getUserHandler) +if err != nil { + // Handle error +} + +// Custom NotFound handler +r.ServeHTTP = func(w http.ResponseWriter, req *http.Request) { + h, params, ok := r.Lookup(req.Method, req.URL.Path) + if !ok { + // Custom 404 handler + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Custom 404: %s not found", req.URL.Path) + return + } + h(w, req, params) +} +``` + +## Complete Application Example + +```go +package main + +import ( + "fmt" + "log" + "net/http" + "github.com/yourusername/router" +) + +func main() { + r := router.New() + + // Global middleware + r.Use(LoggingMiddleware) + + // Basic routes + r.Get("/", homeHandler) + r.Get("/about", aboutHandler) + + // API routes + api := r.Group("/api") + api.Use(ApiKeyMiddleware) + + // Users API + users := api.Group("/users") + users.Get("/", listUsersHandler) + users.Post("/", createUserHandler) + users.Get("/[id]", getUserHandler) + users.Put("/[id]", updateUserHandler) + users.Delete("/[id]", deleteUserHandler) + + // Admin routes with auth + admin := r.Group("/admin") + admin.Use(AuthMiddleware) + admin.Get("/", adminDashboardHandler) + admin.Get("/users", adminUsersHandler) + + // Start server + log.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", r)) +} + +// Handlers +func homeHandler(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "Welcome to the home page") +} + +func aboutHandler(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "About us") +} + +func listUsersHandler(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "List of users") +} + +func getUserHandler(w http.ResponseWriter, r *http.Request, params []string) { + id := params[0] + fmt.Fprintf(w, "User details for ID: %s", id) +} + +func createUserHandler(w http.ResponseWriter, r *http.Request, _ []string) { + // Parse form data or JSON body + fmt.Fprintf(w, "User created") +} + +func updateUserHandler(w http.ResponseWriter, r *http.Request, params []string) { + id := params[0] + fmt.Fprintf(w, "User updated: %s", id) +} + +func deleteUserHandler(w http.ResponseWriter, r *http.Request, params []string) { + id := params[0] + fmt.Fprintf(w, "User deleted: %s", id) +} + +func adminDashboardHandler(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "Admin Dashboard") +} + +func adminUsersHandler(w http.ResponseWriter, r *http.Request, _ []string) { + fmt.Fprintf(w, "Admin Users Management") +} + +// Middleware +func LoggingMiddleware(next router.Handler) router.Handler { + return func(w http.ResponseWriter, r *http.Request, params []string) { + log.Printf("[%s] %s", r.Method, r.URL.Path) + next(w, r, params) + } +} + +func ApiKeyMiddleware(next router.Handler) router.Handler { + return func(w http.ResponseWriter, r *http.Request, params []string) { + apiKey := r.Header.Get("X-API-Key") + if apiKey == "" { + http.Error(w, "API key required", http.StatusUnauthorized) + return + } + next(w, r, params) + } +} + +func AuthMiddleware(next router.Handler) router.Handler { + return func(w http.ResponseWriter, r *http.Request, params []string) { + // Check session or JWT + authorized := checkUserAuth(r) + if !authorized { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + next(w, r, params) + } +} + +func checkUserAuth(r *http.Request) bool { + // Implementation of auth check + return r.Header.Get("Authorization") != "" +} +``` diff --git a/README.md b/README.md index ef33097..7a09123 100644 --- a/README.md +++ b/README.md @@ -108,29 +108,29 @@ http.ListenAndServe(":8080", r) Benchmark comparing Router to the standard `http.ServeMux`: ``` -cpu: AMD Ryzen 9 7950X 16-Core Processor +cpu: 13th Gen Intel(R) Core(TM) i7-1370P 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 +Router: 1.798 ns/op 0 B/op 0 allocs/op +ServeMux: 40.98 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 +Router: 18.41 ns/op 0 B/op 0 allocs/op +ServeMux: 86.04 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 +Router: 24.23 ns/op 0 B/op 0 allocs/op +ServeMux: 221.9 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 +Router: 10.76 ns/op 0 B/op 0 allocs/op +ServeMux: 210.2 ns/op 56 B/op 3 allocs/op ``` -- 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 +- Root path lookups are 22x faster +- Static paths are 4.7x faster with zero allocations +- Dynamic paths are 9x faster with zero allocations +- Not found paths are 19.5x faster with zero allocations ## License diff --git a/router.go b/router.go index 386c1b3..0b1e330 100644 --- a/router.go +++ b/router.go @@ -30,6 +30,7 @@ type node struct { type Router struct { get, post, put, patch, delete *node middleware []Middleware + paramsBuffer []string } type Group struct { @@ -41,12 +42,13 @@ type Group struct { // New creates a new Router instance. func New() *Router { return &Router{ - get: &node{}, - post: &node{}, - put: &node{}, - patch: &node{}, - delete: &node{}, - middleware: []Middleware{}, + get: &node{}, + post: &node{}, + put: &node{}, + patch: &node{}, + delete: &node{}, + middleware: []Middleware{}, + paramsBuffer: make([]string, 64), } } @@ -352,18 +354,28 @@ func (r *Router) Lookup(method, path string) (Handler, []string, bool) { return nil, nil, false } if path == "/" { - return root.handler, []string{}, root.handler != nil + return root.handler, nil, root.handler != nil } - params := make([]string, 0, root.maxParams) - h, found := match(root, path, 0, ¶ms) + + buffer := r.paramsBuffer + if cap(buffer) < int(root.maxParams) { + buffer = make([]string, root.maxParams) + r.paramsBuffer = buffer + } + buffer = buffer[:0] + + h, paramCount, found := match(root, path, 0, &buffer) if !found { return nil, nil, false } - return h, params, true + + return h, buffer[:paramCount], true } // match traverses the trie to find a handler. -func match(current *node, path string, start int, params *[]string) (Handler, bool) { +func match(current *node, path string, start int, params *[]string) (Handler, int, bool) { + paramCount := 0 + for _, c := range current.children { if c.isWildcard { rem := path[start:] @@ -371,26 +383,30 @@ func match(current *node, path string, start int, params *[]string) (Handler, bo rem = rem[1:] } *params = append(*params, rem) - return c.handler, c.handler != nil + return c.handler, 1, c.handler != nil } } + seg, pos, more := readSegment(path, start) if seg == "" { - return current.handler, current.handler != nil + return current.handler, 0, current.handler != nil } + for _, c := range current.children { if c.segment == seg || c.isDynamic { if c.isDynamic { *params = append(*params, seg) + paramCount++ } if !more { - return c.handler, c.handler != nil + return c.handler, paramCount, c.handler != nil } - h, ok := match(c, path, pos, params) + h, nestedCount, ok := match(c, path, pos, params) if ok { - return h, true + return h, paramCount + nestedCount, true } } } - return nil, false + + return nil, 0, false }