225 lines
5.4 KiB
Go
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, ¶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
|
|
}
|