diff --git a/README.md b/README.md index 7e179aa..6ae4989 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A high-performance router for Go with support for path parameters, wildcards, mi - Support for path parameters (`[id]`) and wildcards (`*path`) - Middleware support for request processing pipelines - Route grouping with shared prefixes and middleware -- Adapters for net/http and fasthttp (optional) +- Implements `http.Handler` for direct integration - Up to 15x faster than `http.ServeMux` for dynamic routes - Zero allocations for static routes @@ -27,26 +27,34 @@ go get git.sharkk.net/Go/Router r := router.New() // Static routes -r.Get("/", router.FuncHandler(func(params []string) { - fmt.Println("Root handler") -})) +r.Get("/", func(w http.ResponseWriter, r *http.Request, params []string) { + fmt.Fprintf(w, "Root handler") +}) // Parameter routes -r.Get("/users/[id]", router.FuncHandler(func(params []string) { +r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { userID := params[0] - fmt.Printf("User ID: %s\n", userID) -})) + fmt.Fprintf(w, "User ID: %s", userID) +}) // Wildcard routes -r.Get("/files/*path", router.FuncHandler(func(params []string) { +r.Get("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) { filePath := params[0] - fmt.Printf("File path: %s\n", filePath) + fmt.Fprintf(w, "File path: %s", filePath) +}) + +// Standard http.HandlerFunc adapter +r.Get("/simple", router.StandardHandler(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Simple handler without params") })) -// Lookup routes +// Lookup routes manually if handler, params, ok := r.Lookup("GET", "/users/123"); ok { handler.Serve(params) } + +// Use listen and serve directly +http.ListenAndServe(":8080", r) ``` ### Middleware @@ -54,11 +62,13 @@ if handler, params, ok := r.Lookup("GET", "/users/123"); ok { ```go // Create logging middleware func LoggingMiddleware(next router.Handler) router.Handler { - return router.FuncHandler(func(params []string) { - fmt.Println("Request started") - next.Serve(params) - fmt.Println("Request completed") - }) + return &router.simpleHandler{ + fn: func(params []string) { + fmt.Println("Request started") + next.Serve(params) + fmt.Println("Request completed") + }, + } } // Apply middleware globally @@ -67,6 +77,8 @@ r.Use(LoggingMiddleware) // Apply middleware to specific routes r.WithMiddleware(AuthMiddleware).Get("/admin", adminHandler) + +http.ListenAndServe(":8080", r) ``` ### Route Groups @@ -87,59 +99,13 @@ v1.Get("/products", listProductsHandler) // matches /api/v1/products admin := api.Group("/admin") admin.Use(AuthMiddleware) admin.Get("/stats", statsHandler) // matches /api/admin/stats -``` -### HTTP Integration - -```go -// Standard net/http integration -http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Find the matching handler - handler, params, found := router.Lookup(r.Method, r.URL.Path) - if !found { - http.NotFound(w, r) - return - } - - // Create an HTTP-compatible handler - httpHandler := router.NewHTTP(w, r, func(w http.ResponseWriter, r *http.Request, params []string) { - fmt.Fprintf(w, "Hello, user %s", params[0]) - }) - - // Execute the handler - httpHandler.Serve(params) -}) -``` - -### FastHTTP Integration (Optional) - -Build with `-tags fasthttp` to enable FastHTTP support: - -```go -// FastHTTP integration -fastHandler := func(ctx *fasthttp.RequestCtx) { - path := string(ctx.Path()) - method := string(ctx.Method()) - - handler, params, found := router.Lookup(method, path) - if !found { - ctx.Error("Not found", fasthttp.StatusNotFound) - return - } - - // Create a FastHTTP-compatible handler - fastHandler := router.NewFastHTTP(ctx, func(ctx *fasthttp.RequestCtx, params []string) { - fmt.Fprintf(ctx, "Hello, user %s", params[0]) - }) - - // Execute the handler - fastHandler.Serve(params) -} +http.ListenAndServe(":8080", r) ``` ## Benchmarks -Benchmark results comparing our router to the standard `http.ServeMux` on AMD Ryzen 9 7950X: +Benchmark results comparing Router to the standard `http.ServeMux`: ``` cpu: AMD Ryzen 9 7950X 16-Core Processor @@ -161,7 +127,6 @@ 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 diff --git a/router.go b/router.go index b439e0c..9e27946 100644 --- a/router.go +++ b/router.go @@ -3,6 +3,7 @@ package router import ( "fmt" "net/http" + "slices" ) // Handler is an interface for handling HTTP requests with path parameters. @@ -111,7 +112,7 @@ func (g *Group) Group(prefix string) *Group { return &Group{ router: g.router, prefix: g.prefix + prefix, - middleware: append([]Middleware{}, g.middleware...), + middleware: slices.Clone(g.middleware), } } @@ -124,166 +125,101 @@ func applyMiddleware(handler Handler, middleware []Middleware) Handler { return h } -// Get registers a handler for GET requests at the given path. -func (r *Router) Get(path string, handler http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } +// HandlerFunc is a function that handles HTTP requests with parameters. +type HandlerFunc func(w http.ResponseWriter, r *http.Request, params []string) - return r.addRoute(r.get, path, &httpHandler{h: httpHandlerFunc}, r.middleware) +// 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, &httpHandler{h: handler}, r.middleware) } -// Get registers a handler with parameters for GET requests at the given path. -func (r *Router) GetParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - return r.addRoute(r.get, path, &httpHandler{h: 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - return r.addRoute(r.post, path, &httpHandler{h: httpHandlerFunc}, r.middleware) -} - -// Post registers a handler with parameters for POST requests at the given path. -func (r *Router) PostParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - return r.addRoute(r.post, path, &httpHandler{h: handler}, r.middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - return r.addRoute(r.put, path, &httpHandler{h: httpHandlerFunc}, r.middleware) -} - -// Put registers a handler with parameters for PUT requests at the given path. -func (r *Router) PutParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - return r.addRoute(r.put, path, &httpHandler{h: handler}, r.middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - return r.addRoute(r.patch, path, &httpHandler{h: httpHandlerFunc}, r.middleware) -} - -// Patch registers a handler with parameters for PATCH requests at the given path. -func (r *Router) PatchParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - return r.addRoute(r.patch, path, &httpHandler{h: handler}, r.middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) +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 := slices.Clone(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) } - return r.addRoute(r.delete, path, &httpHandler{h: httpHandlerFunc}, r.middleware) + fullPath := g.prefix + path + return g.router.addRoute(root, fullPath, &httpHandler{h: handler}, g.buildGroupMiddleware()) } -// Delete registers a handler with parameters for DELETE requests at the given path. -func (r *Router) DeleteParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - return r.addRoute(r.delete, path, &httpHandler{h: handler}, r.middleware) -} - -// Group HTTP handler registration methods - // Get registers a handler for GET requests at the given path. -func (g *Group) Get(path string, handler http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.get, g.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Get registers a handler with parameters for GET requests at the given path. -func (g *Group) GetParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.get, g.prefix+path, &httpHandler{h: handler}, middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.post, g.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Post registers a handler with parameters for POST requests at the given path. -func (g *Group) PostParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.post, g.prefix+path, &httpHandler{h: handler}, middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.put, g.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Put registers a handler with parameters for PUT requests at the given path. -func (g *Group) PutParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.put, g.prefix+path, &httpHandler{h: handler}, middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.patch, g.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Patch registers a handler with parameters for PATCH requests at the given path. -func (g *Group) PatchParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.patch, g.prefix+path, &httpHandler{h: handler}, middleware) +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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.delete, g.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Delete registers a handler with parameters for DELETE requests at the given path. -func (g *Group) DeleteParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, g.router.middleware...) - middleware = append(middleware, g.middleware...) - return g.router.addRoute(g.router.delete, g.prefix+path, &httpHandler{h: handler}, middleware) +func (g *Group) Delete(path string, handler HandlerFunc) error { + return g.Handle("DELETE", path, handler) } // WithMiddleware applies specific middleware to the next route registration. @@ -314,198 +250,95 @@ type MiddlewareGroup struct { middleware []Middleware } -// HTTP method registration functions for MiddlewareRouter - -// Get registers a handler for GET requests with specific middleware. -func (mr *MiddlewareRouter) Get(path string, handler http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.get, path, &httpHandler{h: httpHandlerFunc}, middleware) +// buildMiddleware returns combined middleware for the middleware router +func (mr *MiddlewareRouter) buildMiddleware() []Middleware { + middleware := slices.Clone(mr.router.middleware) + return append(middleware, mr.middleware...) } -// Get registers a handler with parameters for GET requests with specific middleware. -func (mr *MiddlewareRouter) GetParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.get, path, &httpHandler{h: handler}, 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, &httpHandler{h: 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.post, path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Post registers a handler with parameters for POST requests with specific middleware. -func (mr *MiddlewareRouter) PostParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.post, path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.put, path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Put registers a handler with parameters for PUT requests with specific middleware. -func (mr *MiddlewareRouter) PutParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.put, path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.patch, path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Patch registers a handler with parameters for PATCH requests with specific middleware. -func (mr *MiddlewareRouter) PatchParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.patch, path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) +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 := slices.Clone(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) } - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.delete, path, &httpHandler{h: httpHandlerFunc}, middleware) + fullPath := mg.group.prefix + path + return mg.group.router.addRoute(root, fullPath, &httpHandler{h: handler}, mg.buildMiddleware()) } -// Delete registers a handler with parameters for DELETE requests with specific middleware. -func (mr *MiddlewareRouter) DeleteParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mr.router.middleware...) - middleware = append(middleware, mr.middleware...) - return mr.router.addRoute(mr.router.delete, path, &httpHandler{h: handler}, middleware) -} - -// HTTP method registration functions for MiddlewareGroup - // Get registers a handler for GET requests with specific middleware. -func (mg *MiddlewareGroup) Get(path string, handler http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.get, mg.group.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Get registers a handler with parameters for GET requests with specific middleware. -func (mg *MiddlewareGroup) GetParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.get, mg.group.prefix+path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.post, mg.group.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Post registers a handler with parameters for POST requests with specific middleware. -func (mg *MiddlewareGroup) PostParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.post, mg.group.prefix+path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.put, mg.group.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Put registers a handler with parameters for PUT requests with specific middleware. -func (mg *MiddlewareGroup) PutParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.put, mg.group.prefix+path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.patch, mg.group.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) -} - -// Patch registers a handler with parameters for PATCH requests with specific middleware. -func (mg *MiddlewareGroup) PatchParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.patch, mg.group.prefix+path, &httpHandler{h: handler}, 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 http.HandlerFunc) error { - httpHandlerFunc := func(w http.ResponseWriter, r *http.Request, _ []string) { - handler(w, r) - } - - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.delete, mg.group.prefix+path, &httpHandler{h: httpHandlerFunc}, middleware) +func (mg *MiddlewareGroup) Delete(path string, handler HandlerFunc) error { + return mg.Handle("DELETE", path, handler) } -// Delete registers a handler with parameters for DELETE requests with specific middleware. -func (mg *MiddlewareGroup) DeleteParam(path string, handler func(w http.ResponseWriter, r *http.Request, params []string)) error { - middleware := append([]Middleware{}, mg.group.router.middleware...) - middleware = append(middleware, mg.group.middleware...) - middleware = append(middleware, mg.middleware...) - return mg.group.router.addRoute(mg.group.router.delete, mg.group.prefix+path, &httpHandler{h: handler}, middleware) +// Adapter for standard http.HandlerFunc +func StandardHandler(handler http.HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, _ []string) { + handler(w, r) + } } // readSegment extracts the next path segment starting at the given position. @@ -599,24 +432,13 @@ func (r *Router) addRoute(root *node, path string, handler Handler, middleware [ // 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) { - var root *node - switch method { - case "GET": - root = r.get - case "POST": - root = r.post - case "PUT": - root = r.put - case "PATCH": - root = r.patch - case "DELETE": - root = r.delete - default: + root := r.methodNode(method) + if root == nil { return nil, nil, false } if path == "/" { - return root.handler, nil, root.handler != nil + return root.handler, []string{}, root.handler != nil } params := make([]string, 0, root.maxParams) diff --git a/router_test.go b/router_test.go index 4b653e8..1829f6f 100644 --- a/router_test.go +++ b/router_test.go @@ -2,6 +2,7 @@ package router import ( "net/http" + "net/http/httptest" "testing" assert "git.sharkk.net/Go/Assert" @@ -23,7 +24,7 @@ func newHandler(fn func(params []string)) Handler { func TestRootPath(t *testing.T) { r := New() - r.GetParam("/", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/", func(w http.ResponseWriter, r *http.Request, params []string) { // No-op for testing }) @@ -34,7 +35,7 @@ func TestRootPath(t *testing.T) { func TestStaticPath(t *testing.T) { r := New() - r.GetParam("/users/all", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/users/all", func(w http.ResponseWriter, r *http.Request, params []string) { // No-op for testing }) @@ -47,7 +48,7 @@ func TestSingleParameter(t *testing.T) { r := New() called := false - r.GetParam("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { called = true assert.Equal(t, params[0], "123") }) @@ -62,7 +63,7 @@ func TestMultipleParameters(t *testing.T) { r := New() called := false - r.GetParam("/users/[id]/posts/[postId]", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/users/[id]/posts/[postId]", func(w http.ResponseWriter, r *http.Request, params []string) { called = true assert.Equal(t, params[0], "123") assert.Equal(t, params[1], "456") @@ -76,7 +77,7 @@ func TestMultipleParameters(t *testing.T) { func TestNonExistentPath(t *testing.T) { r := New() - r.GetParam("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { // No-op for testing }) @@ -86,7 +87,7 @@ func TestNonExistentPath(t *testing.T) { func TestWrongMethod(t *testing.T) { r := New() - r.GetParam("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { // No-op for testing }) @@ -98,7 +99,7 @@ func TestTrailingSlash(t *testing.T) { r := New() called := false - r.GetParam("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { called = true assert.Equal(t, params[0], "123") }) @@ -114,11 +115,11 @@ func TestDifferentMethods(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request, params []string) {} - r.GetParam("/test", handler) - r.PostParam("/test", handler) - r.PutParam("/test", handler) - r.PatchParam("/test", handler) - r.DeleteParam("/test", handler) + 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 { @@ -134,7 +135,7 @@ func TestWildcardPath(t *testing.T) { t.Run("simple wildcard", func(t *testing.T) { called := false - err := r.GetParam("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) { + err := r.Get("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) { called = true assert.Equal(t, params[0], "docs/report.pdf") }) @@ -148,7 +149,7 @@ func TestWildcardPath(t *testing.T) { t.Run("wildcard with empty path", func(t *testing.T) { called := false - err := r.GetParam("/download/*filepath", func(w http.ResponseWriter, r *http.Request, params []string) { + err := r.Get("/download/*filepath", func(w http.ResponseWriter, r *http.Request, params []string) { called = true assert.Equal(t, params[0], "") }) @@ -162,7 +163,7 @@ func TestWildcardPath(t *testing.T) { t.Run("wildcard with parameter", func(t *testing.T) { called := false - err := r.GetParam("/users/[id]/*action", func(w http.ResponseWriter, r *http.Request, params []string) { + err := r.Get("/users/[id]/*action", func(w http.ResponseWriter, r *http.Request, params []string) { called = true assert.Equal(t, params[0], "123") assert.Equal(t, params[1], "settings/profile/avatar") @@ -176,12 +177,12 @@ func TestWildcardPath(t *testing.T) { }) t.Run("multiple wildcards not allowed", func(t *testing.T) { - err := r.GetParam("/api/*version/*path", func(w http.ResponseWriter, r *http.Request, params []string) {}) + err := r.Get("/api/*version/*path", func(w http.ResponseWriter, r *http.Request, params []string) {}) assert.NotNil(t, err) }) t.Run("non-last wildcard not allowed", func(t *testing.T) { - err := r.GetParam("/api/*version/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) + err := r.Get("/api/*version/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) assert.NotNil(t, err) }) } @@ -201,7 +202,7 @@ func TestMiddleware(t *testing.T) { }) }) - r.GetParam("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) + r.Get("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) h, params, found := r.Lookup("GET", "/test") assert.True(t, found) @@ -231,7 +232,7 @@ func TestMiddleware(t *testing.T) { }) }) - r.GetParam("/test", func(w http.ResponseWriter, r *http.Request, params []string) { + r.Get("/test", func(w http.ResponseWriter, r *http.Request, params []string) { order = append(order, 0) }) @@ -260,7 +261,7 @@ func TestMiddleware(t *testing.T) { }) } - r.WithMiddleware(middleware).GetParam("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) + r.WithMiddleware(middleware).Get("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) h, params, found := r.Lookup("GET", "/test") assert.True(t, found) @@ -276,7 +277,7 @@ func TestGroup(t *testing.T) { // Create API group api := r.Group("/api") - api.GetParam("/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) + api.Get("/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) h, params, found := r.Lookup("GET", "/api/users") assert.True(t, found) @@ -289,7 +290,7 @@ func TestGroup(t *testing.T) { // Create nested groups api := r.Group("/api") v1 := api.Group("/v1") - v1.GetParam("/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) + v1.Get("/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) h, params, found := r.Lookup("GET", "/api/v1/users") assert.True(t, found) @@ -309,7 +310,7 @@ func TestGroup(t *testing.T) { }) }) - api.GetParam("/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) + api.Get("/users", func(w http.ResponseWriter, r *http.Request, params []string) {}) h, params, found := r.Lookup("GET", "/api/users") assert.True(t, found) @@ -339,7 +340,7 @@ func TestGroup(t *testing.T) { }) }) - v1.GetParam("/users", func(w http.ResponseWriter, r *http.Request, params []string) { + v1.Get("/users", func(w http.ResponseWriter, r *http.Request, params []string) { order = append(order, 3) }) @@ -373,7 +374,7 @@ func TestGroup(t *testing.T) { order = append(order, 2) next.Serve(params) }) - }).GetParam("/users", func(w http.ResponseWriter, r *http.Request, params []string) { + }).Get("/users", func(w http.ResponseWriter, r *http.Request, params []string) { order = append(order, 3) }) @@ -395,10 +396,10 @@ func BenchmarkRouterLookup(b *testing.B) { handler := func(w http.ResponseWriter, r *http.Request, params []string) {} // Setup routes for benchmarking - r.GetParam("/", handler) - r.GetParam("/users/all", handler) - r.GetParam("/users/[id]", handler) - r.GetParam("/users/[id]/posts/[postId]", handler) + 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++ { @@ -435,9 +436,9 @@ func BenchmarkParallelLookup(b *testing.B) { r := New() handler := func(w http.ResponseWriter, r *http.Request, params []string) {} - r.GetParam("/users/[id]", handler) - r.GetParam("/posts/[id]/comments", handler) - r.GetParam("/products/[category]/[id]", handler) + 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 @@ -460,8 +461,8 @@ func BenchmarkWildcardLookup(b *testing.B) { handler := func(w http.ResponseWriter, r *http.Request, params []string) {} // Setup routes for benchmarking - r.GetParam("/files/*path", handler) - r.GetParam("/users/[id]/*action", handler) + 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++ { @@ -485,7 +486,7 @@ func BenchmarkMiddleware(b *testing.B) { b.Run("no_middleware", func(b *testing.B) { r := New() - r.GetParam("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) + r.Get("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -497,7 +498,7 @@ func BenchmarkMiddleware(b *testing.B) { b.Run("one_middleware", func(b *testing.B) { r := New() r.Use(passthrough) - r.GetParam("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) + r.Get("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -511,7 +512,7 @@ func BenchmarkMiddleware(b *testing.B) { for i := 0; i < 5; i++ { r.Use(passthrough) } - r.GetParam("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) + r.Get("/test", func(w http.ResponseWriter, r *http.Request, params []string) {}) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -526,7 +527,7 @@ func BenchmarkGroups(b *testing.B) { b.Run("flat_route", func(b *testing.B) { r := New() - r.GetParam("/api/v1/users", handler) + r.Get("/api/v1/users", handler) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -538,7 +539,7 @@ func BenchmarkGroups(b *testing.B) { r := New() api := r.Group("/api") v1 := api.Group("/v1") - v1.GetParam("/users", handler) + v1.Get("/users", handler) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -555,7 +556,7 @@ func BenchmarkGroups(b *testing.B) { }) }) v1 := api.Group("/v1") - v1.GetParam("/users", handler) + v1.Get("/users", handler) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -565,15 +566,77 @@ func BenchmarkGroups(b *testing.B) { }) } +func BenchmarkNetHttpServer(b *testing.B) { + r := New() + + // Set up test routes + r.Get("/", func(w http.ResponseWriter, r *http.Request, params []string) { + // No-op for benchmarking + }) + r.Get("/users/all", func(w http.ResponseWriter, r *http.Request, params []string) { + // No-op for benchmarking + }) + r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) { + // No-op for benchmarking + }) + + // Create test server + server := httptest.NewServer(r) + defer server.Close() + + // Create HTTP client + client := &http.Client{ + Transport: &http.Transport{ + DisableKeepAlives: false, + }, + } + + b.Run("root", func(b *testing.B) { + url := server.URL + "/" + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp, err := client.Get(url) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) + + b.Run("static_path", func(b *testing.B) { + url := server.URL + "/users/all" + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp, err := client.Get(url) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) + + b.Run("param_path", func(b *testing.B) { + url := server.URL + "/users/123" + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp, err := client.Get(url) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) +} + func BenchmarkComparison(b *testing.B) { // Custom router setup customRouter := New() handler := func(w http.ResponseWriter, r *http.Request, params []string) {} - customRouter.GetParam("/", handler) - customRouter.GetParam("/users/all", handler) - customRouter.GetParam("/users/[id]", handler) - customRouter.GetParam("/users/[id]/posts/[postId]", handler) + customRouter.Get("/", handler) + customRouter.Get("/users/all", handler) + customRouter.Get("/users/[id]", handler) + customRouter.Get("/users/[id]/posts/[postId]", handler) // Standard mux setup mux := http.NewServeMux()