Skip to content

Commit 4b53481

Browse files
committed
support dynamic mounted routes with regexp
1 parent 874da11 commit 4b53481

File tree

3 files changed

+92
-24
lines changed

3 files changed

+92
-24
lines changed

pkgs/shelf_router/lib/src/router.dart

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import 'package:http_methods/http_methods.dart';
1919
import 'package:meta/meta.dart' show sealed;
2020
import 'package:shelf/shelf.dart';
2121

22-
import 'router_entry.dart' show RouterEntry;
22+
import 'router_entry.dart' show ParamInfo, RouterEntry;
2323

2424
/// Get a URL parameter captured by the [Router].
2525
@Deprecated('Use Request.params instead')
@@ -167,7 +167,7 @@ class Router {
167167
prefix + '<$pathParam|[^]*>',
168168
(Request request, RouterEntry route) {
169169
// Remove path param from extracted route params
170-
final paramsList = [...route.params]..removeLast();
170+
final paramsList = [...route.paramInfos]..removeLast();
171171
return _invokeMountedHandler(request, handler, path, paramsList);
172172
},
173173
mounted: true,
@@ -176,15 +176,16 @@ class Router {
176176
_all(
177177
prefix,
178178
(Request request, RouterEntry route) {
179-
return _invokeMountedHandler(request, handler, path, route.params);
179+
return _invokeMountedHandler(
180+
request, handler, path, route.paramInfos);
180181
},
181182
mounted: true,
182183
);
183184
_all(
184185
prefix + '/<$pathParam|[^]*>',
185186
(Request request, RouterEntry route) {
186187
// Remove path param from extracted route params
187-
final paramsList = [...route.params]..removeLast();
188+
final paramsList = [...route.paramInfos]..removeLast();
188189
return _invokeMountedHandler(
189190
request, handler, path + '/', paramsList);
190191
},
@@ -194,13 +195,14 @@ class Router {
194195
}
195196

196197
Future<Response> _invokeMountedHandler(Request request, Function handler,
197-
String path, List<String> paramsList) async {
198+
String path, List<ParamInfo> paramInfos) async {
198199
final params = _getParamsFromRequest(request);
199-
final resolvedPath = _replaceParamsInPath(request, path, params);
200+
final resolvedPath =
201+
_replaceParamsInPath(request, path, params, paramInfos);
200202

201203
return await Function.apply(handler, [
202204
request.change(path: resolvedPath),
203-
...paramsList.map((n) => params[n]),
205+
...paramInfos.map((info) => params[info.name]),
204206
]) as Response;
205207
}
206208

@@ -214,15 +216,37 @@ class Router {
214216
Request request,
215217
String path,
216218
Map<String, String> params,
219+
List<ParamInfo> paramInfos,
217220
) {
218-
// TODO(davidmartos96): Maybe this could be done in a different way
219-
// to avoid replacing the path N times, N being the number of params
220-
var resolvedPath = path;
221-
for (final paramEntry in params.entries) {
222-
resolvedPath =
223-
resolvedPath.replaceFirst('<${paramEntry.key}>', paramEntry.value);
221+
// we iterate the non-resolved path and we write to a StringBuffer
222+
// resolving ther parameters along the way
223+
final resolvedPathBuff = StringBuffer();
224+
var paramIndex = 0;
225+
var charIndex = 0;
226+
while (charIndex < path.length) {
227+
if (paramIndex < paramInfos.length) {
228+
final paramInfo = paramInfos[paramIndex];
229+
if (charIndex < paramInfo.startIdx - 1) {
230+
// Add up until the param slot starts
231+
final part = path.substring(charIndex, paramInfo.startIdx - 1);
232+
resolvedPathBuff.write(part);
233+
charIndex += part.length;
234+
} else {
235+
// Add the resolved value of the parameter
236+
final paramName = paramInfo.name;
237+
final paramValue = params[paramName]!;
238+
resolvedPathBuff.write(paramValue);
239+
charIndex = paramInfo.endIdx - 1;
240+
paramIndex++;
241+
}
242+
} else {
243+
// All params looped, so add up until the end of the path
244+
final part = path.substring(charIndex, path.length);
245+
resolvedPathBuff.write(part);
246+
charIndex += part.length;
247+
}
224248
}
225-
249+
var resolvedPath = resolvedPathBuff.toString();
226250
return resolvedPath;
227251
}
228252

pkgs/shelf_router/lib/src/router_entry.dart

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,16 @@ class RouterEntry {
4444
final RegExp _routePattern;
4545

4646
/// Names for the parameters in the route pattern.
47-
final List<String> _params;
47+
final List<ParamInfo> _paramInfos;
48+
49+
List<ParamInfo> get paramInfos => _paramInfos.toList();
4850

4951
/// List of parameter names in the route pattern.
50-
List<String> get params => _params.toList(); // exposed for using generator.
52+
// exposed for using generator.
53+
List<String> get params => _paramInfos.map((p) => p.name).toList();
5154

5255
RouterEntry._(this.verb, this.route, this._handler, this._middleware,
53-
this._routePattern, this._params, this._mounted);
56+
this._routePattern, this._paramInfos, this._mounted);
5457

5558
factory RouterEntry(
5659
String verb,
@@ -66,12 +69,21 @@ class RouterEntry {
6669
route, 'route', 'expected route to start with a slash');
6770
}
6871

69-
final params = <String>[];
72+
final params = <ParamInfo>[];
7073
var pattern = '';
7174
for (var m in _parser.allMatches(route)) {
72-
pattern += RegExp.escape(m[1]!);
75+
final firstGroup = m[1]!;
76+
pattern += RegExp.escape(firstGroup);
7377
if (m[2] != null) {
74-
params.add(m[2]!);
78+
final paramName = m[2]!;
79+
final startIdx = firstGroup.length;
80+
//final endIdx = startIdx + paramName.length;
81+
final paramInfo = ParamInfo(
82+
name: paramName,
83+
startIdx: startIdx,
84+
endIdx: m.end,
85+
);
86+
params.add(paramInfo);
7587
if (m[3] != null && !_isNoCapture(m[3]!)) {
7688
throw ArgumentError.value(
7789
route, 'route', 'expression for "${m[2]}" is capturing');
@@ -95,9 +107,10 @@ class RouterEntry {
95107
}
96108
// Construct map from parameter name to matched value
97109
var params = <String, String>{};
98-
for (var i = 0; i < _params.length; i++) {
110+
for (var i = 0; i < _paramInfos.length; i++) {
99111
// first group is always the full match, we ignore this group.
100-
params[_params[i]] = m[i + 1]!;
112+
final paramInfo = _paramInfos[i];
113+
params[paramInfo.name] = m[i + 1]!;
101114
}
102115
return params;
103116
}
@@ -114,14 +127,26 @@ class RouterEntry {
114127
return await _handler(request, this) as Response;
115128
}
116129

117-
if (_handler is Handler || _params.isEmpty) {
130+
if (_handler is Handler || _paramInfos.isEmpty) {
118131
return await _handler(request) as Response;
119132
}
120133

121134
return await Function.apply(_handler, [
122135
request,
123-
..._params.map((n) => params[n]),
136+
..._paramInfos.map((info) => params[info.name]),
124137
]) as Response;
125138
})(request);
126139
}
127140
}
141+
142+
class ParamInfo {
143+
final String name;
144+
final int startIdx;
145+
final int endIdx;
146+
147+
const ParamInfo({
148+
required this.name,
149+
required this.startIdx,
150+
required this.endIdx,
151+
});
152+
}

pkgs/shelf_router/test/router_test.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,23 @@ void main() {
259259
expect(await get('/users/jake'), 'jake root');
260260
expect(await get('/users/david/no-route'), 'catch-all-handler');
261261
});
262+
263+
test('can mount dynamic routes with regexp', () async {
264+
var app = Router();
265+
266+
app.mount(r'/before/<bookId|\d+>/after', (Request request, String bookId) {
267+
var router = Router();
268+
router.get('/', (r) => Response.ok('book ${int.parse(bookId)}'));
269+
return router(request);
270+
});
271+
272+
app.all('/<_|[^]*>', (Request request) {
273+
return Response.ok('catch-all-handler');
274+
});
275+
276+
server.mount(app);
277+
278+
expect(await get('/before/123/after'), 'book 123');
279+
expect(await get('/before/abc/after'), 'catch-all-handler');
280+
});
262281
}

0 commit comments

Comments
 (0)