Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support mounting dynamic routes. FIxes #250 #288

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Next Next commit
support mounting dynamic routes
  • Loading branch information
davidmartos96 committed Sep 15, 2022
commit d6f769f025355580db20332ce9c6bf71f82acb7e
79 changes: 66 additions & 13 deletions pkgs/shelf_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,31 +142,84 @@ class Router {

/// Handle all request to [route] using [handler].
void all(String route, Function handler) {
_routes.add(RouterEntry('ALL', route, handler));
_all(route, handler, mounted: false);
}

void _all(String route, Function handler, {required bool mounted}) {
_routes.add(RouterEntry('ALL', route, handler, mounted: mounted));
}

/// Mount a handler below a prefix.
///
/// In this case prefix may not contain any parameters, nor
void mount(String prefix, Handler handler) {
void mount(String prefix, Function handler) {
if (!prefix.startsWith('/')) {
throw ArgumentError.value(prefix, 'prefix', 'must start with a slash');
}

// first slash is always in request.handlerPath
final path = prefix.substring(1);
const pathParam = '__path';
if (prefix.endsWith('/')) {
all('$prefix<path|[^]*>', (Request request) {
return handler(request.change(path: path));
});
_all(
prefix + '<$pathParam|[^]*>',
(Request request, RouterEntry route) {
// Remove path param from extracted route params
final paramsList = [...route.params]..removeLast();
return _invokeMountedHandler(request, handler, path, paramsList);
},
mounted: true,
);
} else {
all(prefix, (Request request) {
return handler(request.change(path: path));
});
all('$prefix/<path|[^]*>', (Request request) {
return handler(request.change(path: '$path/'));
});
_all(
prefix,
(Request request, RouterEntry route) {
return _invokeMountedHandler(request, handler, path, route.params);
},
mounted: true,
);
_all(
prefix + '/<$pathParam|[^]*>',
(Request request, RouterEntry route) {
// Remove path param from extracted route params
final paramsList = [...route.params]..removeLast();
return _invokeMountedHandler(
request, handler, path + '/', paramsList);
},
mounted: true,
);
}
}

Future<Response> _invokeMountedHandler(Request request, Function handler,
String path, List<String> paramsList) async {
final params = _getParamsFromRequest(request);
final resolvedPath = _replaceParamsInPath(request, path, params);

return await Function.apply(handler, [
request.change(path: resolvedPath),
...paramsList.map((n) => params[n]),
]) as Response;
}

Map<String, String> _getParamsFromRequest(Request request) {
return request.context['shelf_router/params'] as Map<String, String>;
}

/// Replaces the variable slots (<someVar>) from [path] with the
/// values from [params]
String _replaceParamsInPath(
Request request,
String path,
Map<String, String> params,
) {
// TODO(davidmartos96): Maybe this could be done in a different way
// to avoid replacing the path N times, N being the number of params
var resolvedPath = path;
for (final paramEntry in params.entries) {
resolvedPath =
resolvedPath.replaceFirst('<${paramEntry.key}>', paramEntry.value);
}

return resolvedPath;
}

/// Route incoming requests to registered handlers.
Expand Down
17 changes: 15 additions & 2 deletions pkgs/shelf_router/lib/src/router_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class RouterEntry {
final Function _handler;
final Middleware _middleware;

/// This router entry is used
/// as a mount point
final bool _mounted;

/// Expression that the request path must match.
///
/// This also captures any parameters in the route pattern.
Expand All @@ -46,13 +50,14 @@ class RouterEntry {
List<String> get params => _params.toList(); // exposed for using generator.

RouterEntry._(this.verb, this.route, this._handler, this._middleware,
this._routePattern, this._params);
this._routePattern, this._params, this._mounted);

factory RouterEntry(
String verb,
String route,
Function handler, {
Middleware? middleware,
bool mounted = false,
}) {
middleware = middleware ?? ((Handler fn) => fn);

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

return RouterEntry._(
verb, route, handler, middleware, routePattern, params);
verb, route, handler, middleware, routePattern, params, mounted);
}

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

return await _middleware((request) async {
if (_mounted) {
// if this route is mounted, we include
// the route itself as a parameter so
// that the mount can extract the parameters
return await _handler(request, this) as Response;
}

if (_handler is Handler || _params.isEmpty) {
return await _handler(request) as Response;
}

return await Function.apply(_handler, [
request,
..._params.map((n) => params[n]),
Expand Down
57 changes: 57 additions & 0 deletions pkgs/shelf_router/test/router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,61 @@ void main() {
final b2 = await Router.routeNotFound.readAsString();
expect(b2, b1);
});

test('can mount dynamic routes', () async {
// routes for an [user] to [other]. This nests gets nested
// parameters from previous mounts
Handler createUserToOtherHandler(String user, String other) {
var router = Router();

router.get('/<action>', (Request request, String action) {
return Response.ok('$user to $other: $action');
});

return router;
}

// routes for a specific [user]. The user value
// is extracted from the mount
Handler createUserHandler(String user) {
var router = Router();

router.mount('/to/<other>/', (Request request, String other) {
final r = createUserToOtherHandler(user, other);
return r(request);
});

router.get('/self', (Request request) {
return Response.ok("I'm $user");
});

router.get('/', (Request request) {
return Response.ok('$user root');
});
return router;
}

var app = Router();
app.get('/hello', (Request request) {
return Response.ok('hello-world');
});

app.mount('/users/<user>', (Request request, String user) {
final r = createUserHandler(user);
return r(request);
});

app.all('/<_|[^]*>', (Request request) {
return Response.ok('catch-all-handler');
});

server.mount(app);

expect(await get('/hello'), 'hello-world');
expect(await get('/users/david/to/jake/salutes'), 'david to jake: salutes');
expect(await get('/users/jennifer/to/mary/bye'), 'jennifer to mary: bye');
expect(await get('/users/jennifer/self'), "I'm jennifer");
expect(await get('/users/jake'), 'jake root');
expect(await get('/users/david/no-route'), 'catch-all-handler');
});
}