blob: a298b5215d6d1616af487cf02ed31764c1da8982 [file] [log] [blame]
// 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 'router_entry.dart' show RouterEntry;
import 'package:shelf/shelf.dart';
import 'package:http_methods/http_methods.dart';
/// Get a URL parameter captured by the [Router].
String params(Request request, String name) {
ArgumentError.checkNotNull(request, 'request');
ArgumentError.checkNotNull(name, 'name');
final p = request.context['shelf_router/params'];
if (!(p is Map<String, String>)) {
throw new Exception('no such parameter $name');
}
final value = (p as Map<String, String>)[name];
if (value == null) {
throw new Exception('no such parameter $name');
}
return value;
}
/// 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 = params(request, '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 {
/// return Response.ok('Hello ${uName}');
/// });
///
/// // 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(params(request, 'msgId'));
/// return Response.ok(message.getById(msgId));
/// });
///
/// var server = await io.serve(app.handler, 'localhost', 8080);
/// ```
///
/// If multiple routes match the same request, the handler for the first
/// route is called. If the handler returns `null` the next matching handler
/// will be attempted.
///
///
class Router {
final List<RouterEntry> _routes = [];
/// Add [handler] for [verb] requests to [route].
void add(String verb, String route, Function handler) {
ArgumentError.checkNotNull(verb, 'verb');
if (!isHttpMethod(verb)) {
throw ArgumentError.value(verb, 'verb', 'expected a valid HTTP method');
}
verb = verb.toUpperCase();
_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 router below a prefix.
///
/// In this case prefix may not contain any parameters, nor
void mount(String prefix, Router router) {
ArgumentError.checkNotNull(prefix, 'prefix');
ArgumentError.checkNotNull(router, 'router');
if (!prefix.startsWith('/') || !prefix.endsWith('/')) {
throw ArgumentError.value(
prefix, 'prefix', 'must start and end with a slash');
}
final handler = router.handler;
// first slash is always in request.handlerPath
final path = prefix.substring(1);
all(prefix + '<path|[^]*>', (Request request) {
return handler(request.change(path: path));
});
}
/// Get a [Handler] that will route incoming requests to registered handlers.
Handler get handler {
// Note: this is a great place to optimize the implementation by building
// a trie for faster matching... left as an exercise for the reader :)
return (Request request) async {
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) {
var res = await route.invoke(request, params);
if (res != null) {
return res;
}
}
}
return null;
};
}
// Handlers for all methods
/// Handle `GET` request to [route] using [handler].
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);
}