Skip to content

Commit 3d4b688

Browse files
committed
support mounting dynamic routes
1 parent 6f80ea1 commit 3d4b688

File tree

3 files changed

+138
-15
lines changed

3 files changed

+138
-15
lines changed

pkgs/shelf_router/lib/src/router.dart

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,31 +142,84 @@ class Router {
142142

143143
/// Handle all request to [route] using [handler].
144144
void all(String route, Function handler) {
145-
_routes.add(RouterEntry('ALL', route, handler));
145+
_all(route, handler, mounted: false);
146+
}
147+
148+
void _all(String route, Function handler, {required bool mounted}) {
149+
_routes.add(RouterEntry('ALL', route, handler, mounted: mounted));
146150
}
147151

148152
/// Mount a handler below a prefix.
149-
///
150-
/// In this case prefix may not contain any parameters, nor
151-
void mount(String prefix, Handler handler) {
153+
void mount(String prefix, Function handler) {
152154
if (!prefix.startsWith('/')) {
153155
throw ArgumentError.value(prefix, 'prefix', 'must start with a slash');
154156
}
155157

156158
// first slash is always in request.handlerPath
157159
final path = prefix.substring(1);
160+
const pathParam = '__path';
158161
if (prefix.endsWith('/')) {
159-
all(prefix + '<path|[^]*>', (Request request) {
160-
return handler(request.change(path: path));
161-
});
162+
_all(
163+
prefix + '<$pathParam|[^]*>',
164+
(Request request, RouterEntry route) {
165+
// Remove path param from extracted route params
166+
final paramsList = [...route.params]..removeLast();
167+
return _invokeMountedHandler(request, handler, path, paramsList);
168+
},
169+
mounted: true,
170+
);
162171
} else {
163-
all(prefix, (Request request) {
164-
return handler(request.change(path: path));
165-
});
166-
all(prefix + '/<path|[^]*>', (Request request) {
167-
return handler(request.change(path: path + '/'));
168-
});
172+
_all(
173+
prefix,
174+
(Request request, RouterEntry route) {
175+
return _invokeMountedHandler(request, handler, path, route.params);
176+
},
177+
mounted: true,
178+
);
179+
_all(
180+
prefix + '/<$pathParam|[^]*>',
181+
(Request request, RouterEntry route) {
182+
// Remove path param from extracted route params
183+
final paramsList = [...route.params]..removeLast();
184+
return _invokeMountedHandler(
185+
request, handler, path + '/', paramsList);
186+
},
187+
mounted: true,
188+
);
189+
}
190+
}
191+
192+
Future<Response> _invokeMountedHandler(Request request, Function handler,
193+
String path, List<String> paramsList) async {
194+
final params = _getParamsFromRequest(request);
195+
final resolvedPath = _replaceParamsInPath(request, path, params);
196+
197+
return await Function.apply(handler, [
198+
request.change(path: resolvedPath),
199+
...paramsList.map((n) => params[n]),
200+
]) as Response;
201+
}
202+
203+
Map<String, String> _getParamsFromRequest(Request request) {
204+
return request.context['shelf_router/params'] as Map<String, String>;
205+
}
206+
207+
/// Replaces the variable slots (<someVar>) from [path] with the
208+
/// values from [params]
209+
String _replaceParamsInPath(
210+
Request request,
211+
String path,
212+
Map<String, String> params,
213+
) {
214+
// TODO(davidmartos96): Maybe this could be done in a different way
215+
// to avoid replacing the path N times, N being the number of params
216+
var resolvedPath = path;
217+
for (final paramEntry in params.entries) {
218+
resolvedPath =
219+
resolvedPath.replaceFirst('<${paramEntry.key}>', paramEntry.value);
169220
}
221+
222+
return resolvedPath;
170223
}
171224

172225
/// Route incoming requests to registered handlers.

pkgs/shelf_router/lib/src/router_entry.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class RouterEntry {
3434
final Function _handler;
3535
final Middleware _middleware;
3636

37+
/// This router entry is used
38+
/// as a mount point
39+
final bool _mounted;
40+
3741
/// Expression that the request path must match.
3842
///
3943
/// This also captures any parameters in the route pattern.
@@ -46,13 +50,14 @@ class RouterEntry {
4650
List<String> get params => _params.toList(); // exposed for using generator.
4751

4852
RouterEntry._(this.verb, this.route, this._handler, this._middleware,
49-
this._routePattern, this._params);
53+
this._routePattern, this._params, this._mounted);
5054

5155
factory RouterEntry(
5256
String verb,
5357
String route,
5458
Function handler, {
5559
Middleware? middleware,
60+
bool mounted = false,
5661
}) {
5762
middleware = middleware ?? ((Handler fn) => fn);
5863

@@ -77,7 +82,7 @@ class RouterEntry {
7782
final routePattern = RegExp('^$pattern\$');
7883

7984
return RouterEntry._(
80-
verb, route, handler, middleware, routePattern, params);
85+
verb, route, handler, middleware, routePattern, params, mounted);
8186
}
8287

8388
/// Returns a map from parameter name to value, if the path matches the
@@ -102,9 +107,17 @@ class RouterEntry {
102107
request = request.change(context: {'shelf_router/params': params});
103108

104109
return await _middleware((request) async {
110+
if (_mounted) {
111+
// if this route is mounted, we include
112+
// the route itself as a parameter so
113+
// that the mount can extract the parameters
114+
return await _handler(request, this) as Response;
115+
}
116+
105117
if (_handler is Handler || _params.isEmpty) {
106118
return await _handler(request) as Response;
107119
}
120+
108121
return await Function.apply(_handler, [
109122
request,
110123
..._params.map((n) => params[n]),

pkgs/shelf_router/test/router_test.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,61 @@ void main() {
202202
final b2 = await Router.routeNotFound.readAsString();
203203
expect(b2, b1);
204204
});
205+
206+
test('can mount dynamic routes', () async {
207+
// routes for an [user] to [other]. This nests gets nested
208+
// parameters from previous mounts
209+
Handler createUserToOtherHandler(String user, String other) {
210+
var router = Router();
211+
212+
router.get('/<action>', (Request request, String action) {
213+
return Response.ok('$user to $other: $action');
214+
});
215+
216+
return router;
217+
}
218+
219+
// routes for a specific [user]. The user value
220+
// is extracted from the mount
221+
Handler createUserHandler(String user) {
222+
var router = Router();
223+
224+
router.mount('/to/<other>/', (Request request, String other) {
225+
final r = createUserToOtherHandler(user, other);
226+
return r(request);
227+
});
228+
229+
router.get('/self', (Request request) {
230+
return Response.ok("I'm $user");
231+
});
232+
233+
router.get('/', (Request request) {
234+
return Response.ok('$user root');
235+
});
236+
return router;
237+
}
238+
239+
var app = Router();
240+
app.get('/hello', (Request request) {
241+
return Response.ok('hello-world');
242+
});
243+
244+
app.mount('/users/<user>', (Request request, String user) {
245+
final r = createUserHandler(user);
246+
return r(request);
247+
});
248+
249+
app.all('/<_|[^]*>', (Request request) {
250+
return Response.ok('catch-all-handler');
251+
});
252+
253+
server.mount(app);
254+
255+
expect(await get('/hello'), 'hello-world');
256+
expect(await get('/users/david/to/jake/salutes'), 'david to jake: salutes');
257+
expect(await get('/users/jennifer/to/mary/bye'), 'jennifer to mary: bye');
258+
expect(await get('/users/jennifer/self'), "I'm jennifer");
259+
expect(await get('/users/jake'), 'jake root');
260+
expect(await get('/users/david/no-route'), 'catch-all-handler');
261+
});
205262
}

0 commit comments

Comments
 (0)