-
Notifications
You must be signed in to change notification settings - Fork 80
/
router.dart
271 lines (245 loc) · 10.1 KB
/
router.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show UnmodifiableMapView;
import 'package:http_methods/http_methods.dart';
import 'package:meta/meta.dart' show sealed;
import 'package:shelf/shelf.dart';
import 'package:shelf_router/src/router_entry.dart' show RouterEntry;
/// Get a URL parameter captured by the [Router].
@Deprecated('Use Request.params instead')
String params(Request request, String name) {
final value = request.params[name];
if (value == null) {
throw Exception('no such parameter $name');
}
return value;
}
final _emptyParams = UnmodifiableMapView(<String, String>{});
extension RouterParams on Request {
/// Get URL parameters captured by the [Router].
///
/// **Example**
/// ```dart
/// final app = Router();
///
/// app.get('/hello/<name>', (Request request) {
/// final name = request.params['name'];
/// return Response.ok('Hello $name');
/// });
/// ```
///
/// If no parameters are captured this returns an empty map.
///
/// The returned map is unmodifiable.
Map<String, String> get params {
final p = context['shelf_router/params'];
if (p is Map<String, String>) {
return UnmodifiableMapView(p);
}
return _emptyParams;
}
}
/// Middleware to remove body from request.
final _removeBody = createMiddleware(responseHandler: (r) {
if (r.headers.containsKey('content-length')) {
r = r.change(headers: {'content-length': '0'});
}
return r.change(body: <int>[]);
});
/// A shelf [Router] routes requests to handlers based on HTTP verb and route
/// pattern.
///
/// ```dart
/// import 'package:shelf_router/shelf_router.dart';
/// import 'package:shelf/shelf.dart';
/// import 'package:shelf/shelf_io.dart' as io;
///
/// var app = Router();
///
/// // Route pattern parameters can be specified <paramName>
/// app.get('/users/<userName>/whoami', (Request request) async {
/// // The matched values can be read with params(request, param)
/// var userName = request.params['userName'];
/// return Response.ok('You are ${userName}');
/// });
///
/// // The matched value can also be taken as parameter, if the handler given
/// // doesn't implement Handler, it's assumed to take all parameters in the
/// // order they appear in the route pattern.
/// app.get('/users/<userName>/say-hello', (Request request, String userName) async {
/// assert(userName == request.params['userName']);
/// return Response.ok('Hello ${userName}');
/// });
///
/// // It is possible to have multiple parameters, and if desired a custom
/// // regular expression can be specified with <paramName|REGEXP>, where
/// // REGEXP is a regular expression (leaving out ^ and $).
/// // If no regular expression is specified `[^/]+` will be used.
/// app.get('/users/<userName>/messages/<msgId|\d+>', (Request request) async {
/// var msgId = int.parse(request.params['msgId']!);
/// return Response.ok(message.getById(msgId));
/// });
///
/// var server = await io.serve(app, 'localhost', 8080);
/// ```
///
/// If multiple routes match the same request, the handler for the first
/// route is called.
/// If no route matches a request, a [Response.notFound] will be returned
/// instead. The default matcher can be overridden with the `notFoundHandler`
/// constructor parameter.
@sealed
class Router {
final List<RouterEntry> _routes = [];
final Handler _notFoundHandler;
/// Creates a new [Router] routing requests to handlers.
///
/// The [notFoundHandler] will be invoked for requests where no matching route
/// was found. By default, a simple [Response.notFound] will be used instead.
Router({Handler notFoundHandler = _defaultNotFound})
: _notFoundHandler = notFoundHandler;
/// Add [handler] for [verb] requests to [route].
///
/// If [verb] is `GET` the [handler] will also be called for `HEAD` requests
/// matching [route]. This is because handling `GET` requests without handling
/// `HEAD` is always wrong. To explicitely implement a `HEAD` handler it must
/// be registered before the `GET` handler.
void add(String verb, String route, Function handler) {
if (!isHttpMethod(verb)) {
throw ArgumentError.value(verb, 'verb', 'expected a valid HTTP method');
}
verb = verb.toUpperCase();
if (verb == 'GET') {
// Handling in a 'GET' request without handling a 'HEAD' request is always
// wrong, thus, we add a default implementation that discards the body.
_routes.add(RouterEntry('HEAD', route, handler, middleware: _removeBody));
}
_routes.add(RouterEntry(verb, route, handler));
}
/// Handle all request to [route] using [handler].
void all(String route, Function handler) {
_routes.add(RouterEntry('ALL', route, handler));
}
/// Mount a handler below a prefix.
///
/// In this case prefix may not contain any parameters, nor
void mount(String prefix, Handler handler) {
if (!prefix.startsWith('/') || !prefix.endsWith('/')) {
throw ArgumentError.value(
prefix, 'prefix', 'must start and end with a slash');
}
// first slash is always in request.handlerPath
final path = prefix.substring(1);
all(prefix + '<path|[^]*>', (Request request) {
return handler(request.change(path: path));
});
}
/// Route incoming requests to registered handlers.
///
/// This method allows a Router instance to be a [Handler].
Future<Response> call(Request request) async {
// Note: this is a great place to optimize the implementation by building
// a trie for faster matching... left as an exercise for the reader :)
for (var route in _routes) {
if (route.verb != request.method.toUpperCase() && route.verb != 'ALL') {
continue;
}
var params = route.match('/' + request.url.path);
if (params != null) {
final response = await route.invoke(request, params);
if (response != routeNotFound) {
return response;
}
}
}
return _notFoundHandler(request);
}
// Handlers for all methods
/// Handle `GET` request to [route] using [handler].
///
/// If no matching handler for `HEAD` requests is registered, such requests
/// will also be routed to the [handler] registered here.
void get(String route, Function handler) => add('GET', route, handler);
/// Handle `HEAD` request to [route] using [handler].
void head(String route, Function handler) => add('HEAD', route, handler);
/// Handle `POST` request to [route] using [handler].
void post(String route, Function handler) => add('POST', route, handler);
/// Handle `PUT` request to [route] using [handler].
void put(String route, Function handler) => add('PUT', route, handler);
/// Handle `DELETE` request to [route] using [handler].
void delete(String route, Function handler) => add('DELETE', route, handler);
/// Handle `CONNECT` request to [route] using [handler].
void connect(String route, Function handler) =>
add('CONNECT', route, handler);
/// Handle `OPTIONS` request to [route] using [handler].
void options(String route, Function handler) =>
add('OPTIONS', route, handler);
/// Handle `TRACE` request to [route] using [handler].
void trace(String route, Function handler) => add('TRACE', route, handler);
/// Handle `PATCH` request to [route] using [handler].
void patch(String route, Function handler) => add('PATCH', route, handler);
static Response _defaultNotFound(Request request) => routeNotFound;
/// Sentinel [Response] object indicating that no matching route was found.
///
/// This is the default response value from a [Router] created without a
/// `notFoundHandler`, when no routes matches the incoming request.
///
/// If the [routeNotFound] object is returned from a [Handler] the [Router]
/// will consider the route _not matched_, and attempt to match other routes.
/// This is useful when mounting nested routers, or when matching a route
/// is conditioned on properties beyond the path of the URL.
///
/// **Example**
/// ```dart
/// final app = Router();
///
/// // The pattern for this route will match '/search' and '/search?q=...',
/// // but if request does not have `?q=...', then the handler will return
/// // [Router.routeNotFound] causing the router to attempt further routes.
/// app.get('/search', (Request request) async {
/// if (!request.uri.queryParameters.containsKey('q')) {
/// return Router.routeNotFound;
/// }
/// return Response.ok('TODO: make search results');
/// });
///
/// // Same pattern as above
/// app.get('/search', (Request request) async {
/// return Response.ok('TODO: return search form');
/// });
///
/// // Create a single nested router we can mount for handling API requests.
/// final api = Router();
///
/// api.get('/version', (Request request) => Response.ok('1'));
///
/// // Mounting router under '/api'
/// app.mount('/api', api);
///
/// // If a request matches `/api/...` then the routes in the [api] router
/// // will be attempted. However, for a request like `/api/hello` there is
/// // no matching route in the [api] router. Thus, the router will return
/// // [Router.routeNotFound], which will cause matching to continue.
/// // Hence, the catch-all route below will be matched, causing a custom 404
/// // response with message 'nothing found'.
///
/// // In the pattern below `<anything|.*>` is on the form `<name|regex>`,
/// // thus, this simply creates a URL parameter called `anything` which
/// // matches anything.
/// app.all('/<anything|.*>', (Request request) {
/// return Response.notFound('nothing found');
/// });
/// ```
static final Response routeNotFound = Response.notFound('Route not found');
}