@@ -39,6 +39,13 @@ class Router {
3939 Router ({Handler notFoundHandler = _defaultNotFound})
4040 : _notFoundHandler = notFoundHandler;
4141
42+ /// Name of the parameter used for matching
43+ /// the rest of the path in a mounted route.
44+ ///
45+ /// Two underscore prefix to avoid conflicts
46+ /// with user-defined path parameters.
47+ static const _kMountedPathParamRest = '__path' ;
48+
4249 final List <RouterEntry > _routes = [];
4350 final Handler _notFoundHandler;
4451
@@ -65,37 +72,83 @@ class Router {
6572
6673 /// Handle all request to [route] using [handler] .
6774 void all (String route, Function handler) {
68- _routes.add (RouterEntry ('ALL' , route, handler));
75+ _all (route, handler, mounted: false );
76+ }
77+
78+ void _all (String route, Function handler, {required bool mounted}) {
79+ _routes.add (RouterEntry ('ALL' , route, handler, mounted: mounted));
6980 }
7081
7182 /// Mount a handler below a prefix.
72- ///
73- /// In this case prefix may not contain any parameters, nor
74- void mount (String prefix, Handler handler) {
83+ void mount (String prefix, Function handler) {
7584 if (! prefix.startsWith ('/' )) {
7685 throw ArgumentError .value (prefix, 'prefix' , 'must start with a slash' );
7786 }
7887
79- // The first slash is always in request.handlerPath
80- final path = prefix.substring (1 );
8188 if (prefix.endsWith ('/' )) {
82- all ('$prefix <path|[^]*>' , (RequestContext context) {
83- return handler (
84- RequestContext ._(context.request._request.change (path: path)),
85- );
86- });
89+ _all (
90+ '$prefix <$_kMountedPathParamRest |[^]*>' ,
91+ (RequestContext context, List <String > params) {
92+ return _invokeMountedHandler (
93+ context,
94+ handler,
95+ // Remove path param from extracted route params
96+ [...params]..removeLast (),
97+ );
98+ },
99+ mounted: true ,
100+ );
101+ } else {
102+ _all (
103+ prefix,
104+ (RequestContext context, List <String > params) {
105+ return _invokeMountedHandler (context, handler, params);
106+ },
107+ mounted: true ,
108+ );
109+ _all (
110+ '$prefix /<$_kMountedPathParamRest |[^]*>' ,
111+ (RequestContext context, List <String > params) {
112+ return _invokeMountedHandler (
113+ context,
114+ handler,
115+ // Remove path param from extracted route params
116+ [...params]..removeLast (),
117+ );
118+ },
119+ mounted: true ,
120+ );
121+ }
122+ }
123+
124+ Future <Response > _invokeMountedHandler (
125+ RequestContext context,
126+ Function handler,
127+ List <String > pathParams,
128+ ) async {
129+ final request = context.request;
130+ final params = request._request.params;
131+ final pathParamSegment = params[_kMountedPathParamRest];
132+ final urlPath = request.url.path;
133+ late final String effectivePath;
134+ if (pathParamSegment != null && pathParamSegment.isNotEmpty) {
135+ /// If we encounter the `_kMountedPathParamRest` parameter we remove it
136+ /// from the request path that shelf will handle.
137+ effectivePath = urlPath.substring (
138+ 0 ,
139+ urlPath.length - pathParamSegment.length,
140+ );
87141 } else {
88- all (prefix, (RequestContext context) {
89- return handler (
90- RequestContext ._(context.request._request.change (path: path)),
91- );
92- });
93- all ('$prefix /<path|[^]*>' , (RequestContext context) {
94- return handler (
95- RequestContext ._(context.request._request.change (path: '$path /' )),
96- );
97- });
142+ effectivePath = urlPath;
98143 }
144+ final modifiedRequestContext = RequestContext ._(
145+ request._request.change (path: effectivePath),
146+ );
147+
148+ return await Function .apply (handler, [
149+ modifiedRequestContext,
150+ ...pathParams.map ((param) => params[param]),
151+ ]) as Response ;
99152 }
100153
101154 /// Route incoming requests to registered handlers.
@@ -196,6 +249,7 @@ class RouterEntry {
196249 String route,
197250 Function handler, {
198251 Middleware ? middleware,
252+ bool mounted = false ,
199253 }) {
200254 middleware = middleware ?? ((Handler fn) => fn);
201255
@@ -233,6 +287,7 @@ class RouterEntry {
233287 middleware,
234288 routePattern,
235289 params,
290+ mounted,
236291 );
237292 }
238293
@@ -243,6 +298,7 @@ class RouterEntry {
243298 this ._middleware,
244299 this ._routePattern,
245300 this ._params,
301+ this ._mounted,
246302 );
247303
248304 /// Pattern for parsing the route pattern
@@ -253,14 +309,19 @@ class RouterEntry {
253309 final Function _handler;
254310 final Middleware _middleware;
255311
312+ /// Indicates this entry is used as a mounting point.
313+ final bool _mounted;
314+
256315 /// Expression that the request path must match.
257316 ///
258317 /// This also captures any parameters in the route pattern.
259318 final RegExp _routePattern;
260319
261- /// Names for the parameters in the route pattern.
262320 final List <String > _params;
263321
322+ /// Names for the parameters in the route pattern.
323+ List <String > get params => _params.toList ();
324+
264325 /// Returns a map from parameter name to value, if the path matches the
265326 /// route pattern. Otherwise returns null.
266327 Map <String , String >? match (String path) {
@@ -287,6 +348,13 @@ class RouterEntry {
287348 final _context = RequestContext ._(request);
288349
289350 return await _middleware ((request) async {
351+ if (_mounted) {
352+ // if this route is mounted, we include
353+ // the route entry params so that the mount can extract the parameters/
354+ // ignore: avoid_dynamic_calls
355+ return await _handler (_context, this .params) as Response ;
356+ }
357+
290358 if (_handler is Handler || _params.isEmpty) {
291359 // ignore: avoid_dynamic_calls
292360 return await _handler (_context) as Response ;
@@ -300,3 +368,21 @@ class RouterEntry {
300368 })(_context);
301369 }
302370}
371+
372+ final _emptyParams = UnmodifiableMapView (< String , String > {});
373+
374+ /// Extension on [shelf.Request] which provides access to
375+ /// URL parameters captured by the [Router] .
376+ extension RouterParams on shelf.Request {
377+ /// Get URL parameters captured by the [Router] .
378+ /// If no parameters are captured this returns an empty map.
379+ ///
380+ /// The returned map is unmodifiable.
381+ Map <String , String > get params {
382+ final p = context['shelf_router/params' ];
383+ if (p is Map <String , String >) {
384+ return UnmodifiableMapView (p);
385+ }
386+ return _emptyParams;
387+ }
388+ }
0 commit comments