Router/router.go
2025-02-15 21:57:18 -06:00

225 lines
5.4 KiB
Go

package router
import (
"fmt"
)
// Handler processes HTTP requests with optional path parameters.
type Handler func(params []string)
// 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, and wildcards.
type Router struct {
get *node
post *node
put *node
patch *node
delete *node
}
// New creates a new Router instance.
func New() *Router {
return &Router{
get: &node{},
post: &node{},
put: &node{},
patch: &node{},
delete: &node{},
}
}
// HTTP method registration functions
// Routes can contain static segments (/users), parameters ([id]), and wildcards (*filepath)
// Get registers a handler for GET requests at the given path.
func (r *Router) Get(path string, handler Handler) error {
return r.addRoute(r.get, path, handler)
}
// Post registers a handler for POST requests at the given path.
func (r *Router) Post(path string, handler Handler) error {
return r.addRoute(r.post, path, handler)
}
// Put registers a handler for PUT requests at the given path.
func (r *Router) Put(path string, handler Handler) error {
return r.addRoute(r.put, path, handler)
}
// Patch registers a handler for PATCH requests at the given path.
func (r *Router) Patch(path string, handler Handler) error {
return r.addRoute(r.patch, path, handler)
}
// Delete registers a handler for DELETE requests at the given path.
func (r *Router) Delete(path string, handler Handler) error {
return r.addRoute(r.delete, path, handler)
}
// 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.
// It validates wildcard positions and tracks parameter counts.
func (r *Router) addRoute(root *node, path string, handler Handler) error {
if path == "/" {
root.handler = handler
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 = handler
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) {
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:
return nil, nil, false
}
if path == "/" {
return root.handler, nil, root.handler != nil
}
params := make([]string, 0, root.maxParams)
h, found := match(root, path, 0, &params)
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
}