platform/lib/src/core/routable.dart
2018-08-19 23:06:29 -04:00

177 lines
5.6 KiB
Dart

library angel_framework.http.routable;
import 'dart:async';
import 'package:angel_route/angel_route.dart';
import '../util.dart';
import 'hooked_service.dart';
import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'server.dart';
import 'service.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// A function that intercepts a request and determines whether handling of it should continue.
typedef Future<bool> RequestMiddleware(RequestContext req, ResponseContext res);
/// A function that receives an incoming [RequestContext] and responds to it.
typedef FutureOr RequestHandler(RequestContext req, ResponseContext res);
/// Sequentially runs a list of [handlers] of middleware, and returns early if any does not
/// return `true`. Works well with [Router].chain.
RequestMiddleware waterfall(List handlers) {
return (req, res) {
Future<bool> Function() runPipeline;
for (var handler in handlers) {
if (handler == null) break;
if (runPipeline == null)
runPipeline = () => req.app.executeHandler(handler, req, res);
else {
var current = runPipeline;
runPipeline = () => current().then((result) => !result
? new Future.value(result)
: req.app.executeHandler(handler, req, res));
}
}
runPipeline ??= () => new Future.value(true);
return runPipeline();
};
}
/// A routable server that can handle dynamic requests.
class Routable extends Router {
final Map<Pattern, Service> _services = {};
final Map configuration = {};
Routable() : super();
void close() {
_services.clear();
configuration.clear();
requestMiddleware.clear();
_onService.close();
}
/// Additional filters to be run on designated requests.
@override
final Map<String, RequestHandler> requestMiddleware = <String, RequestHandler>{};
/// A set of [Service] objects that have been mapped into routes.
Map<Pattern, Service> get services => _services;
StreamController<Service> _onService =
new StreamController<Service>.broadcast();
/// Fired whenever a service is added to this instance.
///
/// **NOTE**: This is a broadcast stream.
Stream<Service> get onService => _onService.stream;
/// Assigns a middleware to a name for convenience.
@override
registerMiddleware(String name, middleware) {
assert(middleware is RequestMiddleware);
// ignore: deprecated_member_use
super.registerMiddleware(name, middleware);
}
/// Retrieves the service assigned to the given path.
Service service(Pattern path) =>
_services[path] ??
_services[path.toString().replaceAll(_straySlashes, '')];
@override
Route addRoute(String method, Pattern path, Object handler,
{List middleware: const []}) {
final List handlers = [];
// Merge @Middleware declaration, if any
Middleware middlewareDeclaration = getAnnotation(handler, Middleware);
if (middlewareDeclaration != null) {
handlers.addAll(middlewareDeclaration.handlers);
}
final List handlerSequence = [];
handlerSequence.addAll(middleware ?? []);
handlerSequence.addAll(handlers);
return super.addRoute(method, path.toString(), handler,
middleware: handlerSequence);
}
/// Mounts the given [router] on this instance.
///
/// The [router] may only omitted when called via
/// an [Angel] instance.
///
/// Returns either a [Route] or a [Service] (if one was mounted).
use(path, [Router router, String namespace = null]) {
Router _router = router;
Service service;
// If we need to hook this service, do it here. It has to be first, or
// else all routes will point to the old service.
if (router is Service) {
_router = service = new HookedService(router);
_services[path
.toString()
.trim()
.replaceAll(new RegExp(r'(^/+)|(/+$)'), '')] = service;
service.addRoutes();
if (_router is HookedService && _router != router)
router.onHooked(_router);
}
final handlers = [];
if (_router is Angel) {
handlers.add((RequestContext req, ResponseContext res) {
req.app = _router as Angel;
res.app = _router as Angel;
return true;
});
}
// Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = namespace != null ? "$namespace." : "";
Map copiedMiddleware = new Map.from(router.requestMiddleware);
for (String middlewareName in copiedMiddleware.keys) {
requestMiddleware.putIfAbsent("$middlewarePrefix$middlewareName",
() => copiedMiddleware[middlewareName] as RequestHandler);
}
// Also copy properties...
if (router is Routable) {
Map copiedProperties = new Map.from(router.configuration);
for (String propertyName in copiedProperties.keys) {
configuration.putIfAbsent("$middlewarePrefix$propertyName",
() => copiedMiddleware[propertyName]);
}
}
// _router.dumpTree(header: 'Mounting on "$path":');
// root.child(path, debug: debug, handlers: handlers).addChild(router.root);
var mounted = mount(path.toString(), _router);
if (_router is Routable) {
// Copy services, too. :)
for (Pattern servicePath in _router._services.keys) {
String newServicePath =
path.toString().trim().replaceAll(new RegExp(r'(^/+)|(/+$)'), '') +
'/$servicePath';
_services[newServicePath] = _router._services[servicePath];
}
}
if (service != null) {
if (_onService.hasListener) _onService.add(service);
}
return service ?? mounted;
}
}