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 }