Skip to content

Commit 28d72db

Browse files
authored
fix: support mounting dynamic routes (#350)
1 parent e7dbd5d commit 28d72db

File tree

9 files changed

+657
-165
lines changed

9 files changed

+657
-165
lines changed

bricks/dart_frog_dev_server/__brick__/server.dart

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bricks/dart_frog_prod_server/__brick__/build/bin/server.dart

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dart_frog/lib/src/_internal.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:collection';
12
import 'dart:convert';
23
import 'dart:io';
34

packages/dart_frog/lib/src/router.dart

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)