Skip to content

Commit ac228e8

Browse files
committed
Merge branch 'feat/mount_dynamic_regexp' into feat/mount_dynamic_routes
2 parents 874da11 + 54a2f9a commit ac228e8

File tree

3 files changed

+122
-24
lines changed

3 files changed

+122
-24
lines changed

pkgs/shelf_router/lib/src/router.dart

+38-14
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

+51-10
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,26 @@ 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 = '';
74+
// Keep the index where the matches are located
75+
// so that we can calculate the positioning of
76+
// the extracted parameter
77+
var prevMatchIndex = 0;
7178
for (var m in _parser.allMatches(route)) {
72-
pattern += RegExp.escape(m[1]!);
79+
final firstGroup = m[1]!;
80+
pattern += RegExp.escape(firstGroup);
7381
if (m[2] != null) {
74-
params.add(m[2]!);
82+
final paramName = m[2]!;
83+
final startIdx = prevMatchIndex + firstGroup.length;
84+
final paramInfo = ParamInfo(
85+
name: paramName,
86+
startIdx: startIdx,
87+
endIdx: m.end,
88+
);
89+
params.add(paramInfo);
90+
prevMatchIndex = m.end;
91+
7592
if (m[3] != null && !_isNoCapture(m[3]!)) {
7693
throw ArgumentError.value(
7794
route, 'route', 'expression for "${m[2]}" is capturing');
@@ -95,9 +112,10 @@ class RouterEntry {
95112
}
96113
// Construct map from parameter name to matched value
97114
var params = <String, String>{};
98-
for (var i = 0; i < _params.length; i++) {
115+
for (var i = 0; i < _paramInfos.length; i++) {
99116
// first group is always the full match, we ignore this group.
100-
params[_params[i]] = m[i + 1]!;
117+
final paramInfo = _paramInfos[i];
118+
params[paramInfo.name] = m[i + 1]!;
101119
}
102120
return params;
103121
}
@@ -114,14 +132,37 @@ class RouterEntry {
114132
return await _handler(request, this) as Response;
115133
}
116134

117-
if (_handler is Handler || _params.isEmpty) {
135+
if (_handler is Handler || _paramInfos.isEmpty) {
118136
return await _handler(request) as Response;
119137
}
120138

121139
return await Function.apply(_handler, [
122140
request,
123-
..._params.map((n) => params[n]),
141+
..._paramInfos.map((info) => params[info.name]),
124142
]) as Response;
125143
})(request);
126144
}
127145
}
146+
147+
/// This class holds information about a parameter extracted
148+
/// from the route path.
149+
/// The indexes can by used by the mount logic to resolve the
150+
/// parametrized path when handling the request.
151+
class ParamInfo {
152+
/// This is the name of the parameter, without <, >
153+
final String name;
154+
155+
/// The index in the route String where the parameter
156+
/// expression starts (inclusive)
157+
final int startIdx;
158+
159+
/// The index in the route String where the parameter
160+
/// expression ends (exclusive)
161+
final int endIdx;
162+
163+
const ParamInfo({
164+
required this.name,
165+
required this.startIdx,
166+
required this.endIdx,
167+
});
168+
}

pkgs/shelf_router/test/router_test.dart

+33
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,37 @@ 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 multiple parameters', () async {
264+
var app = Router();
265+
app.mount(r'/first/<second>/third/<fourth|\d+>/last',
266+
(Request request, String second, String fourthNum) {
267+
var router = Router();
268+
router.get('/', (r) => Response.ok('$second ${int.parse(fourthNum)}'));
269+
return router(request);
270+
});
271+
272+
server.mount(app);
273+
274+
expect(await get('/first/hello/third/12/last'), 'hello 12');
275+
});
276+
277+
test('can mount dynamic routes with regexp', () async {
278+
var app = Router();
279+
280+
app.mount(r'/before/<bookId|\d+>/after', (Request request, String bookId) {
281+
var router = Router();
282+
router.get('/', (r) => Response.ok('book ${int.parse(bookId)}'));
283+
return router(request);
284+
});
285+
286+
app.all('/<_|[^]*>', (Request request) {
287+
return Response.ok('catch-all-handler');
288+
});
289+
290+
server.mount(app);
291+
292+
expect(await get('/before/123/after'), 'book 123');
293+
expect(await get('/before/abc/after'), 'catch-all-handler');
294+
});
262295
}

0 commit comments

Comments
 (0)