From d6f769f025355580db20332ce9c6bf71f82acb7e Mon Sep 17 00:00:00 2001 From: David Martos Date: Sat, 10 Sep 2022 20:53:15 +0200 Subject: [PATCH 01/12] support mounting dynamic routes --- pkgs/shelf_router/lib/src/router.dart | 79 +++++++++++++++++---- pkgs/shelf_router/lib/src/router_entry.dart | 17 ++++- pkgs/shelf_router/test/router_test.dart | 57 +++++++++++++++ 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 720d3c3e..9a12a38f 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -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', (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/', (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 _invokeMountedHandler(Request request, Function handler, + String path, List 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 _getParamsFromRequest(Request request) { + return request.context['shelf_router/params'] as Map; + } + + /// Replaces the variable slots () from [path] with the + /// values from [params] + String _replaceParamsInPath( + Request request, + String path, + Map 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. diff --git a/pkgs/shelf_router/lib/src/router_entry.dart b/pkgs/shelf_router/lib/src/router_entry.dart index e7b0bbbb..dcc6d3a9 100644 --- a/pkgs/shelf_router/lib/src/router_entry.dart +++ b/pkgs/shelf_router/lib/src/router_entry.dart @@ -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. @@ -46,13 +50,14 @@ class RouterEntry { List 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); @@ -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 @@ -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]), diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart index f664f563..a8ce72dd 100644 --- a/pkgs/shelf_router/test/router_test.dart +++ b/pkgs/shelf_router/test/router_test.dart @@ -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('/', (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//', (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/', (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'); + }); } From fc6a2088a9e01b83e6ab58d89d68c9b8c509fe50 Mon Sep 17 00:00:00 2001 From: David Martos Date: Sat, 10 Sep 2022 21:09:01 +0200 Subject: [PATCH 02/12] comment --- pkgs/shelf_router/lib/src/router.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 9a12a38f..451e4bbc 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -157,7 +157,11 @@ class Router { // first slash is always in request.handlerPath final path = prefix.substring(1); + + // Prefix it with two underscores to avoid conflicts + // with user defined path parameters const pathParam = '__path'; + if (prefix.endsWith('/')) { _all( prefix + '<$pathParam|[^]*>', From ebc69e4e2951c0bc8f39d5a5c30e6bf3cb8299ec Mon Sep 17 00:00:00 2001 From: David Martos Date: Sat, 10 Sep 2022 21:10:39 +0200 Subject: [PATCH 03/12] rename variables --- pkgs/shelf_router/test/router_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart index a8ce72dd..617e2d77 100644 --- a/pkgs/shelf_router/test/router_test.dart +++ b/pkgs/shelf_router/test/router_test.dart @@ -222,8 +222,8 @@ void main() { var router = Router(); router.mount('/to//', (Request request, String other) { - final r = createUserToOtherHandler(user, other); - return r(request); + final handler = createUserToOtherHandler(user, other); + return handler(request); }); router.get('/self', (Request request) { @@ -242,8 +242,8 @@ void main() { }); app.mount('/users/', (Request request, String user) { - final r = createUserHandler(user); - return r(request); + final handler = createUserHandler(user); + return handler(request); }); app.all('/<_|[^]*>', (Request request) { From 73f3e28ea4bd1cdabc8ff3ce0558957d05cfd360 Mon Sep 17 00:00:00 2001 From: David Martos Date: Sat, 10 Sep 2022 21:27:01 +0200 Subject: [PATCH 04/12] dart format --- pkgs/shelf_router/lib/src/router.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 451e4bbc..7d78d07f 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -157,11 +157,11 @@ class Router { // first slash is always in request.handlerPath final path = prefix.substring(1); - + // Prefix it with two underscores to avoid conflicts // with user defined path parameters const pathParam = '__path'; - + if (prefix.endsWith('/')) { _all( prefix + '<$pathParam|[^]*>', From 7d4fd13eb04164a642009484136bc4cc64fd3336 Mon Sep 17 00:00:00 2001 From: David Martos Date: Sat, 10 Sep 2022 21:33:16 +0200 Subject: [PATCH 05/12] typo --- pkgs/shelf_router/test/router_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart index 617e2d77..1e137bdf 100644 --- a/pkgs/shelf_router/test/router_test.dart +++ b/pkgs/shelf_router/test/router_test.dart @@ -204,7 +204,7 @@ void main() { }); test('can mount dynamic routes', () async { - // routes for an [user] to [other]. This nests gets nested + // routes for an [user] to [other]. This gets nested // parameters from previous mounts Handler createUserToOtherHandler(String user, String other) { var router = Router(); From cbb346e22206a03d2e0dff82be64fb45bd2c72c1 Mon Sep 17 00:00:00 2001 From: David Martos Date: Sun, 11 Sep 2022 09:39:51 +0200 Subject: [PATCH 06/12] support dynamic mounted routes with regexp --- pkgs/shelf_router/lib/src/router.dart | 52 +++++++++++++----- pkgs/shelf_router/lib/src/router_entry.dart | 61 +++++++++++++++++---- pkgs/shelf_router/test/router_test.dart | 33 +++++++++++ 3 files changed, 122 insertions(+), 24 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 7d78d07f..d6a3ad0b 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -19,7 +19,7 @@ import 'package:http_methods/http_methods.dart'; import 'package:meta/meta.dart' show sealed; import 'package:shelf/shelf.dart'; -import 'router_entry.dart' show RouterEntry; +import 'router_entry.dart' show ParamInfo, RouterEntry; /// Get a URL parameter captured by the [Router]. @Deprecated('Use Request.params instead') @@ -167,7 +167,7 @@ class Router { prefix + '<$pathParam|[^]*>', (Request request, RouterEntry route) { // Remove path param from extracted route params - final paramsList = [...route.params]..removeLast(); + final paramsList = [...route.paramInfos]..removeLast(); return _invokeMountedHandler(request, handler, path, paramsList); }, mounted: true, @@ -176,7 +176,8 @@ class Router { _all( prefix, (Request request, RouterEntry route) { - return _invokeMountedHandler(request, handler, path, route.params); + return _invokeMountedHandler( + request, handler, path, route.paramInfos); }, mounted: true, ); @@ -184,7 +185,7 @@ class Router { prefix + '/<$pathParam|[^]*>', (Request request, RouterEntry route) { // Remove path param from extracted route params - final paramsList = [...route.params]..removeLast(); + final paramsList = [...route.paramInfos]..removeLast(); return _invokeMountedHandler( request, handler, path + '/', paramsList); }, @@ -194,13 +195,14 @@ class Router { } Future _invokeMountedHandler(Request request, Function handler, - String path, List paramsList) async { + String path, List paramInfos) async { final params = _getParamsFromRequest(request); - final resolvedPath = _replaceParamsInPath(request, path, params); + final resolvedPath = + _replaceParamsInPath(request, path, params, paramInfos); return await Function.apply(handler, [ request.change(path: resolvedPath), - ...paramsList.map((n) => params[n]), + ...paramInfos.map((info) => params[info.name]), ]) as Response; } @@ -214,15 +216,37 @@ class Router { Request request, String path, Map params, + List paramInfos, ) { - // 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); + // we iterate the non-resolved path and we write to a StringBuffer + // resolving ther parameters along the way + final resolvedPathBuff = StringBuffer(); + var paramIndex = 0; + var charIndex = 0; + while (charIndex < path.length) { + if (paramIndex < paramInfos.length) { + final paramInfo = paramInfos[paramIndex]; + if (charIndex < paramInfo.startIdx - 1) { + // Add up until the param slot starts + final part = path.substring(charIndex, paramInfo.startIdx - 1); + resolvedPathBuff.write(part); + charIndex += part.length; + } else { + // Add the resolved value of the parameter + final paramName = paramInfo.name; + final paramValue = params[paramName]!; + resolvedPathBuff.write(paramValue); + charIndex = paramInfo.endIdx - 1; + paramIndex++; + } + } else { + // All params looped, so add up until the end of the path + final part = path.substring(charIndex, path.length); + resolvedPathBuff.write(part); + charIndex += part.length; + } } - + var resolvedPath = resolvedPathBuff.toString(); return resolvedPath; } diff --git a/pkgs/shelf_router/lib/src/router_entry.dart b/pkgs/shelf_router/lib/src/router_entry.dart index dcc6d3a9..0ce00b6e 100644 --- a/pkgs/shelf_router/lib/src/router_entry.dart +++ b/pkgs/shelf_router/lib/src/router_entry.dart @@ -44,13 +44,16 @@ class RouterEntry { final RegExp _routePattern; /// Names for the parameters in the route pattern. - final List _params; + final List _paramInfos; + + List get paramInfos => _paramInfos.toList(); /// List of parameter names in the route pattern. - List get params => _params.toList(); // exposed for using generator. + // exposed for using generator. + List get params => _paramInfos.map((p) => p.name).toList(); RouterEntry._(this.verb, this.route, this._handler, this._middleware, - this._routePattern, this._params, this._mounted); + this._routePattern, this._paramInfos, this._mounted); factory RouterEntry( String verb, @@ -66,12 +69,26 @@ class RouterEntry { route, 'route', 'expected route to start with a slash'); } - final params = []; + final params = []; var pattern = ''; + // Keep the index where the matches are located + // so that we can calculate the positioning of + // the extracted parameter + var prevMatchIndex = 0; for (var m in _parser.allMatches(route)) { - pattern += RegExp.escape(m[1]!); + final firstGroup = m[1]!; + pattern += RegExp.escape(firstGroup); if (m[2] != null) { - params.add(m[2]!); + final paramName = m[2]!; + final startIdx = prevMatchIndex + firstGroup.length; + final paramInfo = ParamInfo( + name: paramName, + startIdx: startIdx, + endIdx: m.end, + ); + params.add(paramInfo); + prevMatchIndex = m.end; + if (m[3] != null && !_isNoCapture(m[3]!)) { throw ArgumentError.value( route, 'route', 'expression for "${m[2]}" is capturing'); @@ -95,9 +112,10 @@ class RouterEntry { } // Construct map from parameter name to matched value var params = {}; - for (var i = 0; i < _params.length; i++) { + for (var i = 0; i < _paramInfos.length; i++) { // first group is always the full match, we ignore this group. - params[_params[i]] = m[i + 1]!; + final paramInfo = _paramInfos[i]; + params[paramInfo.name] = m[i + 1]!; } return params; } @@ -114,14 +132,37 @@ class RouterEntry { return await _handler(request, this) as Response; } - if (_handler is Handler || _params.isEmpty) { + if (_handler is Handler || _paramInfos.isEmpty) { return await _handler(request) as Response; } return await Function.apply(_handler, [ request, - ..._params.map((n) => params[n]), + ..._paramInfos.map((info) => params[info.name]), ]) as Response; })(request); } } + +/// This class holds information about a parameter extracted +/// from the route path. +/// The indexes can by used by the mount logic to resolve the +/// parametrized path when handling the request. +class ParamInfo { + /// This is the name of the parameter, without <, > + final String name; + + /// The index in the route String where the parameter + /// expression starts (inclusive) + final int startIdx; + + /// The index in the route String where the parameter + /// expression ends (exclusive) + final int endIdx; + + const ParamInfo({ + required this.name, + required this.startIdx, + required this.endIdx, + }); +} diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart index 1e137bdf..93a00646 100644 --- a/pkgs/shelf_router/test/router_test.dart +++ b/pkgs/shelf_router/test/router_test.dart @@ -259,4 +259,37 @@ void main() { expect(await get('/users/jake'), 'jake root'); expect(await get('/users/david/no-route'), 'catch-all-handler'); }); + + test('can mount dynamic routes with multiple parameters', () async { + var app = Router(); + app.mount(r'/first//third//last', + (Request request, String second, String fourthNum) { + var router = Router(); + router.get('/', (r) => Response.ok('$second ${int.parse(fourthNum)}')); + return router(request); + }); + + server.mount(app); + + expect(await get('/first/hello/third/12/last'), 'hello 12'); + }); + + test('can mount dynamic routes with regexp', () async { + var app = Router(); + + app.mount(r'/before//after', (Request request, String bookId) { + var router = Router(); + router.get('/', (r) => Response.ok('book ${int.parse(bookId)}')); + return router(request); + }); + + app.all('/<_|[^]*>', (Request request) { + return Response.ok('catch-all-handler'); + }); + + server.mount(app); + + expect(await get('/before/123/after'), 'book 123'); + expect(await get('/before/abc/after'), 'catch-all-handler'); + }); } From 07e27453d59a707e1169f0fd928dfbc706e92397 Mon Sep 17 00:00:00 2001 From: David Martos Date: Tue, 13 Sep 2022 09:50:20 +0200 Subject: [PATCH 07/12] mounted dynamic routes simplification --- pkgs/shelf_router/lib/src/router.dart | 98 +++++++-------------- pkgs/shelf_router/lib/src/router_entry.dart | 61 +++---------- 2 files changed, 42 insertions(+), 117 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index d6a3ad0b..7153deec 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -19,7 +19,7 @@ import 'package:http_methods/http_methods.dart'; import 'package:meta/meta.dart' show sealed; import 'package:shelf/shelf.dart'; -import 'router_entry.dart' show ParamInfo, RouterEntry; +import 'router_entry.dart' show RouterEntry; /// Get a URL parameter captured by the [Router]. @Deprecated('Use Request.params instead') @@ -113,6 +113,12 @@ class Router { final List _routes = []; final Handler _notFoundHandler; + /// Name of the parameter used for matching the rest of te path in a mounted + /// route. + /// Prefixed with two underscores to avoid conflicts + /// with user defined path parameters + static const _kRestPathParam = '__path'; + /// Creates a new [Router] routing requests to handlers. /// /// The [notFoundHandler] will be invoked for requests where no matching route @@ -156,19 +162,15 @@ class Router { } // first slash is always in request.handlerPath - final path = prefix.substring(1); - - // Prefix it with two underscores to avoid conflicts - // with user defined path parameters - const pathParam = '__path'; + const restPathParam = _kRestPathParam; if (prefix.endsWith('/')) { _all( - prefix + '<$pathParam|[^]*>', + prefix + '<$restPathParam|[^]*>', (Request request, RouterEntry route) { // Remove path param from extracted route params - final paramsList = [...route.paramInfos]..removeLast(); - return _invokeMountedHandler(request, handler, path, paramsList); + final paramsList = [...route.params]..removeLast(); + return _invokeMountedHandler(request, handler, paramsList); }, mounted: true, ); @@ -176,80 +178,44 @@ class Router { _all( prefix, (Request request, RouterEntry route) { - return _invokeMountedHandler( - request, handler, path, route.paramInfos); + return _invokeMountedHandler(request, handler, route.params); }, mounted: true, ); _all( - prefix + '/<$pathParam|[^]*>', + prefix + '/<$restPathParam|[^]*>', (Request request, RouterEntry route) { // Remove path param from extracted route params - final paramsList = [...route.paramInfos]..removeLast(); - return _invokeMountedHandler( - request, handler, path + '/', paramsList); + final paramsList = [...route.params]..removeLast(); + return _invokeMountedHandler(request, handler, paramsList); }, mounted: true, ); } } - Future _invokeMountedHandler(Request request, Function handler, - String path, List paramInfos) async { - final params = _getParamsFromRequest(request); - final resolvedPath = - _replaceParamsInPath(request, path, params, paramInfos); + Future _invokeMountedHandler( + Request request, Function handler, List pathParams) async { + final paramsMap = request.params; + + final pathParamSegment = paramsMap[_kRestPathParam]; + final urlPath = request.url.path; + late final String effectivePath; + if (pathParamSegment != null && pathParamSegment.isNotEmpty) { + /// If we encounter the "rest path" parameter we remove it + /// from the request path that shelf will handle. + effectivePath = + urlPath.substring(0, urlPath.length - pathParamSegment.length); + } else { + effectivePath = urlPath; + } return await Function.apply(handler, [ - request.change(path: resolvedPath), - ...paramInfos.map((info) => params[info.name]), + request.change(path: effectivePath), + ...pathParams.map((param) => paramsMap[param]), ]) as Response; } - Map _getParamsFromRequest(Request request) { - return request.context['shelf_router/params'] as Map; - } - - /// Replaces the variable slots () from [path] with the - /// values from [params] - String _replaceParamsInPath( - Request request, - String path, - Map params, - List paramInfos, - ) { - // we iterate the non-resolved path and we write to a StringBuffer - // resolving ther parameters along the way - final resolvedPathBuff = StringBuffer(); - var paramIndex = 0; - var charIndex = 0; - while (charIndex < path.length) { - if (paramIndex < paramInfos.length) { - final paramInfo = paramInfos[paramIndex]; - if (charIndex < paramInfo.startIdx - 1) { - // Add up until the param slot starts - final part = path.substring(charIndex, paramInfo.startIdx - 1); - resolvedPathBuff.write(part); - charIndex += part.length; - } else { - // Add the resolved value of the parameter - final paramName = paramInfo.name; - final paramValue = params[paramName]!; - resolvedPathBuff.write(paramValue); - charIndex = paramInfo.endIdx - 1; - paramIndex++; - } - } else { - // All params looped, so add up until the end of the path - final part = path.substring(charIndex, path.length); - resolvedPathBuff.write(part); - charIndex += part.length; - } - } - var resolvedPath = resolvedPathBuff.toString(); - return resolvedPath; - } - /// Route incoming requests to registered handlers. /// /// This method allows a Router instance to be a [Handler]. diff --git a/pkgs/shelf_router/lib/src/router_entry.dart b/pkgs/shelf_router/lib/src/router_entry.dart index 0ce00b6e..dcc6d3a9 100644 --- a/pkgs/shelf_router/lib/src/router_entry.dart +++ b/pkgs/shelf_router/lib/src/router_entry.dart @@ -44,16 +44,13 @@ class RouterEntry { final RegExp _routePattern; /// Names for the parameters in the route pattern. - final List _paramInfos; - - List get paramInfos => _paramInfos.toList(); + final List _params; /// List of parameter names in the route pattern. - // exposed for using generator. - List get params => _paramInfos.map((p) => p.name).toList(); + List get params => _params.toList(); // exposed for using generator. RouterEntry._(this.verb, this.route, this._handler, this._middleware, - this._routePattern, this._paramInfos, this._mounted); + this._routePattern, this._params, this._mounted); factory RouterEntry( String verb, @@ -69,26 +66,12 @@ class RouterEntry { route, 'route', 'expected route to start with a slash'); } - final params = []; + final params = []; var pattern = ''; - // Keep the index where the matches are located - // so that we can calculate the positioning of - // the extracted parameter - var prevMatchIndex = 0; for (var m in _parser.allMatches(route)) { - final firstGroup = m[1]!; - pattern += RegExp.escape(firstGroup); + pattern += RegExp.escape(m[1]!); if (m[2] != null) { - final paramName = m[2]!; - final startIdx = prevMatchIndex + firstGroup.length; - final paramInfo = ParamInfo( - name: paramName, - startIdx: startIdx, - endIdx: m.end, - ); - params.add(paramInfo); - prevMatchIndex = m.end; - + params.add(m[2]!); if (m[3] != null && !_isNoCapture(m[3]!)) { throw ArgumentError.value( route, 'route', 'expression for "${m[2]}" is capturing'); @@ -112,10 +95,9 @@ class RouterEntry { } // Construct map from parameter name to matched value var params = {}; - for (var i = 0; i < _paramInfos.length; i++) { + for (var i = 0; i < _params.length; i++) { // first group is always the full match, we ignore this group. - final paramInfo = _paramInfos[i]; - params[paramInfo.name] = m[i + 1]!; + params[_params[i]] = m[i + 1]!; } return params; } @@ -132,37 +114,14 @@ class RouterEntry { return await _handler(request, this) as Response; } - if (_handler is Handler || _paramInfos.isEmpty) { + if (_handler is Handler || _params.isEmpty) { return await _handler(request) as Response; } return await Function.apply(_handler, [ request, - ..._paramInfos.map((info) => params[info.name]), + ..._params.map((n) => params[n]), ]) as Response; })(request); } } - -/// This class holds information about a parameter extracted -/// from the route path. -/// The indexes can by used by the mount logic to resolve the -/// parametrized path when handling the request. -class ParamInfo { - /// This is the name of the parameter, without <, > - final String name; - - /// The index in the route String where the parameter - /// expression starts (inclusive) - final int startIdx; - - /// The index in the route String where the parameter - /// expression ends (exclusive) - final int endIdx; - - const ParamInfo({ - required this.name, - required this.startIdx, - required this.endIdx, - }); -} From c1378fc647e8cec95af2b73dded8ca8360e77371 Mon Sep 17 00:00:00 2001 From: David Martos Date: Tue, 13 Sep 2022 10:04:42 +0200 Subject: [PATCH 08/12] some comments --- pkgs/shelf_router/lib/src/router.dart | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 7153deec..f9a9e7e0 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -197,9 +197,21 @@ class Router { Future _invokeMountedHandler( Request request, Function handler, List pathParams) async { final paramsMap = request.params; + final effectivePath = _replaceParamsInPath(request.url.path, paramsMap); + return await Function.apply(handler, [ + request.change(path: effectivePath), + ...pathParams.map((param) => paramsMap[param]), + ]) as Response; + } + + /// Replaces the variable slots () from [path] with the + /// values from [paramsMap] + String _replaceParamsInPath( + String urlPath, + Map paramsMap, + ) { final pathParamSegment = paramsMap[_kRestPathParam]; - final urlPath = request.url.path; late final String effectivePath; if (pathParamSegment != null && pathParamSegment.isNotEmpty) { /// If we encounter the "rest path" parameter we remove it @@ -207,13 +219,10 @@ class Router { effectivePath = urlPath.substring(0, urlPath.length - pathParamSegment.length); } else { + // No parameters in the requested path effectivePath = urlPath; } - - return await Function.apply(handler, [ - request.change(path: effectivePath), - ...pathParams.map((param) => paramsMap[param]), - ]) as Response; + return effectivePath; } /// Route incoming requests to registered handlers. From b75065c727a4e1ed8ef00f09e87684e86d72d16c Mon Sep 17 00:00:00 2001 From: David Martos Date: Tue, 13 Sep 2022 10:18:04 +0200 Subject: [PATCH 09/12] update docstring --- pkgs/shelf_router/lib/src/router.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index f9a9e7e0..ed1c3dc3 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -197,7 +197,7 @@ class Router { Future _invokeMountedHandler( Request request, Function handler, List pathParams) async { final paramsMap = request.params; - final effectivePath = _replaceParamsInPath(request.url.path, paramsMap); + final effectivePath = _getEffectiveMountPath(request.url.path, paramsMap); return await Function.apply(handler, [ request.change(path: effectivePath), @@ -205,9 +205,10 @@ class Router { ]) as Response; } - /// Replaces the variable slots () from [path] with the - /// values from [paramsMap] - String _replaceParamsInPath( + /// Removes the "rest path" from the requested url in mounted routes. This + /// new path is then used to update the scope of the mounted handler with + /// [Request.change] + String _getEffectiveMountPath( String urlPath, Map paramsMap, ) { From d1db0742cf556aee5eeb7c18525504e19c32c379 Mon Sep 17 00:00:00 2001 From: David Martos Date: Tue, 13 Sep 2022 15:36:25 +0200 Subject: [PATCH 10/12] update docstring --- pkgs/shelf_router/lib/src/router.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index ed1c3dc3..86d7036c 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -205,8 +205,8 @@ class Router { ]) as Response; } - /// Removes the "rest path" from the requested url in mounted routes. This - /// new path is then used to update the scope of the mounted handler with + /// Removes the "rest path" from the requested [urlPath] in mounted routes. + /// This new path is then used to update the scope of the mounted handler with /// [Request.change] String _getEffectiveMountPath( String urlPath, From 1a7d4dcdc7a8156b3275543cca664b002930148d Mon Sep 17 00:00:00 2001 From: David Martos Date: Wed, 14 Sep 2022 15:40:59 +0200 Subject: [PATCH 11/12] don't create Routers when handling the request --- pkgs/shelf_router/lib/src/router.dart | 68 ++++++++++++++-- pkgs/shelf_router/lib/src/router_entry.dart | 29 ++++--- pkgs/shelf_router/test/router_test.dart | 86 +++++++++++++-------- 3 files changed, 134 insertions(+), 49 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 86d7036c..0a8923d9 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -56,6 +56,40 @@ extension RouterParams on Request { } return _emptyParams; } + + /// Get URL parameters captured by the [Router.mount]. + /// They can be accessed from inside the mounted routes. + /// + /// **Example** + /// ```dart + /// Router createUsersRouter() { + /// var router = Router(); + /// + /// String getUser(Request r) => r.mountedParams['user']!; + /// + /// router.get('/self', (Request request) { + /// return Response.ok("I'm ${getUser(request)}"); + /// }); + /// + /// return router; + /// } + /// + /// var app = Router(); + /// + /// final usersRouter = createUsersRouter(); + /// app.mount('/users/', (Request r, String user) => usersRouter(r)); + /// ``` + /// + /// If no parameters are captured this returns an empty map. + /// + /// The returned map is unmodifiable. + Map get mountedParams { + final p = context['shelf_router/mountedParams']; + if (p is Map) { + return UnmodifiableMapView(p); + } + return _emptyParams; + } } /// Middleware to remove body from request. @@ -148,11 +182,17 @@ class Router { /// Handle all request to [route] using [handler]. void all(String route, Function handler) { - _all(route, handler, mounted: false); + _all(route, handler, applyParamsOnHandle: true); } - void _all(String route, Function handler, {required bool mounted}) { - _routes.add(RouterEntry('ALL', route, handler, mounted: mounted)); + void _all(String route, Function handler, + {required bool applyParamsOnHandle}) { + _routes.add(RouterEntry( + 'ALL', + route, + handler, + applyParamsOnHandle: applyParamsOnHandle, + )); } /// Mount a handler below a prefix. @@ -161,7 +201,6 @@ class Router { throw ArgumentError.value(prefix, 'prefix', 'must start with a slash'); } - // first slash is always in request.handlerPath const restPathParam = _kRestPathParam; if (prefix.endsWith('/')) { @@ -172,7 +211,7 @@ class Router { final paramsList = [...route.params]..removeLast(); return _invokeMountedHandler(request, handler, paramsList); }, - mounted: true, + applyParamsOnHandle: false, ); } else { _all( @@ -180,7 +219,7 @@ class Router { (Request request, RouterEntry route) { return _invokeMountedHandler(request, handler, route.params); }, - mounted: true, + applyParamsOnHandle: false, ); _all( prefix + '/<$restPathParam|[^]*>', @@ -189,7 +228,7 @@ class Router { final paramsList = [...route.params]..removeLast(); return _invokeMountedHandler(request, handler, paramsList); }, - mounted: true, + applyParamsOnHandle: false, ); } } @@ -199,8 +238,21 @@ class Router { final paramsMap = request.params; final effectivePath = _getEffectiveMountPath(request.url.path, paramsMap); + final modifiedRequest = request.change( + path: effectivePath, + context: { + // Include the parameters captured here as mounted parameters. + // We also include previous mounted params in case there is double + // nesting of `mount`s + 'shelf_router/mountedParams': { + ...request.mountedParams, + ...paramsMap, + }, + }, + ); + return await Function.apply(handler, [ - request.change(path: effectivePath), + modifiedRequest, ...pathParams.map((param) => paramsMap[param]), ]) as Response; } diff --git a/pkgs/shelf_router/lib/src/router_entry.dart b/pkgs/shelf_router/lib/src/router_entry.dart index dcc6d3a9..fae4e760 100644 --- a/pkgs/shelf_router/lib/src/router_entry.dart +++ b/pkgs/shelf_router/lib/src/router_entry.dart @@ -34,9 +34,13 @@ class RouterEntry { final Function _handler; final Middleware _middleware; - /// This router entry is used - /// as a mount point - final bool _mounted; + /// If the arguments should be applied or not to the handler function. + /// This is useful to have as false when there is + /// internal logic that registers routes and the number of expected arguments + /// by the user is unknown. i.e: [Router.mount] + /// When this is false, this [RouterEntry] is provided as an argument along + /// the [Request] so that the caller can read information from the route. + final bool _applyParamsOnHandle; /// Expression that the request path must match. /// @@ -50,14 +54,14 @@ class RouterEntry { List get params => _params.toList(); // exposed for using generator. RouterEntry._(this.verb, this.route, this._handler, this._middleware, - this._routePattern, this._params, this._mounted); + this._routePattern, this._params, this._applyParamsOnHandle); factory RouterEntry( String verb, String route, Function handler, { Middleware? middleware, - bool mounted = false, + bool applyParamsOnHandle = true, }) { middleware = middleware ?? ((Handler fn) => fn); @@ -82,7 +86,14 @@ class RouterEntry { final routePattern = RegExp('^$pattern\$'); return RouterEntry._( - verb, route, handler, middleware, routePattern, params, mounted); + verb, + route, + handler, + middleware, + routePattern, + params, + applyParamsOnHandle, + ); } /// Returns a map from parameter name to value, if the path matches the @@ -107,10 +118,8 @@ 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 + if (!_applyParamsOnHandle) { + // We handle the request just providing this route return await _handler(request, this) as Response; } diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart index 93a00646..def93336 100644 --- a/pkgs/shelf_router/test/router_test.dart +++ b/pkgs/shelf_router/test/router_test.dart @@ -204,34 +204,40 @@ void main() { }); test('can mount dynamic routes', () async { - // routes for an [user] to [other]. This gets nested - // parameters from previous mounts - Handler createUserToOtherHandler(String user, String other) { + // routes for a specific [user]. The user value + // is extracted from the mount + Router createUsersRouter() { var router = Router(); - router.get('/', (Request request, String action) { - return Response.ok('$user to $other: $action'); - }); + String getUser(Request r) => r.mountedParams['user']!; - return router; - } + // Nested mount + // Routes for an [user] to [other]. This gets nested + // parameters from previous mounts + Router createUserToOtherRouter() { + var router = Router(); - // routes for a specific [user]. The user value - // is extracted from the mount - Handler createUserHandler(String user) { - var router = Router(); + String getOtherUser(Request r) => r.mountedParams['other']!; - router.mount('/to//', (Request request, String other) { - final handler = createUserToOtherHandler(user, other); - return handler(request); - }); + router.get('/', (Request request, String action) { + return Response.ok( + '${getUser(request)} to ${getOtherUser(request)}: $action', + ); + }); + + return router; + } + + final userToOtherRouter = createUserToOtherRouter(); + router.mount( + '/to//', (Request r, String other) => userToOtherRouter(r)); router.get('/self', (Request request) { - return Response.ok("I'm $user"); + return Response.ok("I'm ${getUser(request)}"); }); router.get('/', (Request request) { - return Response.ok('$user root'); + return Response.ok('${getUser(request)} root'); }); return router; } @@ -241,10 +247,8 @@ void main() { return Response.ok('hello-world'); }); - app.mount('/users/', (Request request, String user) { - final handler = createUserHandler(user); - return handler(request); - }); + final usersRouter = createUsersRouter(); + app.mount('/users/', (Request r, String user) => usersRouter(r)); app.all('/<_|[^]*>', (Request request) { return Response.ok('catch-all-handler'); @@ -262,12 +266,24 @@ void main() { test('can mount dynamic routes with multiple parameters', () async { var app = Router(); - app.mount(r'/first//third//last', - (Request request, String second, String fourthNum) { + + final mountedRouter = () { var router = Router(); - router.get('/', (r) => Response.ok('$second ${int.parse(fourthNum)}')); - return router(request); - }); + + String getSecond(Request r) => r.mountedParams['second']!; + int getFourth(Request r) => int.parse(r.mountedParams['fourth']!); + + router.get( + '/', + (Request r) => Response.ok('${getSecond(r)} ${getFourth(r)}'), + ); + return router; + }(); + + app.mount( + r'/first//third//last', + (Request r, String second, String fourth) => mountedRouter(r), + ); server.mount(app); @@ -277,11 +293,19 @@ void main() { test('can mount dynamic routes with regexp', () async { var app = Router(); - app.mount(r'/before//after', (Request request, String bookId) { + final mountedRouter = () { var router = Router(); - router.get('/', (r) => Response.ok('book ${int.parse(bookId)}')); - return router(request); - }); + + int getBookId(Request r) => int.parse(r.mountedParams['bookId']!); + + router.get('/', (Request r) => Response.ok('book ${getBookId(r)}')); + return router; + }(); + + app.mount( + r'/before//after', + (Request r, String bookId) => mountedRouter(r), + ); app.all('/<_|[^]*>', (Request request) { return Response.ok('catch-all-handler'); From c1d4d07c881ebf2ef1f3e834baf263c1ace9a765 Mon Sep 17 00:00:00 2001 From: David Martos Date: Thu, 15 Sep 2022 09:30:56 +0200 Subject: [PATCH 12/12] lints --- pkgs/shelf_router/lib/src/router.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart index 0a8923d9..380863db 100644 --- a/pkgs/shelf_router/lib/src/router.dart +++ b/pkgs/shelf_router/lib/src/router.dart @@ -205,7 +205,7 @@ class Router { if (prefix.endsWith('/')) { _all( - prefix + '<$restPathParam|[^]*>', + '$prefix<$restPathParam|[^]*>', (Request request, RouterEntry route) { // Remove path param from extracted route params final paramsList = [...route.params]..removeLast(); @@ -222,7 +222,7 @@ class Router { applyParamsOnHandle: false, ); _all( - prefix + '/<$restPathParam|[^]*>', + '$prefix/<$restPathParam|[^]*>', (Request request, RouterEntry route) { // Remove path param from extracted route params final paramsList = [...route.params]..removeLast();