@@ -212,6 +212,33 @@ func (r *Router) QueryOPTIONS(pattern string, handler http.Handler) *RouteBuilde
212212 return r .handleQuery ("OPTIONS" , pattern , handler )
213213}
214214
215+ // Handle registers an arbitrary http.Handler with a pattern and method
216+ // Supports both direct registration and query-based multiplexing via RouteBuilder
217+ func (r * Router ) Handle (method , pattern string , handler http.Handler ) * RouteBuilder {
218+ return r .handleDirect (method , pattern , handler )
219+ }
220+
221+ // RegisterExternal adds a "phantom" route to the router.
222+ // Phantom routes are only for documentation and do not dispatch requests.
223+ // They appear in the Routes() listing and can be used for URL generation.
224+ func (r * Router ) RegisterExternal (method , pattern , name , action string ) {
225+ // Create a dummy route metadata
226+ rt := & core.Route {
227+ Method : method ,
228+ Pattern : r .pathPrefix + pattern ,
229+ Name : r .namePrefix + name ,
230+ Action : action ,
231+ }
232+
233+ // Add to routes list for listing
234+ * r .routes = append (* r .routes , rt )
235+
236+ // Add to name index for URL generation
237+ if rt .Name != "" {
238+ r .nameIndex [rt .Name ] = rt
239+ }
240+ }
241+
215242// handleDirect registers a route directly with Chi (no dispatcher, best performance)
216243// This is used by GET, POST, PUT, DELETE, etc.
217244// Automatically promotes to dispatcher-based routing if multiple routes exist on same method+pattern
@@ -250,7 +277,7 @@ func (r *Router) handleDirect(method, pattern string, handler http.Handler) *Rou
250277
251278 // Check if another direct route already exists
252279 if existingRoute , exists := r .directRoutes [dispatcherKey ]; exists {
253- r .debugLogf ("Auto-promoting to dispatcher: %s %s (multiple routes on same path)" , method , fullPattern )
280+ r .debugLogf ("Auto-promoting to dispatcher: %s %s (chi: %s) ( multiple routes on same path)" , method , fullPattern , chiPattern )
254281 // Promote to dispatcher!
255282 disp := & core.Dispatcher {Routes : make ([]* core.Route , 0 )}
256283 disp .AddRoute (existingRoute ) // Old route becomes fallback
@@ -271,6 +298,7 @@ func (r *Router) handleDirect(method, pattern string, handler http.Handler) *Rou
271298 }
272299 * r .optimizedHandlers = append (* r .optimizedHandlers , optHandler )
273300
301+ r .debugLogf ("Registering direct route with Chi: %s %s (chi: %s)" , method , fullPattern , chiPattern )
274302 r .mux .Method (method , chiPattern , optHandler )
275303 r .directRoutes [dispatcherKey ] = rt
276304
@@ -432,6 +460,68 @@ func (r *Router) Use(middlewares ...func(http.Handler) http.Handler) {
432460 r .mux .Use (middlewares ... )
433461}
434462
463+ // Mount attaches another http.Handler (typically a router) to a prefix.
464+ // If the handler is a *teapot.Router, all its routes are propagated to the
465+ // parent router with the prefix prepended for unified listing and URL generation.
466+ func (r * Router ) Mount (pattern string , handler http.Handler ) {
467+ fullPattern := r .pathPrefix + pattern
468+ r .mux .Mount (pattern , handler )
469+
470+ r .propagateRoutes (fullPattern , handler )
471+ }
472+
473+ func (r * Router ) propagateRoutes (prefix string , handler http.Handler ) {
474+ // Propagate routes if it's a teapot Router
475+ if subRouter , ok := handler .(* Router ); ok {
476+ for _ , rt := range * subRouter .routes {
477+ // Create a copy of the route with updated pattern and name
478+ newRoute := & core.Route {
479+ Method : rt .Method ,
480+ Pattern : prefix + rt .Pattern ,
481+ ChiPattern : prefix + rt .ChiPattern ,
482+ Handler : rt .Handler ,
483+ Name : r .namePrefix + rt .Name ,
484+ Action : rt .Action ,
485+ QueryMatchers : rt .QueryMatchers ,
486+ Middlewares : rt .Middlewares ,
487+ WildcardParams : rt .WildcardParams ,
488+ }
489+ * r .routes = append (* r .routes , newRoute )
490+
491+ // Update name index for URL generation
492+ if newRoute .Name != "" {
493+ r .nameIndex [newRoute .Name ] = newRoute
494+ }
495+ }
496+ }
497+ }
498+
499+ // SubRouter creates a new child router whose routes are automatically
500+ // visible in the parent router with the prefix prepended.
501+ //
502+ // Unlike Group(), SubRouter() returns a separate Router instance that
503+ // can be used independently (e.g., as its own HTTP server) but still
504+ // reports its routes to the parent.
505+ func (r * Router ) SubRouter (prefix string ) * Router {
506+ subRouter := New ()
507+ // Create a proxy that notifies the parent when routes are added
508+ // Actually, easier to just wrap the subRouter's routes slice
509+ // but that might be messy with patterns.
510+
511+ // Instead, we can make SubRouter aware of its parent
512+ // or just rely on the fact that SubRouter is empty now,
513+ // and we want it to propagate FUTURE routes too.
514+
515+ // Let's change the approach for SubRouter to support live propagation.
516+ subRouter .routes = r .routes
517+ subRouter .nameIndex = r .nameIndex
518+ subRouter .pathPrefix = r .pathPrefix + prefix
519+ subRouter .namePrefix = r .namePrefix // SubRouter usually doesn't prefix names unless told
520+
521+ r .mux .Mount (prefix , subRouter )
522+ return subRouter
523+ }
524+
435525// findMatchingRoute manually matches a request against registered routes
436526// This is used as a fallback when Chi's RouteContext isn't available (e.g., in global middleware)
437527func (r * Router ) findMatchingRoute (method , path string ) * core.Route {
@@ -576,6 +666,9 @@ func (r *Router) URL(name string, params ...string) (string, error) {
576666 url = strings .ReplaceAll (url , "{" + key + ":.*}" , value )
577667 }
578668
669+ // Remove Go 1.22+ exact match operator {$} for URL generation
670+ url = strings .ReplaceAll (url , "{$}" , "" )
671+
579672 // Check if any parameters remain unreplaced
580673 if strings .Contains (url , "{" ) {
581674 return "" , fmt .Errorf ("missing parameters for route %q" , name )
@@ -625,31 +718,45 @@ type HeaderParam struct {
625718func (r * Router ) Routes () []RouteInfo {
626719 var infos []RouteInfo
627720 for _ , rt := range * r .routes {
628- var queryParams []QueryParam
629- var headerParams []HeaderParam
630- for _ , matcher := range rt .QueryMatchers {
631- switch m := matcher .(type ) {
632- case dispatch.QueryExistsMatcher :
633- queryParams = append (queryParams , QueryParam {Key : m .Key })
634- case dispatch.QueryValueMatcher :
635- queryParams = append (queryParams , QueryParam {Key : m .Key , Value : m .Value })
636- case dispatch.HeaderExistsMatcher :
637- headerParams = append (headerParams , HeaderParam {Key : m .Key })
638- case dispatch.HeaderValueMatcher :
639- headerParams = append (headerParams , HeaderParam {Key : m .Key , Value : m .Value })
640- }
721+ infos = append (infos , transformRoute (rt ))
722+ }
723+ return infos
724+ }
725+
726+ // AggregateRoutes merges routes from multiple routers into a single slice.
727+ // This is useful for unified route listings across routers on different ports.
728+ func AggregateRoutes (routers ... * Router ) []RouteInfo {
729+ var allRoutes []RouteInfo
730+ for _ , r := range routers {
731+ allRoutes = append (allRoutes , r .Routes ()... )
732+ }
733+ return allRoutes
734+ }
735+
736+ func transformRoute (rt * core.Route ) RouteInfo {
737+ var queryParams []QueryParam
738+ var headerParams []HeaderParam
739+ for _ , matcher := range rt .QueryMatchers {
740+ switch m := matcher .(type ) {
741+ case dispatch.QueryExistsMatcher :
742+ queryParams = append (queryParams , QueryParam {Key : m .Key })
743+ case dispatch.QueryValueMatcher :
744+ queryParams = append (queryParams , QueryParam {Key : m .Key , Value : m .Value })
745+ case dispatch.HeaderExistsMatcher :
746+ headerParams = append (headerParams , HeaderParam {Key : m .Key })
747+ case dispatch.HeaderValueMatcher :
748+ headerParams = append (headerParams , HeaderParam {Key : m .Key , Value : m .Value })
641749 }
750+ }
642751
643- infos = append (infos , RouteInfo {
644- Method : rt .Method ,
645- Pattern : rt .Pattern ,
646- Name : rt .Name ,
647- Action : rt .Action ,
648- QueryParams : queryParams ,
649- HeaderParams : headerParams ,
650- })
752+ return RouteInfo {
753+ Method : rt .Method ,
754+ Pattern : rt .Pattern ,
755+ Name : rt .Name ,
756+ Action : rt .Action ,
757+ QueryParams : queryParams ,
758+ HeaderParams : headerParams ,
651759 }
652- return infos
653760}
654761
655762// GetAction retrieves the S3 action from the request context
0 commit comments