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
Prev Previous commit
Next Next commit
mounted dynamic routes simplification
  • Loading branch information
davidmartos96 committed Sep 15, 2022
commit 07e27453d59a707e1169f0fd928dfbc706e92397
98 changes: 32 additions & 66 deletions pkgs/shelf_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -113,6 +113,12 @@ class Router {
final List<RouterEntry> _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
Expand Down Expand Up @@ -156,100 +162,60 @@ 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,
);
} else {
_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<Response> _invokeMountedHandler(Request request, Function handler,
String path, List<ParamInfo> paramInfos) async {
final params = _getParamsFromRequest(request);
final resolvedPath =
_replaceParamsInPath(request, path, params, paramInfos);
Future<Response> _invokeMountedHandler(
Request request, Function handler, List<String> 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<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,
List<ParamInfo> 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].
Expand Down
61 changes: 10 additions & 51 deletions pkgs/shelf_router/lib/src/router_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,13 @@ class RouterEntry {
final RegExp _routePattern;

/// Names for the parameters in the route pattern.
final List<ParamInfo> _paramInfos;

List<ParamInfo> get paramInfos => _paramInfos.toList();
final List<String> _params;

/// List of parameter names in the route pattern.
// exposed for using generator.
List<String> get params => _paramInfos.map((p) => p.name).toList();
List<String> 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,
Expand All @@ -69,26 +66,12 @@ class RouterEntry {
route, 'route', 'expected route to start with a slash');
}

final params = <ParamInfo>[];
final params = <String>[];
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');
Expand All @@ -112,10 +95,9 @@ class RouterEntry {
}
// Construct map from parameter name to matched value
var params = <String, String>{};
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;
}
Expand All @@ -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,
});
}