Allows configuring a custom handler for unmatched routes, enabling consistent JSON error responses instead of ServeMux's default plain text. NewRouter now accepts RouterOption functional options. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
3.7 KiB
Go
127 lines
3.7 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Router is a lightweight wrapper around http.ServeMux that adds middleware
|
|
// groups and sub-router mounting. It leverages Go 1.22+ enhanced patterns
|
|
// like "GET /users/{id}".
|
|
type Router struct {
|
|
mux *http.ServeMux
|
|
prefix string
|
|
middlewares []Middleware
|
|
notFoundHandler http.Handler
|
|
}
|
|
|
|
// RouterOption configures a Router.
|
|
type RouterOption func(*Router)
|
|
|
|
// WithNotFoundHandler sets a custom handler for requests that don't match
|
|
// any registered pattern. This is useful for returning JSON 404/405 responses
|
|
// instead of the default plain text.
|
|
func WithNotFoundHandler(h http.Handler) RouterOption {
|
|
return func(r *Router) { r.notFoundHandler = h }
|
|
}
|
|
|
|
// NewRouter creates a new Router backed by a fresh http.ServeMux.
|
|
func NewRouter(opts ...RouterOption) *Router {
|
|
r := &Router{
|
|
mux: http.NewServeMux(),
|
|
}
|
|
for _, opt := range opts {
|
|
opt(r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Handle registers a handler for the given pattern. The pattern follows
|
|
// http.ServeMux conventions, including method-based patterns like "GET /users".
|
|
func (r *Router) Handle(pattern string, handler http.Handler) {
|
|
if len(r.middlewares) > 0 {
|
|
handler = Chain(r.middlewares...)(handler)
|
|
}
|
|
r.mux.Handle(r.prefixedPattern(pattern), handler)
|
|
}
|
|
|
|
// HandleFunc registers a handler function for the given pattern.
|
|
func (r *Router) HandleFunc(pattern string, fn http.HandlerFunc) {
|
|
r.Handle(pattern, fn)
|
|
}
|
|
|
|
// Group creates a sub-router with a shared prefix and optional middleware.
|
|
// Patterns registered on the group are prefixed automatically. The group
|
|
// shares the underlying ServeMux with the parent router.
|
|
//
|
|
// Example:
|
|
//
|
|
// api := router.Group("/api/v1", authMiddleware)
|
|
// api.HandleFunc("GET /users", listUsers) // registers "GET /api/v1/users"
|
|
func (r *Router) Group(prefix string, mws ...Middleware) *Router {
|
|
return &Router{
|
|
mux: r.mux,
|
|
prefix: r.prefix + prefix,
|
|
middlewares: append(r.middlewaresSnapshot(), mws...),
|
|
}
|
|
}
|
|
|
|
// Mount attaches an http.Handler under the given prefix. All requests
|
|
// starting with prefix are forwarded to the handler with the prefix stripped.
|
|
func (r *Router) Mount(prefix string, handler http.Handler) {
|
|
full := r.prefix + prefix
|
|
if !strings.HasSuffix(full, "/") {
|
|
full += "/"
|
|
}
|
|
r.mux.Handle(full, http.StripPrefix(strings.TrimSuffix(full, "/"), handler))
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler, making Router usable as a handler.
|
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
if r.notFoundHandler != nil {
|
|
// Use the mux to check for a match. If none, use the custom handler.
|
|
_, pattern := r.mux.Handler(req)
|
|
if pattern == "" {
|
|
r.notFoundHandler.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
}
|
|
r.mux.ServeHTTP(w, req)
|
|
}
|
|
|
|
// prefixedPattern inserts the router prefix into a pattern. It is aware of
|
|
// method prefixes: "GET /users" with prefix "/api" becomes "GET /api/users".
|
|
func (r *Router) prefixedPattern(pattern string) string {
|
|
if r.prefix == "" {
|
|
return pattern
|
|
}
|
|
|
|
// Split method prefix if present: "GET /users" → method="GET ", path="/users"
|
|
method, path, hasMethod := splitMethodPattern(pattern)
|
|
|
|
path = r.prefix + path
|
|
|
|
if hasMethod {
|
|
return method + path
|
|
}
|
|
return path
|
|
}
|
|
|
|
// splitMethodPattern splits "GET /path" into ("GET ", "/path", true).
|
|
// If there is no method prefix, returns ("", pattern, false).
|
|
func splitMethodPattern(pattern string) (method, path string, hasMethod bool) {
|
|
if idx := strings.IndexByte(pattern, ' '); idx >= 0 {
|
|
return pattern[:idx+1], pattern[idx+1:], true
|
|
}
|
|
return "", pattern, false
|
|
}
|
|
|
|
func (r *Router) middlewaresSnapshot() []Middleware {
|
|
if len(r.middlewares) == 0 {
|
|
return nil
|
|
}
|
|
cp := make([]Middleware, len(r.middlewares))
|
|
copy(cp, r.middlewares)
|
|
return cp
|
|
}
|