Moonshark/http/router/router.go
2025-07-14 17:36:59 -05:00

242 lines
4.9 KiB
Go

package router
import (
"fmt"
)
// Handler function that takes parameters as strings
type Handler func(params []string)
type node struct {
segment string
handlerID int
children []*node
isDynamic bool
isWildcard bool
maxParams uint8
}
type Router struct {
get, post, put, patch, delete *node
paramsBuffer []string
}
// Params holds URL parameters
type Params struct {
Keys []string
Values []string
}
// Get returns a parameter value by name
func (p *Params) Get(name string) string {
for i, key := range p.Keys {
if key == name && i < len(p.Values) {
return p.Values[i]
}
}
return ""
}
// New creates a new Router instance
func New() *Router {
return &Router{
get: &node{},
post: &node{},
put: &node{},
patch: &node{},
delete: &node{},
paramsBuffer: make([]string, 64),
}
}
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
}
}
// AddRoute adds a route with handler ID (for compatibility)
func (r *Router) AddRoute(method, path string, handlerID int) error {
root := r.methodNode(method)
if root == nil {
return fmt.Errorf("unsupported method: %s", method)
}
// Create a handler that stores the ID
h := func(params []string) {
// This is a placeholder - the actual execution happens in HTTP module
}
return r.addRoute(root, path, h, handlerID)
}
// readSegment extracts the next path segment
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 trie
func (r *Router) addRoute(root *node, path string, h Handler, handlerID int) error {
if path == "/" {
root.handlerID = handlerID
return nil
}
current := root
pos := 0
lastWC := false
count := uint8(0)
for {
seg, newPos, more := readSegment(path, pos)
if seg == "" {
break
}
isDyn := len(seg) > 1 && seg[0] == ':'
isWC := len(seg) > 0 && seg[0] == '*'
if isWC {
if lastWC || more {
return fmt.Errorf("wildcard must be the last segment in the path")
}
lastWC = true
}
if isDyn || isWC {
count++
}
var child *node
for _, c := range current.children {
if c.segment == seg {
child = c
break
}
}
if child == nil {
child = &node{segment: seg, isDynamic: isDyn, isWildcard: isWC}
current.children = append(current.children, child)
}
if child.maxParams < count {
child.maxParams = count
}
current = child
pos = newPos
}
current.handlerID = handlerID
return nil
}
// Lookup finds a handler matching method and path
func (r *Router) Lookup(method, path string) (int, *Params, bool) {
root := r.methodNode(method)
if root == nil {
return 0, nil, false
}
if path == "/" {
if root.handlerID != 0 {
return root.handlerID, &Params{}, true
}
return 0, nil, false
}
buffer := r.paramsBuffer
if cap(buffer) < int(root.maxParams) {
buffer = make([]string, root.maxParams)
r.paramsBuffer = buffer
}
buffer = buffer[:0]
handlerID, paramCount, paramKeys, found := r.match(root, path, 0, &buffer)
if !found {
return 0, nil, false
}
params := &Params{
Keys: paramKeys,
Values: buffer[:paramCount],
}
return handlerID, params, true
}
// match traverses the trie to find a handler
func (r *Router) match(current *node, path string, start int, params *[]string) (int, int, []string, bool) {
paramCount := 0
var paramKeys []string
// Check wildcards first
for _, c := range current.children {
if c.isWildcard {
rem := path[start:]
if len(rem) > 0 && rem[0] == '/' {
rem = rem[1:]
}
*params = append(*params, rem)
// Extract param name from *name format
paramName := c.segment[1:] // Remove the * prefix
paramKeys = append(paramKeys, paramName)
return c.handlerID, 1, paramKeys, c.handlerID != 0
}
}
seg, pos, more := readSegment(path, start)
if seg == "" {
return current.handlerID, 0, paramKeys, current.handlerID != 0
}
for _, c := range current.children {
if c.segment == seg || c.isDynamic {
if c.isDynamic {
*params = append(*params, seg)
// Extract param name from :name format
paramName := c.segment[1:] // Remove the : prefix
paramKeys = append(paramKeys, paramName)
paramCount++
}
if !more {
return c.handlerID, paramCount, paramKeys, c.handlerID != 0
}
handlerID, nestedCount, nestedKeys, ok := r.match(c, path, pos, params)
if ok {
allKeys := append(paramKeys, nestedKeys...)
return handlerID, paramCount + nestedCount, allKeys, true
}
}
}
return 0, 0, nil, false
}