diff --git a/.travis.yml b/.travis.yml index de2210c9..f5888800 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,2 @@ -language: dart \ No newline at end of file +language: dart +script: ./tool/travis.sh \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..20af2f68 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +// Place your settings in this file to overwrite default and user settings. +{ +} \ No newline at end of file diff --git a/README.md b/README.md index bd34cb42..939a6fd0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # angel_framework -[![pub 1.0.0-dev.70](https://img.shields.io/badge/pub-1.0.0--dev.70-red.svg)](https://pub.dartlang.org/packages/angel_framework) +[![pub 1.0.0-dev.71](https://img.shields.io/badge/pub-1.0.0--dev.71-red.svg)](https://pub.dartlang.org/packages/angel_framework) [![build status](https://travis-ci.org/angel-dart/framework.svg)](https://travis-ci.org/angel-dart/framework) -Core libraries for the Angel Framework. +A high-powered HTTP server with support for dependency injection, sophisticated routing and more. ```dart import 'package:angel_framework/angel_framework.dart'; diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 197cf9f3..16715d42 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -1,6 +1,9 @@ /// Various libraries useful for creating highly-extensible servers. library angel_framework.http; +import 'dart:async'; +import 'dart:io'; +import 'server.dart' show ServerGenerator; export 'package:angel_route/angel_route.dart'; export 'package:body_parser/body_parser.dart' show FileUploadInfo; export 'angel_base.dart'; @@ -21,3 +24,13 @@ export 'server.dart'; export 'service.dart'; export 'typed_service.dart'; +/// Boots a shared server instance. Use this if launching multiple isolates +Future startShared(InternetAddress address, int port) => HttpServer + .bind(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0, shared: true); + +/// Boots a secure shared server instance. Use this if launching multiple isolates +ServerGenerator startSharedSecure(SecurityContext securityContext) { + return (InternetAddress address, int port) => HttpServer.bindSecure( + address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0, securityContext, + shared: true); +} diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 53b73672..757e6e3f 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -4,6 +4,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:mirrors'; import 'package:angel_route/angel_route.dart'; +export 'package:container/container.dart'; +import 'package:flatten/flatten.dart'; +import 'package:json_god/json_god.dart' as god; import 'angel_base.dart'; import 'angel_http_exception.dart'; import 'controller.dart'; @@ -12,7 +15,6 @@ import 'request_context.dart'; import 'response_context.dart'; import 'routable.dart'; import 'service.dart'; -export 'package:container/container.dart'; final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); @@ -37,10 +39,14 @@ class Angel extends AngelBase { StreamController _onController = new StreamController.broadcast(); final List _children = []; + Router _flattened; + bool _isProduction; Angel _parent; ServerGenerator _serverGenerator = HttpServer.bind; + final Map _injections = {}; final Map _preContained = {}; + ResponseSerializer _serializer; /// Determines whether to allow HTTP request method overrides. bool allowMethodOverrides = true; @@ -61,7 +67,15 @@ class Angel extends AngelBase { /// /// The criteria for this is the `ANGEL_ENV` environment variable being set to /// `'production'`. - bool get isProduction => Platform.environment['ANGEL_ENV'] == 'production'; + /// + /// This value is memoized the first time you call it, so do not change environment + /// configuration at runtime! + bool get isProduction { + if (_isProduction != null) + return _isProduction; + else + return _isProduction = Platform.environment['ANGEL_ENV'] == 'production'; + } /// The function used to bind this instance to an HTTP server. ServerGenerator get serverGenerator => _serverGenerator; @@ -130,10 +144,22 @@ class Angel extends AngelBase { await configure(configurer); } - preprocessRoutes(); + optimizeForProduction(); return httpServer..listen(handleRequest); } + @override + Route addRoute(String method, String path, Object handler, + {List middleware: const []}) { + if (_flattened != null) { + print( + 'WARNING: You added a route ($method $path) to the router, after it has been optimized.'); + print('This route will be ignored, and no requests will ever reach it.'); + } + + return super.addRoute(method, path, handler, middleware: middleware ?? []); + } + /// Loads some base dependencies into the service container. void bootstrapContainer() { if (runtimeType != Angel) container.singleton(this, as: Angel); @@ -143,20 +169,45 @@ class Angel extends AngelBase { container.singleton(this); } + @override + void dumpTree( + {callback(String tree), + String header: 'Dumping route tree:', + String tab: ' ', + bool showMatchers: false}) { + if (isProduction) { + if (_flattened == null) _flattened = flatten(this); + + _flattened.dumpTree( + callback: callback, + header: header?.isNotEmpty == true + ? header + : (isProduction + ? 'Dumping flattened route tree:' + : 'Dumping route tree:'), + tab: tab ?? ' ', + showMatchers: showMatchers == true); + } else { + super.dumpTree( + callback: callback, + header: header?.isNotEmpty == true + ? header + : (isProduction + ? 'Dumping flattened route tree:' + : 'Dumping route tree:'), + tab: tab ?? ' ', + showMatchers: showMatchers == true); + } + } + /// Shortcut for adding a middleware to inject a key/value pair on every request. void inject(key, value) { - before.add((RequestContext req, ResponseContext res) async { - req.inject(key, value); - return true; - }); + _injections[key] = value; } /// Shortcut for adding a middleware to inject a serialize on every request. void injectSerializer(ResponseSerializer serializer) { - before.add((RequestContext req, ResponseContext res) async { - res.serializer = serializer; - return true; - }); + _serializer = serializer; } /// Runs some [handler]. Returns `true` if request execution should continue. @@ -227,11 +278,14 @@ class Angel extends AngelBase { Future createRequestContext(HttpRequest request) { _beforeProcessed.add(request); - return RequestContext.from(request, this); + return RequestContext.from(request, this).then((req) { + return req..injections.addAll(_injections ?? {}); + }); } Future createResponseContext(HttpResponse response) async => - new ResponseContext(response, this); + new ResponseContext(response, this) + ..serializer = (_serializer ?? god.serialize); /// Handles a single request. Future handleRequest(HttpRequest request) async { @@ -242,7 +296,10 @@ class Angel extends AngelBase { if (requestedUrl.isEmpty) requestedUrl = '/'; - var resolved = resolveAll(requestedUrl, requestedUrl, method: req.method); + Router r = + isProduction ? (_flattened ?? (_flattened = flatten(this))) : this; + var resolved = + r.resolveAll(requestedUrl, requestedUrl, method: req.method); for (var result in resolved) req.params.addAll(result.allParams); @@ -317,29 +374,35 @@ class Angel extends AngelBase { } } - /// Preprocesses all routes, and eliminates the burden of reflecting handlers + /// Runs several optimizations, *if* [isProduction] is `true`. + /// + /// * Preprocesses all dependency injection, and eliminates the burden of reflecting handlers /// at run-time. - void preprocessRoutes() { - _add(v) { - if (v is Function && !_preContained.containsKey(v)) { - _preContained[v] = preInject(v); - } - } - - void _walk(Router router) { - if (router is Angel) { - router..before.forEach(_add)..after.forEach(_add); + /// * [flatten]s the route tree into a linear one. + void optimizeForProduction() { + if (isProduction == true) { + _add(v) { + if (v is Function && !_preContained.containsKey(v)) { + _preContained[v] = preInject(v); + } } - router.requestMiddleware.forEach((k, v) => _add(v)); - router.middleware.forEach(_add); - router.routes - .where((r) => r is SymlinkRoute) - .map((SymlinkRoute r) => r.router) - .forEach(_walk); - } + void _walk(Router router) { + if (router is Angel) { + router..before.forEach(_add)..after.forEach(_add); + } - _walk(this); + router.requestMiddleware.forEach((k, v) => _add(v)); + router.middleware.forEach(_add); + router.routes + .where((r) => r is SymlinkRoute) + .map((SymlinkRoute r) => r.router) + .forEach(_walk); + } + + _walk(_flattened = flatten(this)); + print('Angel is running in production mode.'); + } } /// Run a function after injecting from service container. diff --git a/pubspec.yaml b/pubspec.yaml index 83b150fa..7c68938c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_framework -version: 1.0.0-dev.70 -description: Core libraries for the Angel framework. +version: 1.0.0-dev.71 +description: A high-powered HTTP server with DI, routing and more. author: Tobe O homepage: https://github.com/angel-dart/angel_framework environment: @@ -9,6 +9,7 @@ dependencies: angel_route: ^1.0.0-dev body_parser: ^1.0.0-dev container: ^0.1.2 + flatten: ^1.0.0 json_god: ^2.0.0-beta merge_map: ^1.0.0 random_string: ^0.0.1 diff --git a/test/routing_test.dart b/test/routing_test.dart index 070dd23a..183ac2f6 100644 --- a/test/routing_test.dart +++ b/test/routing_test.dart @@ -31,6 +31,11 @@ main() { nested = new Angel(debug: debug); todos = new Angel(debug: debug); + // Lazy-parse in production + [app, nested, todos].forEach((Angel app) { + app.lazyParseBodies = app.isProduction; + }); + app ..registerMiddleware('interceptor', (req, res) async { res.write('Middleware'); diff --git a/tool/travis.sh b/tool/travis.sh new file mode 100644 index 00000000..1afd39d1 --- /dev/null +++ b/tool/travis.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +pub run test +ANGEL_ENV=production pub run test \ No newline at end of file