├── .analysis_options ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── embla.dart ├── lib ├── application.dart ├── bootstrap.dart ├── container.dart ├── http.dart ├── http_annotations.dart ├── http_basic_middleware.dart └── src │ ├── container.dart │ ├── http │ ├── context.dart │ ├── error_template.dart │ ├── http_exceptions.dart │ ├── middleware.dart │ ├── middleware │ │ ├── conditional_middleware.dart │ │ ├── error_handler_middleware.dart │ │ ├── forwarder_middleware.dart │ │ ├── handler_middleware.dart │ │ ├── input_parser_middleware.dart │ │ ├── logger_middleware.dart │ │ ├── pub_middleware.dart │ │ ├── remove_trailing_slash_middleware.dart │ │ └── static_files_middleware.dart │ ├── pipeline.dart │ ├── request_response.dart │ ├── response_maker.dart │ ├── route_expander.dart │ └── routing.dart │ └── util │ ├── concat.dart │ ├── container_state.dart │ ├── nothing.dart │ ├── process_handler.dart │ ├── stylizer.dart │ ├── terminal.dart │ └── trace_formatting.dart ├── pubspec.yaml ├── resources └── error-page │ ├── .gitignore │ ├── index.html │ └── index.scss └── test ├── integration └── bootstrap_test.dart └── unit ├── bootstrappers └── http_bootstrapper_test.dart ├── container_test.dart └── http ├── middleware ├── conditional_middleware_test.dart ├── error_handler_middleware_test.dart ├── handler_middleware_test.dart ├── input_parser_middleware_test.dart ├── input_parsers │ ├── input_parser_expectation.dart │ ├── json_input_parser_test.dart │ ├── raw_input_parser_test.dart │ └── url_encoded_input_parser_test.dart └── middleware_call.dart ├── pipeline_test.dart ├── response_maker_test.dart └── route_expander_test.dart /.analysis_options: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | .packages 3 | pubspec.lock 4 | .pub 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.2 4 | * Fixed a bug where routing stars didn't require a slash to follow: 5 | 6 | ```dart 7 | Route.get('path/*') 8 | // no longer matches "/path_and_more" 9 | // only "/path/and_more" 10 | ``` 11 | 12 | * Fixed a bug where the `UrlEncodedInputParser` didn't actually decode the URL component: 13 | 14 | ```dart 15 | key=value -> { 'key': 'value' } 16 | 17 | // before 0.2.2 18 | key=value%20with%20spaces -> { 'key': 'value%20with%20spaces' } 19 | // after 0.2.2 20 | key=value%20with%20spaces -> { 'key': 'value with spaces' } 21 | ``` 22 | 23 | ## 0.2.1 24 | Adds a new `ForwarderMiddleware` that acts as a proxy to another server. 25 | 26 | Also adds a `PubMiddleware` which combines the `StaticFilesMiddleware` and the new 27 | `ForwarderMiddleware` to forward to Pub serve in dev mode, and to the build directory in 28 | production mode. 29 | 30 | ```dart 31 | pipe( 32 | // Middleware preceding the PubMiddleware will now be available on the same server 33 | // that deals with transformers and stuff! 34 | Route.get('special-endpoint', () => 'Hello from server!'), 35 | PubMiddleware 36 | ) 37 | ``` 38 | 39 | ```shell 40 | > pub serve 41 | # In another tab 42 | > APP_ENV=development embla start 43 | ``` 44 | 45 | ## Pre 0.2 46 | An empty Embla app is an empty getter called `embla` in the script, with an export statement. 47 | 48 | ```dart 49 | export 'package:embla/bootstrap.dart'; 50 | get embla => []; 51 | ``` 52 | 53 | The getter should return a `List`. 54 | 55 | ```dart 56 | import 'package:embla/application.dart'; 57 | export 'package:embla/bootstrap.dart'; 58 | 59 | List get embla => []; 60 | ``` 61 | 62 | Bootstrappers attach listeners to application lifetime hooks. 63 | 64 | ```dart 65 | import 'package:embla/application.dart'; 66 | export 'package:embla/bootstrap.dart'; 67 | 68 | get embla => [ 69 | new MyBootstrapper() 70 | ]; 71 | 72 | class MyBootstrapper extends Bootstrapper { 73 | @Hook.init 74 | init() { 75 | print('Starting the application up!'); 76 | } 77 | 78 | @Hook.exit 79 | exit() { 80 | print('Shutting the application down!'); 81 | } 82 | } 83 | ``` 84 | 85 | Methods in a bootstrapper can use Dependency Injection to inject classes. Since Embla uses a stateless 86 | IoC container, adding bindings to the container returns a new instance. To push the new bindings into 87 | the application, the bootstrappers can return the new container in any of its methods. 88 | 89 | The container itself is available from the `Bootstrapper` superclass. 90 | 91 | ```dart 92 | class AddsBindingsBootstrapper extends Bootstrapper { 93 | @Hook.bindings 94 | bindings() { 95 | return container.bind(SomeAbstractClass, to: SomeConcreteClass); 96 | } 97 | } 98 | ``` 99 | 100 | The hooks, as well as the container, is documented in doc comments. 101 | 102 | #### HTTP Pipeline 103 | The basic Embla library comes with an `HttpBootstrapper`, which takes some configuration as named 104 | parameters. One of which is the `pipeline` parameter, expecting a `Pipeline`. 105 | 106 | The `pipe` helper creates a `Pipeline` from one or more `Middleware`: 107 | 108 | ```dart 109 | import 'package:embla/http.dart'; 110 | export 'package:embla/bootstrap.dart'; 111 | 112 | get embla => [ 113 | new HttpBootstrapper( 114 | pipeline: pipe( 115 | SomeMiddleware 116 | ) 117 | ) 118 | ]; 119 | ``` 120 | 121 | There are some middleware that comes out-of-the-box, for routing as well as for some common tasks like 122 | removing trailing slashes from URLs, parsing the request body, or handling errors thrown in the pipeline. 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Emil Persson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embla 2 | 3 | Embla is a powerful but simple server side application framework for Dart. 4 | 5 | ## Usage 6 | Install like so: 7 | 8 | ```yaml 9 | # pubspec.yaml 10 | dependencies: 11 | embla: any 12 | ``` 13 | 14 | ```shell 15 | > pub get 16 | ``` 17 | 18 | Embla scripts can be run directly with `dart my_script.dart`, but for development we can use the Embla CLI: 19 | 20 | ```shell 21 | > pub global activate embla 22 | # Add pub's binaries to PATH, to be able to omit "pub run" (Also: https://github.com/dart-lang/pub/issues/1204) 23 | > PATH=$PATH:~/.pub-cache/bin 24 | > embla start 25 | ``` 26 | 27 | Currently, `embla start` will look for a `bin/server.dart` file and start the app. If you make changes to 28 | your project files, the app will automatically restart. 29 | 30 | ## Overview 31 | Here's an example of a super simple Embla app. 32 | 33 | ```dart 34 | export 'package:embla/bootstrap.dart'; 35 | import 'package:embla/http.dart'; 36 | 37 | get embla => [ 38 | new HttpBootstrapper( 39 | pipeline: pipe(() => 'Hello world!') 40 | ) 41 | ]; 42 | ``` 43 | 44 | This application starts a server, and responds with "Hello world!" on every request. Looks weird? 45 | Let's figure out what's going on. 46 | 47 | ## Bootstrapping 48 | Instead of the good old `main` function, Embla requires a getter called `embla` in the main entry 49 | point script. The actual main function will be provided by `bootstrap.dart`. 50 | 51 | ```dart 52 | export 'package:embla/bootstrap.dart'; 53 | 54 | get embla => []; 55 | ``` 56 | 57 | If we were to run the above script, we would get an empty Dart process that did nothing, and 58 | would close on Ctrl+C. 59 | 60 | To hook into the application, we can add `Bootstrappers` to the `embla` function. `HttpBootstrapper` 61 | comes out of the box if we just import `'package:embla/http.dart'`. Each bootstrapper should be 62 | instantiated in the `embla` function, and any configuration needed is passed through the constructor. 63 | 64 | ## HTTP Pipeline 65 | It just so happens the `HttpBootstrapper` takes a named `pipeline` parameter, that represents the 66 | request/response pipeline for the server. 67 | 68 | To create a pipeline, we use the `pipe` helper provided by `embla/http.dart`. A pipeline consists 69 | of a series of Middleware. Embla wraps `Shelf` for this. 70 | 71 | ```dart 72 | import 'dart:async'; 73 | 74 | export 'package:embla/bootstrap.dart'; 75 | import 'package:embla/http.dart'; 76 | 77 | get embla => [ 78 | new HttpBootstrapper( 79 | pipeline: pipe( 80 | MyMiddleware 81 | ) 82 | ) 83 | ]; 84 | 85 | class MyMiddleware extends Middleware { 86 | Future handle(Request request) { 87 | // Pass along to the next middleware 88 | return super.handle(request); 89 | } 90 | } 91 | ``` 92 | 93 | The pipe allows for different formats for Middleware. You can pass in a Shelf Middleware 94 | directly, or the `Type` of a middleware class. It also supports passing in a `Function`, 95 | which will be converted to a route handler. 96 | 97 | ## Routing 98 | Routes are nothing more than conditional paths in the pipeline. Here's an example: 99 | 100 | ```dart 101 | pipeline: pipe( 102 | 103 | MiddlewareForAllRoutes, 104 | 105 | Route.get('/', () => 'Hello world'), 106 | 107 | Route.all('subroutes/*', 108 | MiddlewareForAllRoutesInSubroutes, 109 | 110 | Route.get('', () => 'Will be reached by GET /subroutes'), 111 | 112 | Route.put('action', () => 'Will be reached by PUT /subroutes/action'), 113 | 114 | Route.post('another', 115 | SpecialMiddlewareForThisRoute, 116 | () => 'Will be reached by POST /subroutes/another' 117 | ), 118 | 119 | Route.get('deeper/:wildcard', 120 | ({String wildcard}) => 'GET /subroutes/deeper/$wildcard' 121 | ) 122 | ), 123 | 124 | () => 'This will be reached by request not matching the routes above' 125 | ) 126 | ``` 127 | 128 | ## Controller 129 | In Embla, controllers are also middleware. They are collections of routes, after all. 130 | The controllers use annotations to declare routes. 131 | 132 | ```dart 133 | export 'package:embla/bootstrap.dart'; 134 | import 'package:embla/http.dart'; 135 | import 'package:embla/http_annotations.dart'; 136 | 137 | get embla => [new HttpBootstrapper(pipeline: pipe(MyController))]; 138 | 139 | class MyController extends Controller { 140 | /// GET /action -> 'Response' 141 | @Get() action() { 142 | return 'Response'; 143 | } 144 | 145 | /// POST /endpoint -> 302 / 146 | @Post('endpoint') methodName() { 147 | return redirect('/action'); 148 | } 149 | } 150 | ``` 151 | 152 | Since controllers are middleware too, we can easily route our controllers to endpoints like this: 153 | 154 | ```dart 155 | Route.all('pages/*', PagesController) 156 | ``` 157 | 158 | ## Custom Bootstrappers 159 | Bootstrappers hook into the initialization and deinitialization of the application. Creating one is 160 | super simple. 161 | 162 | ```dart 163 | export 'package:embla/bootstrap.dart'; 164 | import 'package:embla/application.dart'; 165 | 166 | get embla => [new MyBootstrapper()]; 167 | 168 | class MyBootstrapper extends Bootstrapper { 169 | @Hook.init 170 | init() { 171 | print('Initializing the application!'); 172 | } 173 | } 174 | ``` 175 | -------------------------------------------------------------------------------- /bin/embla.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | 3 | import 'dart:async'; 4 | import 'dart:io'; 5 | import 'dart:isolate'; 6 | 7 | import 'package:watcher/watcher.dart'; 8 | 9 | main(List arguments) async { 10 | if (arguments.length < 1) return print('Usage: embla start'); 11 | final command = arguments[0]; 12 | if (command != 'start') return print('Usage: embla start (Only the run command available currently)'); 13 | 14 | final filePath = '${Directory.current.path}/bin/server.dart'; 15 | final fileUri = new Uri.file(filePath); 16 | bool willRestart = true; 17 | SendPort restartPort; 18 | StreamController changeBroadcast = new StreamController.broadcast(); 19 | 20 | final watcher = new Watcher(Directory.current.path).events.listen((event) { 21 | if (!event.path.endsWith('.dart')) return; 22 | 23 | changeBroadcast.add(event); 24 | willRestart = true; 25 | restartPort?.send(null); 26 | }); 27 | 28 | while (willRestart) { 29 | willRestart = false; 30 | final exitPort = new ReceivePort(); 31 | final errorPort = new ReceivePort(); 32 | final receiveExitCommandPort = new ReceivePort(); 33 | await Isolate.spawnUri( 34 | fileUri, 35 | [], 36 | receiveExitCommandPort.sendPort, 37 | onExit: exitPort.sendPort, 38 | onError: errorPort.sendPort, 39 | errorsAreFatal: false, 40 | automaticPackageResolution: true 41 | ); 42 | 43 | restartPort = await receiveExitCommandPort.first 44 | .timeout(const Duration(seconds: 10)); 45 | 46 | final process = new Completer(); 47 | errorPort.listen((List l) async { 48 | print(l[0]); 49 | print('Listening for changes...'); 50 | await changeBroadcast.stream.first; 51 | if (!process.isCompleted) process.complete(0); 52 | }); 53 | exitPort.listen((_) { 54 | if (!process.isCompleted) process.complete(0); 55 | }); 56 | 57 | exitCode = await process.future; 58 | exitPort.close(); 59 | errorPort.close(); 60 | receiveExitCommandPort.close(); 61 | } 62 | 63 | await watcher.cancel(); 64 | } 65 | -------------------------------------------------------------------------------- /lib/application.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:mirrors'; 3 | import 'container.dart'; 4 | import 'src/util/trace_formatting.dart'; 5 | import 'src/util/container_state.dart'; 6 | 7 | class Application { 8 | final IoCContainer container; 9 | final List bootstrappers; 10 | 11 | Application._(this.container, this.bootstrappers); 12 | 13 | static Future boot(Iterable bootstrappers) async { 14 | final containerState = new ContainerState(new IoCContainer()); 15 | return new Application._( 16 | containerState.state, 17 | new List.unmodifiable( 18 | (await Future.wait( 19 | bootstrappers.map((b) => _timeline(containerState, b)) 20 | )).where((b) => b != null) 21 | ) 22 | ); 23 | } 24 | 25 | static Future _timeline(ContainerState state, Bootstrapper bootstrapper) async { 26 | try { 27 | await bootstrapper._run(state); 28 | return bootstrapper; 29 | } catch (e, s) { 30 | TraceFormatter.print(e, s); 31 | return null; 32 | } 33 | } 34 | 35 | Future exit() async { 36 | await Future.wait(bootstrappers.map/**/((b) async => b._exit())); 37 | } 38 | } 39 | 40 | /// In [Bootstrapper]s, methods can be annotated with hooks to attach scripts to the overall 41 | /// setup and teardown procedure of the application. 42 | abstract class Hook { 43 | Hook._(); 44 | 45 | /// This hook will be the first to run, and should contain plugin interal initialization. 46 | static const init = 'bootstrap:init'; 47 | /// This hook will be run after every [Bootstrapper] has run its [init] hook(s). 48 | static const afterInit = 'bootstrap:afterInit'; 49 | 50 | /// Any bindings that must be made to the IoCContainer before the actual [bindings] hook is 51 | /// run will be made in a [beforeBindings] hook. 52 | static const beforeBindings = 'bootstrap:beforeBindings'; 53 | /// This hook is the main place to make bindings in the global [IoCContainer] that will be 54 | /// available throughout the lifetime of the application. 55 | static const bindings = 'bootstrap:bindings'; 56 | /// This hook will run just after every [Bootstrapper] has run its main [bindings] hook(s). 57 | static const afterBindings = 'bootstrap:afterBindings'; 58 | 59 | /// This hook is run in preparation of the main [interaction] hook(s). 60 | static const beforeInteraction = 'bootstrap:beforeInteraction'; 61 | /// This hook should contain any cross [Bootstrapper] communication. 62 | static const interaction = 'bootstrap:interaction'; 63 | /// This hook is run just after the main [interaction] hook(s). 64 | static const afterInteraction = 'bootstrap:afterInteraction'; 65 | 66 | /// This hook is run in preparation of the main [reaction] hook(s). 67 | static const beforeReaction = 'bootstrap:beforeReaction'; 68 | /// This hook should contain any scripts that is a reaction to the messages sent to other 69 | /// [Bootstrapper]s in the [interaction] hook(s). 70 | static const reaction = 'bootstrap:reaction'; 71 | /// This hook is run just after the main [reaction] hook(s). 72 | static const afterReaction = 'bootstrap:afterReaction'; 73 | 74 | /// This hook will be run after the program has received the exit command. 75 | static const beforeExit = 'bootstrap:beforeExit'; 76 | /// This final hook contains the deinitialization scripts, in which all ports and streams 77 | /// must be closed. 78 | static const exit = 'bootstrap:exit'; 79 | } 80 | 81 | /// Bootstrappers bootstrap different components of an Embla application. Every Bootstrapper 82 | /// adds one or more hooks to itself, each in which it can run initialization or deinitialization 83 | /// scripts. Check out the [Hook] class for information about each hook. 84 | /// 85 | /// Each hook _can_ return either an [IoCContainer] or a [Future], in which case 86 | /// all changes to the IoC Container will be applied to the global container in the application. 87 | /// 88 | /// class MyBoostrapper extends Bootstrapper { 89 | /// @Hook.init 90 | /// init() { 91 | /// print('MyBootstrapper is starting!'); 92 | /// } 93 | /// 94 | /// @Hook.bindings 95 | /// bindings() { 96 | /// return container 97 | /// .bind(SomeInterface, toSubtype: SomeImplementation); 98 | /// } 99 | /// } 100 | abstract class Bootstrapper { 101 | IoCContainer get container => _containerState?.state ?? (throw new Exception('To manually run hooks, first run $runtimeType#attach()')); 102 | 103 | InstanceMirror _mirror; 104 | ContainerState _containerState; 105 | Iterable __methods; 106 | 107 | void attach([IoCContainer container]) { 108 | _containerState = new ContainerState(container ?? new IoCContainer()); 109 | } 110 | 111 | Future _run(ContainerState containerState) async { 112 | _containerState = containerState; 113 | _mirror = reflect(this); 114 | __methods = _methods(); 115 | await _callAnnotation(__methods, Hook.init); 116 | await _callAnnotation(__methods, Hook.afterInit); 117 | await _callAnnotation(__methods, Hook.beforeBindings); 118 | await _callAnnotation(__methods, Hook.bindings); 119 | await _callAnnotation(__methods, Hook.afterBindings); 120 | await _callAnnotation(__methods, Hook.beforeInteraction); 121 | await _callAnnotation(__methods, Hook.interaction); 122 | await _callAnnotation(__methods, Hook.afterInteraction); 123 | await _callAnnotation(__methods, Hook.beforeReaction); 124 | await _callAnnotation(__methods, Hook.reaction); 125 | await _callAnnotation(__methods, Hook.afterReaction); 126 | } 127 | 128 | Future _exit() async { 129 | try { 130 | await _callAnnotation(__methods, Hook.beforeExit); 131 | await _callAnnotation(__methods, Hook.exit); 132 | } catch (e, s) { 133 | TraceFormatter.print(e, s); 134 | } 135 | } 136 | 137 | Future _callAnnotation(Iterable methods, annotation) async { 138 | await Future.wait( 139 | _annotated(methods, annotation) 140 | .map/**/((c) => _runClosure(c)) 141 | ); 142 | } 143 | 144 | Iterable _methods() { 145 | return _mirror.type.instanceMembers.values 146 | .where((i) => i is MethodMirror && i.isRegularMethod); 147 | } 148 | 149 | Iterable _annotated(Iterable methods, annotation) { 150 | return methods.where((m) => m.metadata.any((t) => t.reflectee == annotation)); 151 | } 152 | 153 | Future _runClosure(MethodMirror method) async { 154 | ClosureMirror closure = _mirror.getField(method.simpleName) as ClosureMirror; 155 | await traceIdentifier_PJ9ZCKjkkKPFYjgH3jkW(() async { 156 | final response = await _containerState.state.resolve(closure.reflectee); 157 | if (response is IoCContainer) { 158 | _containerState.state = response; 159 | } 160 | }); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/bootstrap.dart: -------------------------------------------------------------------------------- 1 | import 'dart:mirrors'; 2 | import 'dart:io'; 3 | import 'application.dart'; 4 | import 'src/util/process_handler.dart'; 5 | import 'dart:isolate'; 6 | 7 | main(List arguments, SendPort sendExitCommandPort) async { 8 | final exitCommandPort = new ReceivePort(); 9 | 10 | final processHandler = new ProcessHandler( 11 | init: () { 12 | print('Press Ctrl+C to exit'); 13 | return Application.boot(_findConfig()); 14 | }, 15 | deinit: (Application app) { 16 | return app.exit(); 17 | } 18 | ); 19 | 20 | if (sendExitCommandPort != null) { 21 | sendExitCommandPort.send(exitCommandPort.sendPort); 22 | 23 | exitCommandPort.listen((_) { 24 | processHandler.interrupt(); 25 | }); 26 | } 27 | 28 | await processHandler.run(); 29 | 30 | exitCommandPort.close(); 31 | } 32 | 33 | List _findConfig() { 34 | final library = currentMirrorSystem().libraries[Platform.script]; 35 | if (library == null) { 36 | throw new Exception('The script entry point is not a library'); 37 | } 38 | 39 | final emblaMethod = library.declarations[#embla]; 40 | if (emblaMethod == null) { 41 | throw new Exception('Found no [embla] getter in ${Platform.script}'); 42 | } 43 | 44 | return library 45 | .getField(#embla) 46 | .reflectee as List; 47 | } 48 | -------------------------------------------------------------------------------- /lib/container.dart: -------------------------------------------------------------------------------- 1 | import 'src/container.dart' as implementation; 2 | 3 | class InjectionException implements Exception { 4 | final String message; 5 | final InjectionException because; 6 | 7 | InjectionException(this.message, [this.because]); 8 | 9 | String toString() => 'InjectionException: $reason'; 10 | 11 | String get reason => '$message' + 12 | (because == null ? '' : ' because\n ${because.reason}'); 13 | } 14 | 15 | class BindingException implements Exception { 16 | final String message; 17 | 18 | BindingException(this.message); 19 | 20 | String toString() => 'BindingException: $message'; 21 | } 22 | 23 | abstract class IoCContainer { 24 | factory IoCContainer() => new implementation.IoCContainer(); 25 | 26 | /// Creates a new instance of the provided [type], resolving the 27 | /// constructor parameters automatically. 28 | /// 29 | /// class A {} 30 | /// 31 | /// class B { 32 | /// B(A a) { 33 | /// print(a); // Instance of 'A' 34 | /// } 35 | /// } 36 | dynamic/*=T*/ make/**/(Type/* extends T*/ type); 37 | 38 | /// Finds all parameters in a [function] and injects the dependencies automatically. 39 | /// 40 | /// resolve((Dependency dep) { 41 | /// print(dep); // Instance of 'Dependency' 42 | /// }); 43 | dynamic/*=T*/ resolve/**/(Function/* -> T*/ function); 44 | 45 | /// Returns a lazily resolved version of [function], injecting the arguments passed in to the 46 | /// curried function into the original function by their type. 47 | /// 48 | /// var curried = curry((Dependency dependency, String string) { 49 | /// print('$dependency, $string'); 50 | /// }); 51 | /// curried("Hello!"); // Instance of 'Dependency', Hello! 52 | /// 53 | /// You can invoke the curried function just like you would the original, but leave out any 54 | /// argument any the container will inject the arguments you don't supply. 55 | /// 56 | /// var curried = curry((String s, int i, {a, Dependency b, c}) {}); 57 | /// curried("s", 0, a: 0, c: 0); // Named parameter b will be injected 58 | Function curry(Function function); 59 | 60 | /// Returns a copy of the container, with an implementation of a [type] bound. Subsequent requests 61 | /// for injections of the [type] will be provided the [to] value. 62 | /// 63 | /// [to] can be either a [Type] that is an instance of, or a subtype of [type], or an instance 64 | /// of [type]. 65 | /// 66 | /// bind(A, to: B).make(A); // Instance of 'B' 67 | /// bind(A, to: new A()).make(A); // Instance of 'A' 68 | /// bind(A, to: new B()).make(A); // Instance of 'B' 69 | IoCContainer bind(Type type, {to}); 70 | 71 | /// Acts like [bind], but instead of binding to a specific type request, it gets bound to named 72 | /// parameters with this [name]. 73 | /// 74 | /// Without bindings, named parameters will only be resolved if they don't have a default value. 75 | /// If they have no default value, they will be resolved if they can, but will be set 76 | /// to `null` if the resolve fails. 77 | /// 78 | /// [to] will only be bound to parameters with a compatible type. Therefore, multiple bindings 79 | /// can be made to the same [name]. The value that will be injected is chosen based on the type 80 | /// annotation of the parameter: 81 | /// 82 | /// final c = bindName("x", to: 123) 83 | /// .bindName("x", to: "string") 84 | /// .bindName("x", to: SomeImplementation); 85 | /// 86 | /// c.resolve(({y}) { 87 | /// print(y); // null <-- nothing is bound to y, so attempt to make or else return null 88 | /// }); 89 | /// c.resolve(({num x}) { 90 | /// print(x); // 123 <-- bound int is assignable to num 91 | /// }); 92 | /// c.resolve(({String x}) { 93 | /// print(x); // "string" <-- bound String is assignable to String 94 | /// }); 95 | /// c.resolve(({SomeClass x}) { 96 | /// print(x); // Instance of 'SomeClass' <-- no binding is assignable, so attempt to make 97 | /// }); 98 | /// c.resolve(({SomeClass x: defaultValue}) { 99 | /// print(x); // defaultValue <-- no binding is assignable, so use default value 100 | /// }); 101 | /// c.resolve(({SomeInterface x}) { 102 | /// print(x); // Instance of 'SomeImplementation' 103 | /// // ^-- bound SomeImplementation is assignable to SomeInterface 104 | /// }); 105 | /// c.resolve(({x}) { 106 | /// print(x); // 123 <-- everything is assignable to dynamic, so choose first binding 107 | /// }); 108 | IoCContainer bindName(String name, {to}); 109 | 110 | /// Returns a copy of the container, with a decorator for this [type] bound. 111 | /// 112 | /// class Greeter { 113 | /// greet() => "Hello, world"; 114 | /// } 115 | /// 116 | /// class ExclaimDecorator implements Greeter { 117 | /// final Greeter _super; 118 | /// ExclaimDecorator(this._super); 119 | /// greet() => _super.greet() + '!'; 120 | /// } 121 | /// 122 | /// class ScreamDecorator implements Greeter { 123 | /// final Greeter _super; 124 | /// ScreamDecorator(this._super); 125 | /// greet() => _super.greet().toUpperCase(); 126 | /// } 127 | /// 128 | /// print( 129 | /// decorate(Greeter, withDecorator: ExclaimDecorator) 130 | /// .decorate(Greeter, withDecorator: ExclaimDecorator) 131 | /// .decorate(Greeter, withDecorator: ScreamDecorator) 132 | /// .make(Greeter) 133 | /// .greet() 134 | /// ); // HELLO, WORLD!! 135 | IoCContainer decorate(Type type, {Type withDecorator}); 136 | 137 | /// Combines all bindings from the [other] container with this one's, and returns a new 138 | /// instance that combines the bindings. The bindings in [other] takes precedence. 139 | /// 140 | /// var a = new IoCContainer().bind(int, toValue: 1).bind(String, toValue: "x"); 141 | /// var b = new IoCContainer().bind(int, toValue: 2); 142 | /// var c = a.apply(b); 143 | /// c.resolve((int i, String s) { 144 | /// print(s); // x 145 | /// print(i); // 2 146 | /// }); 147 | IoCContainer apply(IoCContainer other); 148 | } 149 | -------------------------------------------------------------------------------- /lib/http.dart: -------------------------------------------------------------------------------- 1 | import 'package:shelf/shelf_io.dart' as shelf_io; 2 | import 'dart:io' hide HttpException; 3 | import 'application.dart'; 4 | import 'src/http/response_maker.dart'; 5 | import 'src/http/pipeline.dart'; 6 | import 'src/http/request_response.dart'; 7 | import 'src/http/http_exceptions.dart'; 8 | import 'src/util/trace_formatting.dart'; 9 | import 'src/http/context.dart'; 10 | import 'dart:async'; 11 | import 'dart:convert'; 12 | 13 | export 'src/http/context.dart'; 14 | export 'src/http/http_exceptions.dart'; 15 | export 'src/http/request_response.dart'; 16 | export 'src/http/pipeline.dart'; 17 | export 'src/http/middleware.dart'; 18 | export 'src/http/routing.dart'; 19 | 20 | typedef Future ServerFactory(dynamic host, int port); 21 | 22 | class HttpBootstrapper extends Bootstrapper { 23 | final PipelineFactory pipeline; 24 | final String host; 25 | final int port; 26 | final ResponseMaker _responseMaker = new ResponseMaker(); 27 | final ServerFactory _serverFactory; 28 | 29 | factory HttpBootstrapper({ 30 | String host: 'localhost', 31 | int port: 1337, 32 | PipelineFactory pipeline 33 | }) { 34 | return new HttpBootstrapper.internal( 35 | HttpServer.bind, 36 | host, 37 | port, 38 | pipeline 39 | ); 40 | } 41 | 42 | HttpBootstrapper.internal( 43 | this._serverFactory, 44 | this.host, 45 | this.port, 46 | this.pipeline 47 | ); 48 | 49 | @Hook.bindings 50 | bindings() async { 51 | return container 52 | .bind(HttpServer, to: await _serverFactory(this.host, this.port)); 53 | } 54 | 55 | @Hook.interaction 56 | start(HttpServer server) { 57 | return runInContext(container, () { 58 | final pipe = pipeline(container); 59 | server.autoCompress = true; 60 | server.listen((request) { 61 | request.response.bufferOutput = true; 62 | shelf_io.handleRequest( 63 | request, 64 | (_) => handleRequest(_, pipe).then((r) { 65 | final c = new StreamController>(); 66 | 67 | r.read().listen(c.add, onDone: c.close, onError: (e, s) { 68 | c.add(UTF8.encode(""" 69 |
70 |

71 | An error was thrown after headers were sent. 72 |

73 |

${e.toString().replaceAll("<", "<")}

74 |
${s.toString().replaceAll("<", "<")}
75 |
76 | """)); 77 | }); 78 | 79 | return r.change(body: c.stream); 80 | }) 81 | ); 82 | }); 83 | print('Server started on http://${server.address.host}:${server.port}'); 84 | }); 85 | } 86 | 87 | Future handleRequest(Request request, Pipeline pipe) async { 88 | Future run() async { 89 | context.container = context.container 90 | .bind(Request, to: request); 91 | 92 | try { 93 | return await pipe(request); 94 | } on NoResponseFromPipelineException { 95 | return new Response.notFound('Not Found'); 96 | } on HttpException catch(e) { 97 | return _responseMaker.parse(e.body).status(e.statusCode); 98 | } catch(e, s) { 99 | TraceFormatter.print(e, s); 100 | return new Response.internalServerError(body: 'Internal Server Error'); 101 | } 102 | } 103 | return runInContext/*>*/(container, run); 104 | } 105 | 106 | @Hook.exit 107 | stop(HttpServer server) async { 108 | await server.close(); 109 | print('Server stopped'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/http_annotations.dart: -------------------------------------------------------------------------------- 1 | import 'http.dart'; 2 | 3 | class RouteHandler { 4 | final Iterable methods; 5 | final String path; 6 | 7 | const RouteHandler(this.methods, this.path); 8 | 9 | Middleware toHandler(Function method, [String fallbackPath]) { 10 | return Route.match(methods, path ?? fallbackPath, handler(method)); 11 | } 12 | } 13 | 14 | class Get extends RouteHandler { 15 | const Get([String path]) : super(const ['GET', 'HEAD'], path); 16 | } 17 | 18 | class Post extends RouteHandler { 19 | const Post([String path]) : super(const ['POST'], path); 20 | } 21 | 22 | class Put extends RouteHandler { 23 | const Put([String path]) : super(const ['PUT'], path); 24 | } 25 | 26 | class Patch extends RouteHandler { 27 | const Patch([String path]) : super(const ['PATCH'], path); 28 | } 29 | 30 | class Update extends RouteHandler { 31 | const Update([String path]) : super(const ['UPDATE'], path); 32 | } 33 | 34 | class Delete extends RouteHandler { 35 | const Delete([String path]) : super(const ['DELETE'], path); 36 | } 37 | 38 | class Options extends RouteHandler { 39 | const Options([String path]) : super(const ['OPTIONS'], path); 40 | } 41 | 42 | class All extends RouteHandler { 43 | const All([String path]) 44 | : super(const ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'UPDATE', 'DELETE'], path); 45 | } 46 | -------------------------------------------------------------------------------- /lib/http_basic_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'src/http/middleware/input_parser_middleware.dart'; 2 | import 'src/http/middleware/logger_middleware.dart'; 3 | import 'src/http/middleware/remove_trailing_slash_middleware.dart'; 4 | 5 | export 'src/http/middleware/conditional_middleware.dart'; 6 | export 'src/http/middleware/error_handler_middleware.dart'; 7 | export 'src/http/middleware/forwarder_middleware.dart'; 8 | export 'src/http/middleware/input_parser_middleware.dart'; 9 | export 'src/http/middleware/logger_middleware.dart'; 10 | export 'src/http/middleware/remove_trailing_slash_middleware.dart'; 11 | export 'src/http/middleware/static_files_middleware.dart'; 12 | export 'src/http/middleware/pub_middleware.dart'; 13 | 14 | /// Reasonable basic middleware that should probably always be used. 15 | Iterable get basicMiddleware => [ 16 | RemoveTrailingSlashMiddleware, 17 | LoggerMiddleware, 18 | InputParserMiddleware, 19 | ]; 20 | -------------------------------------------------------------------------------- /lib/src/container.dart: -------------------------------------------------------------------------------- 1 | import 'dart:mirrors'; 2 | 3 | import '../container.dart' as interface show IoCContainer; 4 | import '../container.dart' show InjectionException, BindingException; 5 | 6 | typedef T Factory(IoCContainer container); 7 | typedef InstanceMirror Invoker(List positional, Map named); 8 | 9 | class Nothing { 10 | const Nothing(); 11 | } 12 | 13 | const nothing = const Nothing(); 14 | 15 | Map merge/**/(Map a, Map b) { 16 | return new Map.unmodifiable({}..addAll(a)..addAll(b)); 17 | } 18 | 19 | class IoCContainer implements interface.IoCContainer { 20 | final Map bindings; 21 | final Map> nameBindings; 22 | 23 | IoCContainer({this.bindings: const {}, this.nameBindings: const {}}); 24 | 25 | @override 26 | IoCContainer bind(Type type, {to: nothing}) { 27 | _checkForNull('type', type); 28 | _checkForNothing('to', to); 29 | _checkAssignable( 30 | type, 31 | to: to, 32 | message: '${to is Type ? to : to.runtimeType} cannot be assigned to $type' 33 | ); 34 | return new IoCContainer( 35 | bindings: merge/**/(bindings, {type: _makeFactory(to)}), 36 | nameBindings: nameBindings 37 | ); 38 | } 39 | 40 | void _checkAssignable(Type type, {to, String message}) { 41 | if (!_isAssignable(type, to: to)) { 42 | throw new BindingException(message); 43 | } 44 | } 45 | 46 | bool _isAssignable(Type type, {to}) { 47 | if (to == Null || to == null) return true; 48 | return _getType(to).isAssignableTo(reflectType(type)); 49 | } 50 | 51 | Factory _makeFactory(binding) { 52 | if (binding is Type) { 53 | return (container) => container.make(binding); 54 | } 55 | return (_) => binding; 56 | } 57 | 58 | @override 59 | IoCContainer bindName(String name, {to: nothing}) { 60 | _checkForNull('name', name); 61 | _checkForNothing('to', to); 62 | final nameSymbol = new Symbol(name); 63 | 64 | _checkForMoreGenericExistingNameBinding(name, nameSymbol, _getType(to)); 65 | 66 | return new IoCContainer( 67 | bindings: bindings, 68 | nameBindings: merge/*>*/(nameBindings, { 69 | new Symbol(name): merge/**/(nameBindings[nameSymbol] ?? {}, { 70 | _getType(to): _makeFactory(to) 71 | }) 72 | }) 73 | ); 74 | } 75 | 76 | void _checkForMoreGenericExistingNameBinding(String name, Symbol symbol, TypeMirror newBindType) { 77 | if (!nameBindings.containsKey(symbol)) return; 78 | 79 | final bindings = nameBindings[symbol]; 80 | 81 | final moreGenericBinding = bindings.keys 82 | .firstWhere((t) => newBindType.isSubtypeOf(t), orElse: () => null); 83 | 84 | if (moreGenericBinding == null) return; 85 | 86 | throw new BindingException( 87 | 'Cannot bind ${newBindType.reflectedType} to parameter named $name ' 88 | 'because ${moreGenericBinding.reflectedType} is already bound to the same name ' 89 | 'and is more generic' 90 | ); 91 | } 92 | 93 | TypeMirror _getType(binding) { 94 | if (binding is Type) { 95 | return reflectType(binding); 96 | } 97 | return reflect(binding).type; 98 | } 99 | 100 | @override 101 | Function curry(Function function) { 102 | _checkForNull('function', function); 103 | return new CurriedFunction(this, reflect(function)); 104 | } 105 | 106 | @override 107 | IoCContainer decorate(Type type, {Type withDecorator}) { 108 | _checkForNull('type', type); 109 | _checkForNull('withDecorator', withDecorator); 110 | 111 | if (!reflectType(withDecorator).isSubtypeOf(reflectType(type))) { 112 | throw new BindingException( 113 | '$withDecorator is not a subtype of $type. ' 114 | 'Decorators must implement their decoratee.' 115 | ); 116 | } 117 | 118 | if (!_hasTypeAnnotationInConstructor(withDecorator, type)) { 119 | throw new BindingException( 120 | '$withDecorator must inject $type in its constructor to be ' 121 | 'a valid decorator.' 122 | ); 123 | } 124 | 125 | return new IoCContainer( 126 | bindings: merge/**/(bindings, { 127 | type: (container) { 128 | final decoratee = make(type); 129 | return container.bind(type, to: decoratee).make(withDecorator); 130 | } 131 | }), 132 | nameBindings: nameBindings 133 | ); 134 | } 135 | 136 | bool _hasTypeAnnotationInConstructor(Type type, Type annotation) { 137 | final ClassMirror classMirror = reflectType(type); 138 | final MethodMirror constructor = classMirror.declarations[classMirror.simpleName]; 139 | return constructor.parameters.any((p) => p.type.reflectedType == annotation); 140 | } 141 | 142 | @override 143 | dynamic/*=T*/ make/**/(Type/* extends T*/ type) { 144 | _checkForNull('type', type); 145 | if (_hasBinding(type)) { 146 | return _binding/**/(type); 147 | } 148 | 149 | final TypeMirror typeMirror = reflectType(type); 150 | if (typeMirror is! ClassMirror) 151 | throw new InjectionException('Only classes can be instantiated. $type is not a class.'); 152 | final ClassMirror classMirror = typeMirror as ClassMirror; 153 | final MethodMirror constructor = classMirror.declarations[typeMirror.simpleName]; 154 | try { 155 | return _resolve/**/( 156 | constructor?.parameters ?? [], 157 | (p, n) => classMirror.newInstance(const Symbol(''), p, n) 158 | ); 159 | } on AbstractClassInstantiationError { 160 | throw new InjectionException('$type is abstract.'); 161 | } on NoSuchMethodError catch(e) { 162 | if ('$e'.startsWith("No constructor '$type' declared in class '$type'.")) { 163 | throw new InjectionException('$type has no default constructor.'); 164 | } 165 | rethrow; 166 | } on InjectionException catch(e) { 167 | throw new InjectionException('Cannot instantiate $type', e); 168 | } 169 | } 170 | 171 | bool _hasBinding(Type type) => bindings.containsKey(type); 172 | 173 | dynamic/*=T*/ _binding/**/(Type type) => bindings[type](this) as dynamic/*=T*/; 174 | 175 | @override 176 | dynamic/*=T*/ resolve/**/(Function/* -> T*/ function) { 177 | _checkForNull('function', function); 178 | final ClosureMirror closureMirror = reflect(function); 179 | return _resolve/**/( 180 | closureMirror.function.parameters, 181 | closureMirror.apply 182 | ); 183 | } 184 | 185 | dynamic/*=T*/ _resolve/**/(Iterable params, Invoker invoker) { 186 | return invoker(_positional(params), _named(params)).reflectee as dynamic/*=T*/; 187 | } 188 | 189 | List _positional(Iterable params) { 190 | return params 191 | .where((p) => !p.isNamed) 192 | .map((p) => p.type.reflectedType) 193 | .map(make) 194 | .toList(); 195 | } 196 | 197 | Map _named(Iterable params) { 198 | final Iterable p = params.where((p) => p.isNamed); 199 | return new Map.fromIterables( 200 | p.map((p) => p.simpleName), 201 | p.map((p) { 202 | if (nameBindings.containsKey(p.simpleName)) { 203 | return _getBoundNamed(p); 204 | } 205 | if (p.defaultValue.reflectee != null) { 206 | return p.defaultValue.reflectee; 207 | } 208 | return _makeOrNull(p.type.reflectedType); 209 | }) 210 | ); 211 | } 212 | 213 | dynamic/*=T*/ _getBoundNamed/**/(ParameterMirror named) { 214 | final record = nameBindings[named.simpleName]; 215 | final requestedType = named.type; 216 | for (final boundType in record.keys) { 217 | if (boundType.isAssignableTo(requestedType)) { 218 | return record[boundType](this) as dynamic/*=T*/; 219 | } 220 | } 221 | final returnValue = named.defaultValue?.reflectee ?? _makeOrNull(requestedType.reflectedType); 222 | return returnValue as dynamic/*=T*/; 223 | } 224 | 225 | dynamic/*=T*/ _makeOrNull/**/(Type type) { 226 | try { 227 | return make/**/(type); 228 | } on InjectionException { 229 | return null; 230 | } 231 | } 232 | 233 | void _checkForNull(String paramName, value) { 234 | if (value == null) { 235 | throw new ArgumentError.notNull(paramName); 236 | } 237 | } 238 | 239 | void _checkForNothing(String paramName, value) { 240 | if (value == nothing) { 241 | throw new ArgumentError.value(null, paramName, 'Must be supplied'); 242 | } 243 | } 244 | 245 | @override 246 | IoCContainer apply(interface.IoCContainer other) { 247 | if (other is! IoCContainer) { 248 | throw new ArgumentError.value(other, 'other', 'Must be the same implementation'); 249 | } 250 | final IoCContainer otherC = other; 251 | return new IoCContainer( 252 | bindings: merge(bindings, otherC.bindings), 253 | nameBindings: merge(nameBindings, otherC.nameBindings) 254 | ); 255 | } 256 | } 257 | 258 | class CurriedFunction implements Function { 259 | final IoCContainer container; 260 | final ClosureMirror closure; 261 | 262 | CurriedFunction(this.container, this.closure); 263 | 264 | @override 265 | bool operator ==(Object other) { 266 | return other.hashCode == hashCode; 267 | } 268 | 269 | noSuchMethod(Invocation invocation) { 270 | if (invocation.memberName != #call) { 271 | return super.noSuchMethod(invocation); 272 | } 273 | return closure.apply( 274 | _positional(invocation.positionalArguments).toList(), 275 | _named(invocation.namedArguments) 276 | ).reflectee; 277 | } 278 | 279 | Iterable _positional(Iterable args) { 280 | return closure 281 | .function 282 | .parameters 283 | .where((p) => !p.isNamed) 284 | .map((parameter) { 285 | for (final a in args) { 286 | if (reflect(a).type.isAssignableTo(parameter.type)) { 287 | return a; 288 | } 289 | } 290 | return container.make(parameter.type.reflectedType); 291 | }); 292 | } 293 | 294 | Map _named(Map args) { 295 | final params = closure.function.parameters.where((p) => p.isNamed); 296 | return new Map.fromIterables( 297 | params.map((p) => p.simpleName), 298 | params.map((p) { 299 | if (args.containsKey(p.simpleName)) { 300 | return args[p.simpleName]; 301 | } 302 | return container.make(p.type.reflectedType); 303 | }) 304 | ); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /lib/src/http/context.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection' show MapBase; 3 | import '../../container.dart'; 4 | 5 | const _contextZoneValueKey = '__emblaHttpContext'; 6 | 7 | HttpContext _testingContext; 8 | 9 | setUpContextForTesting({Map values}) { 10 | _testingContext = new HttpContext({ 11 | '__container': new IoCContainer() 12 | }..addAll(values ?? {})); 13 | } 14 | 15 | bool get isInHttpContext { 16 | return _testingContext != null || Zone.current[_contextZoneValueKey] != null; 17 | } 18 | 19 | HttpContext get context => _testingContext ?? Zone.current[_contextZoneValueKey] 20 | ?? (throw new Exception('You have to [runInContext] before accessing the current context')); 21 | 22 | dynamic/*=T*/ runInContext/**/(IoCContainer container, dynamic/*=T*/ body()) { 23 | return runZoned(body, zoneValues: { 24 | _contextZoneValueKey: new HttpContext({ 25 | '__container': container 26 | }) 27 | }) as dynamic/*=T*/; 28 | } 29 | 30 | class HttpContext extends MapBase { 31 | final Map _values; 32 | 33 | HttpContext(this._values); 34 | 35 | void _ensureIsntReservedKey(String key) { 36 | if (key.startsWith('__')) { 37 | throw new UnimplementedError('Context keys cannot start with two underscores.'); 38 | } 39 | } 40 | 41 | Map get locals => (_values['__locals'] ??= {}) as Map; 42 | IoCContainer get container => _values['__container'] as IoCContainer; 43 | void set container(IoCContainer other) { 44 | _values['__container'] = container.apply(other); 45 | } 46 | 47 | operator [](Object key) { 48 | _ensureIsntReservedKey('$key'); 49 | return _values[key]; 50 | } 51 | 52 | @override 53 | void operator []=(String key, value) { 54 | _ensureIsntReservedKey(key); 55 | return _values[key] = value; 56 | } 57 | 58 | @override 59 | void clear() { 60 | throw new UnsupportedError("Clearing the context will mess things up."); 61 | } 62 | 63 | @override 64 | Iterable get keys => _values.keys.where((k) => !k.startsWith('__')); 65 | 66 | @override 67 | remove(Object key) { 68 | _ensureIsntReservedKey(key); 69 | _values.remove(key); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/http/error_template.dart: -------------------------------------------------------------------------------- 1 | import 'package:embla/src/http/request_response.dart'; 2 | import 'dart:async'; 3 | import 'dart:convert'; 4 | import 'dart:io'; 5 | import 'package:embla/src/http/http_exceptions.dart'; 6 | import 'package:stack_trace/stack_trace.dart'; 7 | import 'package:embla/src/util/trace_formatting.dart'; 8 | 9 | class ErrorTemplate { 10 | static final _packagesPath = Platform.packageRoot ?? Directory.current.path + '/packages'; 11 | 12 | static const String _hljs = 13 | r"""!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function i(e){var n,t,r,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",t=/\blang(?:uage)?-([\w-]+)\b/i.exec(i))return w(t[1])?t[1]:"no-highlight";for(i=i.split(/\s+/),n=0,r=i.length;r>n;n++)if(w(i[n])||a(i[n]))return i[n]}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3==i.nodeType?a+=i.nodeValue.length:1==i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!=r[0].offset?e[0].offset"}function u(e){f+=""}function c(e){("start"==e.event?o:u)(e.node)}for(var s=0,f="",l=[];e.length||r.length;){var g=i();if(f+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g==e){l.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g==e&&g.length&&g[0].offset==s);l.reverse().forEach(o)}else"start"==g[0].event?l.push(g[0].node):l.pop(),c(g.splice(0,1)[0])}return f+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):Object.keys(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\b\w+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"==e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var f=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=f.length?t(f.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){for(var t=0;t";return i+=e+'">',i+n+o}function h(){if(!k.k)return n(M);var e="",t=0;k.lR.lastIndex=0;for(var r=k.lR.exec(M);r;){e+=n(M.substr(t,r.index-t));var a=g(k,r);a?(B+=a[1],e+=p(a[0],n(r[0]))):e+=n(r[0]),t=k.lR.lastIndex,r=k.lR.exec(M)}return e+n(M.substr(t))}function d(){var e="string"==typeof k.sL;if(e&&!R[k.sL])return n(M);var t=e?f(k.sL,M,!0,y[k.sL]):l(M,k.sL.length?k.sL:void 0);return k.r>0&&(B+=t.r),e&&(y[k.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=void 0!==k.sL?d():h(),M=""}function v(e,n){L+=e.cN?p(e.cN,"",!0):"",k=Object.create(e,{parent:{value:k}})}function m(e,n){if(M+=e,void 0===n)return b(),0;var t=o(n,k);if(t)return t.skip?M+=n:(t.eB&&(M+=n),b(),t.rB||t.eB||(M=n)),v(t,n),t.rB?0:n.length;var r=u(k,n);if(r){var a=k;a.skip?M+=n:(a.rE||a.eE||(M+=n),b(),a.eE&&(M=n));do k.cN&&(L+=""),k.skip||(B+=k.r),k=k.parent;while(k!=r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,k))throw new Error('Illegal lexeme "'+n+'" for mode "'+(k.cN||"")+'"');return M+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var x,k=i||N,y={},L="";for(x=k;x!=N;x=x.parent)x.cN&&(L=p(x.cN,"",!0)+L);var M="",B=0;try{for(var C,j,I=0;;){if(k.t.lastIndex=I,C=k.t.exec(t),!C)break;j=m(t.substr(I,C.index-I),C[0]),I=C.index+j}for(m(t.substr(I)),x=k;x.parent;x=x.parent)x.cN&&(L+="");return{r:B,value:L,language:e,top:k}}catch(O){if(-1!=O.message.indexOf("Illegal"))return{r:0,value:n(t)};throw O}}function l(e,t){t=t||E.languages||Object.keys(R);var r={r:0,value:n(e)},a=r;return t.forEach(function(n){if(w(n)){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}}),a.language&&(r.second_best=a),r}function g(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,n){return n.replace(/\t/g,E.tabReplace)})),E.useBR&&(e=e.replace(/\n/g,"
")),e}function p(e,n,t){var r=n?x[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function h(e){var n=i(e);if(!a(n)){var t;E.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var r=t.textContent,o=n?f(n,r,!0):l(r),s=u(t);if(s.length){var h=document.createElementNS("http://www.w3.org/1999/xhtml","div");h.innerHTML=o.value,o.value=c(s,u(h),r)}o.value=g(o.value),e.innerHTML=o.value,e.className=p(e.className,n,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function d(e){E=o(E,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,h)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=R[n]=t(e);r.aliases&&r.aliases.forEach(function(e){x[e]=n})}function N(){return Object.keys(R)}function w(e){return e=(e||"").toLowerCase(),R[e]||R[x[e]]}var E={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},R={},x={};return e.highlight=f,e.highlightAuto=l,e.fixMarkup=g,e.highlightBlock=h,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("dart",function(e){var t={cN:"subst",b:"\\$\\{",e:"}",k:"true false null this is new super"},r={cN:"string",v:[{b:"r'''",e:"'''"},{b:'r""" 14 | '"""' 15 | "',e:'" 16 | '"""' 17 | r"""'},{b:"r'",e:"'",i:"\\n"},{b:'r"',e:'"',i:"\\n"},{b:"'''",e:"'''",c:[e.BE,t]},{b:'""" 18 | '"""' 19 | "',e:'" 20 | '"""' 21 | r"""',c:[e.BE,t]},{b:"'",e:"'",i:"\\n",c:[e.BE,t]},{b:'"',e:'"',i:"\\n",c:[e.BE,t]}]};t.c=[e.CNM,r];var n={keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"};return{k:n,c:[r,e.C("/\\*\\*","\\*/",{sL:"markdown"}),e.C("///","$",{sL:"markdown"}),e.CLCM,e.CBCM,{cN:"class",bK:"class interface",e:"{",eE:!0,c:[{bK:"extends implements"},e.UTM]},e.CNM,{cN:"meta",b:"@[A-Za-z]+"},{b:"=>"}]}});"""; 22 | 23 | static const String _style = r""" 24 | body { 25 | background: #f8f8f8; 26 | margin: 0; 27 | font-family: "Roboto Slab", sans-serif; 28 | color: #333; } 29 | 30 | .container { 31 | background: #fefefe; 32 | display: flex; 33 | flex-direction: column; 34 | max-width: 80em; 35 | margin: auto; 36 | height: 100vh; 37 | box-shadow: 0 -5em 6em -5em black; } 38 | .container__header { 39 | padding: 1rem; } 40 | .container__header__code { 41 | color: #555; 42 | font-size: 0.9em; 43 | font-family: "Cousine", monospace; } 44 | .container__header__heading { 45 | margin: 0; } 46 | .container__content { 47 | flex: 1; 48 | display: flex; } 49 | 50 | .source { 51 | flex: 2; 52 | display: flex; } 53 | .source__pre { 54 | overflow: auto; 55 | padding: 1em; 56 | border-top: 1px solid rgba(51, 51, 51, 0.1); 57 | font-family: "Cousine", monospace; 58 | line-height: 1.4; 59 | font-size: 1.1em; 60 | margin: 0; 61 | background: #f8f8f8; 62 | flex: 1; } 63 | .source__pre:not(.active) { 64 | display: none; } 65 | .source__highlighted-line { 66 | background: #de4530; 67 | display: inline-block; 68 | width: 100%; 69 | box-shadow: -1em 0 0 0.3em #de4530, 1em 0 0 0.3em #de4530; 70 | color: white; } 71 | .source__highlighted-line * { 72 | color: white !important; } 73 | .source__line-number .hljs-number { 74 | color: #a2a2a2; } 75 | 76 | .stack { 77 | flex: 1; 78 | overflow: auto; } 79 | .stack__list { 80 | margin: 0; 81 | list-style: none; 82 | padding: 0; 83 | font-family: "Cousine", monospace; 84 | cursor: pointer; } 85 | .stack__list__item { 86 | width: 100%; 87 | border: 0; 88 | background: #fefefe; 89 | margin: 0; 90 | font: inherit; 91 | text-align: left; 92 | -webkit-appearance: none; 93 | -moz-appearance: none; 94 | -ms-appearance: none; 95 | -o-appearance: none; 96 | appearance: none; 97 | padding: 1em; 98 | cursor: pointer; 99 | border-top: 1px solid rgba(51, 51, 51, 0.1); } 100 | .stack__list__item__file { 101 | overflow: hidden; 102 | white-space: nowrap; 103 | text-overflow: ellipsis; 104 | position: relative; 105 | display: block; 106 | margin-bottom: 0.2em; } 107 | .active .stack__list__item { 108 | border: 1px solid rgba(51, 51, 51, 0.1); 109 | background-color: #3385ff; 110 | color: #fefefe; } 111 | .active .stack__list__item:active { 112 | color: #fefefe; } 113 | .active + li .stack__list__item { 114 | border-top: 0; } 115 | .stack__list__item:active { 116 | color: #555; } 117 | .stack__list__item__function { 118 | margin: 0; } 119 | 120 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 121 | /* Tomorrow Comment */ 122 | .hljs-comment, 123 | .hljs-quote { 124 | color: #8e908c; } 125 | 126 | /* Tomorrow Red */ 127 | .hljs-variable, 128 | .hljs-template-variable, 129 | .hljs-tag, 130 | .hljs-name, 131 | .hljs-selector-id, 132 | .hljs-selector-class, 133 | .hljs-regexp, 134 | .hljs-deletion { 135 | color: #c82829; } 136 | 137 | /* Tomorrow Orange */ 138 | .hljs-number, 139 | .hljs-built_in, 140 | .hljs-builtin-name, 141 | .hljs-literal, 142 | .hljs-type, 143 | .hljs-params, 144 | .hljs-meta, 145 | .hljs-link { 146 | color: #f5871f; } 147 | 148 | /* Tomorrow Yellow */ 149 | .hljs-attribute { 150 | color: #eab700; } 151 | 152 | /* Tomorrow Green */ 153 | .hljs-string, 154 | .hljs-symbol, 155 | .hljs-bullet, 156 | .hljs-addition { 157 | color: #718c00; } 158 | 159 | /* Tomorrow Blue */ 160 | .hljs-title, 161 | .hljs-section { 162 | color: #4271ae; } 163 | 164 | /* Tomorrow Purple */ 165 | .hljs-keyword, 166 | .hljs-selector-tag { 167 | color: #8959a8; } 168 | 169 | .hljs-emphasis { 170 | font-style: italic; } 171 | 172 | .hljs-strong { 173 | font-weight: bold; } 174 | """; 175 | 176 | ErrorTemplate(); 177 | 178 | Future catchError(error, StackTrace stack) async { 179 | final code = _code(error); 180 | return new Response( 181 | code, 182 | body: renderError(error, stack, code), 183 | headers: { 184 | 'Content-Type': ContentType.HTML.toString() 185 | } 186 | ); 187 | } 188 | 189 | Stream> renderError(error, StackTrace stack, int code) { 190 | return _template(error, new Chain.forTrace(stack), code).map(UTF8.encode); 191 | } 192 | 193 | int _code(error) { 194 | if (error is HttpBadRequestException) return 400; 195 | if (error is HttpUnauthorizedException) return 401; 196 | if (error is HttpPaymentRequiredException) return 402; 197 | if (error is HttpForbiddenException) return 403; 198 | if (error is HttpNotFoundException) return 404; 199 | if (error is HttpMethodNotAllowedException) return 405; 200 | if (error is HttpNotAcceptableException) return 406; 201 | if (error is HttpProxyAuthenticationRequiredException) return 407; 202 | if (error is HttpRequestTimeoutException) return 408; 203 | if (error is HttpConflictException) return 409; 204 | if (error is HttpGoneException) return 410; 205 | if (error is HttpLengthRequiredException) return 411; 206 | if (error is HttpPreconditionFailedException) return 412; 207 | if (error is HttpPayloadTooLargeException) return 413; 208 | if (error is HttpURITooLongException) return 414; 209 | if (error is HttpUnsupportedMediaTypeException) return 415; 210 | if (error is HttpRangeNotSatisfiableException) return 416; 211 | if (error is HttpExpectationFailedException) return 417; 212 | if (error is HttpImATeapotException) return 418; 213 | if (error is HttpAuthenticationTimeoutException) return 419; 214 | if (error is HttpMisdirectedRequestException) return 421; 215 | if (error is HttpUnprocessableEntityException) return 422; 216 | if (error is HttpLockedException) return 423; 217 | if (error is HttpFailedDependencyException) return 424; 218 | if (error is HttpUpgradeRequiredException) return 426; 219 | if (error is HttpPreconditionRequiredException) return 428; 220 | if (error is HttpTooManyRequestsException) return 429; 221 | if (error is HttpRequestHeaderFieldsTooLargeException) return 431; 222 | if (error is HttpInternalServerErrorException) return 500; 223 | if (error is HttpNotImplementedException) return 501; 224 | if (error is HttpBadGatewayException) return 502; 225 | if (error is HttpServiceUnavailableException) return 503; 226 | if (error is HttpGatewayTimeoutException) return 504; 227 | if (error is HttpVersionNotSupportedException) return 505; 228 | if (error is HttpVariantAlsoNegotiatesException) return 506; 229 | if (error is HttpInsufficientStorageException) return 507; 230 | if (error is HttpLoopDetectedException) return 508; 231 | if (error is HttpNotExtendedException) return 510; 232 | if (error is HttpNetworkAuthenticationRequiredException) return 511; 233 | return 500; 234 | } 235 | 236 | String _esc(input) { 237 | return input.toString() 238 | .replaceAll('<', '<') 239 | .replaceAll('>', '>'); 240 | } 241 | 242 | Uri _resolveUri(Uri input) { 243 | if (input.scheme == 'dart' || !input.path.endsWith('.dart')) { 244 | return null; 245 | } 246 | if (input.scheme == 'package') { 247 | return Uri.parse('$_packagesPath/${input.path}'); 248 | } 249 | return input; 250 | } 251 | 252 | Stream _template(error, Chain chain, int statusCode) async* { 253 | final formatter = new TraceFormatter(chain); 254 | final List frames = formatter.frames.reversed.toList(); 255 | final String message = error.toString() == "Instance of '${error.runtimeType}'" 256 | ? error.runtimeType.toString() 257 | : error.toString(); 258 | 259 | yield ''' 260 | 261 | 262 | 263 | 264 | 265 | $statusCode – ${_esc(message)} 266 | 267 | 268 | 269 | 270 | 271 | 272 |
273 |
274 | Error $statusCode 275 |

${_esc(message)}

276 |
277 |
278 | 295 |
'''; 296 | 297 | for (final Frame frame in frames) { 298 | final index = frames.indexOf(frame); 299 | final uri = _resolveUri(frame.uri); 300 | final Stream file = uri == null ? new Stream.empty() : new File.fromUri(uri).openRead().map(UTF8.decode).map((s) => _esc(s)); 301 | yield '
';
302 |           var line = 0;
303 |           final lines = (await file.join()).split('\n');
304 |           final lineColumnWidth = lines.length.toString().length + 1;
305 |           yield lines.map((s) {
306 |             line++;
307 |             final linePrefix = '${line.toString().padRight(lineColumnWidth)}';
308 |             if (line == frame.line) {
309 |               return '$linePrefix $s';
310 |             }
311 |             return '$linePrefix $s';
312 |           }).join('\n');
313 |           yield '
'; 314 | } 315 | 316 | yield ''' 317 |
318 |
319 |
320 | 321 | 356 | 357 | 358 | 359 | '''; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /lib/src/http/http_exceptions.dart: -------------------------------------------------------------------------------- 1 | class HttpException implements Exception { 2 | final int statusCode; 3 | final body; 4 | 5 | HttpException._(this.statusCode, this.body); 6 | 7 | factory HttpException(int statusCode, [body]) { 8 | if (statusCode == 400) return new HttpBadRequestException(body); 9 | if (statusCode == 401) return new HttpUnauthorizedException(body); 10 | if (statusCode == 402) return new HttpPaymentRequiredException(body); 11 | if (statusCode == 403) return new HttpForbiddenException(body); 12 | if (statusCode == 404) return new HttpNotFoundException(body); 13 | if (statusCode == 405) return new HttpMethodNotAllowedException(body); 14 | if (statusCode == 406) return new HttpNotAcceptableException(body); 15 | if (statusCode == 407) return new HttpProxyAuthenticationRequiredException(body); 16 | if (statusCode == 408) return new HttpRequestTimeoutException(body); 17 | if (statusCode == 409) return new HttpConflictException(body); 18 | if (statusCode == 410) return new HttpGoneException(body); 19 | if (statusCode == 411) return new HttpLengthRequiredException(body); 20 | if (statusCode == 412) return new HttpPreconditionFailedException(body); 21 | if (statusCode == 413) return new HttpPayloadTooLargeException(body); 22 | if (statusCode == 414) return new HttpURITooLongException(body); 23 | if (statusCode == 415) return new HttpUnsupportedMediaTypeException(body); 24 | if (statusCode == 416) return new HttpRangeNotSatisfiableException(body); 25 | if (statusCode == 417) return new HttpExpectationFailedException(body); 26 | if (statusCode == 418) return new HttpImATeapotException(body); 27 | if (statusCode == 419) return new HttpAuthenticationTimeoutException(body); 28 | if (statusCode == 421) return new HttpMisdirectedRequestException(body); 29 | if (statusCode == 422) return new HttpUnprocessableEntityException(body); 30 | if (statusCode == 423) return new HttpLockedException(body); 31 | if (statusCode == 424) return new HttpFailedDependencyException(body); 32 | if (statusCode == 426) return new HttpUpgradeRequiredException(body); 33 | if (statusCode == 428) return new HttpPreconditionRequiredException(body); 34 | if (statusCode == 429) return new HttpTooManyRequestsException(body); 35 | if (statusCode == 431) return new HttpRequestHeaderFieldsTooLargeException(body); 36 | if (statusCode == 500) return new HttpInternalServerErrorException(body); 37 | if (statusCode == 501) return new HttpNotImplementedException(body); 38 | if (statusCode == 502) return new HttpBadGatewayException(body); 39 | if (statusCode == 503) return new HttpServiceUnavailableException(body); 40 | if (statusCode == 504) return new HttpGatewayTimeoutException(body); 41 | if (statusCode == 505) return new HttpVersionNotSupportedException(body); 42 | if (statusCode == 506) return new HttpVariantAlsoNegotiatesException(body); 43 | if (statusCode == 507) return new HttpInsufficientStorageException(body); 44 | if (statusCode == 508) return new HttpLoopDetectedException(body); 45 | if (statusCode == 510) return new HttpNotExtendedException(body); 46 | if (statusCode == 511) return new HttpNetworkAuthenticationRequiredException(body); 47 | return new HttpException._(statusCode, body); 48 | } 49 | 50 | String toString() => '$runtimeType: $body'; 51 | } 52 | 53 | class HttpBadRequestException extends HttpException { 54 | HttpBadRequestException([body]) : super._(400, body ?? 'Bad Request'); 55 | } 56 | 57 | class HttpUnauthorizedException extends HttpException { 58 | HttpUnauthorizedException([body]) : super._(401, body ?? 'Unauthorized'); 59 | } 60 | 61 | class HttpPaymentRequiredException extends HttpException { 62 | HttpPaymentRequiredException([body]) : super._(402, body ?? 'Payment Required'); 63 | } 64 | 65 | class HttpForbiddenException extends HttpException { 66 | HttpForbiddenException([body]) : super._(403, body ?? 'Forbidden'); 67 | } 68 | 69 | class HttpNotFoundException extends HttpException { 70 | HttpNotFoundException([body]) : super._(404, body ?? 'Not Found'); 71 | } 72 | 73 | class HttpMethodNotAllowedException extends HttpException { 74 | HttpMethodNotAllowedException([body]) : super._(405, body ?? 'Method Not Allowed'); 75 | } 76 | 77 | class HttpNotAcceptableException extends HttpException { 78 | HttpNotAcceptableException([body]) : super._(406, body ?? 'Not Acceptable'); 79 | } 80 | 81 | class HttpProxyAuthenticationRequiredException extends HttpException { 82 | HttpProxyAuthenticationRequiredException([body]) : super._(407, body ?? 'Proxy Authentication Required'); 83 | } 84 | 85 | class HttpRequestTimeoutException extends HttpException { 86 | HttpRequestTimeoutException([body]) : super._(408, body ?? 'Request Timeout'); 87 | } 88 | 89 | class HttpConflictException extends HttpException { 90 | HttpConflictException([body]) : super._(409, body ?? 'Conflict'); 91 | } 92 | 93 | class HttpGoneException extends HttpException { 94 | HttpGoneException([body]) : super._(410, body ?? 'Gone'); 95 | } 96 | 97 | class HttpLengthRequiredException extends HttpException { 98 | HttpLengthRequiredException([body]) : super._(411, body ?? 'Length Required'); 99 | } 100 | 101 | class HttpPreconditionFailedException extends HttpException { 102 | HttpPreconditionFailedException([body]) : super._(412, body ?? 'Precondition Failed'); 103 | } 104 | 105 | class HttpPayloadTooLargeException extends HttpException { 106 | HttpPayloadTooLargeException([body]) : super._(413, body ?? 'Payload Too Large'); 107 | } 108 | 109 | class HttpURITooLongException extends HttpException { 110 | HttpURITooLongException([body]) : super._(414, body ?? 'URI Too Long'); 111 | } 112 | 113 | class HttpUnsupportedMediaTypeException extends HttpException { 114 | HttpUnsupportedMediaTypeException([body]) : super._(415, body ?? 'Unsupported Media Type'); 115 | } 116 | 117 | class HttpRangeNotSatisfiableException extends HttpException { 118 | HttpRangeNotSatisfiableException([body]) : super._(416, body ?? 'Range Not Satisfiable'); 119 | } 120 | 121 | class HttpExpectationFailedException extends HttpException { 122 | HttpExpectationFailedException([body]) : super._(417, body ?? 'Expectation Failed'); 123 | } 124 | 125 | class HttpImATeapotException extends HttpException { 126 | HttpImATeapotException([body]) : super._(418, body ?? 'I\'m a Teapot'); 127 | } 128 | 129 | class HttpAuthenticationTimeoutException extends HttpException { 130 | HttpAuthenticationTimeoutException([body]) : super._(419, body ?? 'Authentication Timeout'); 131 | } 132 | 133 | class HttpMisdirectedRequestException extends HttpException { 134 | HttpMisdirectedRequestException([body]) : super._(421, body ?? 'Misdirected Request'); 135 | } 136 | 137 | class HttpUnprocessableEntityException extends HttpException { 138 | HttpUnprocessableEntityException([body]) : super._(422, body ?? 'Unprocessable Entity'); 139 | } 140 | 141 | class HttpLockedException extends HttpException { 142 | HttpLockedException([body]) : super._(423, body ?? 'Locked'); 143 | } 144 | 145 | class HttpFailedDependencyException extends HttpException { 146 | HttpFailedDependencyException([body]) : super._(424, body ?? 'Failed Dependency'); 147 | } 148 | 149 | class HttpUpgradeRequiredException extends HttpException { 150 | HttpUpgradeRequiredException([body]) : super._(426, body ?? 'Upgrade Required'); 151 | } 152 | 153 | class HttpPreconditionRequiredException extends HttpException { 154 | HttpPreconditionRequiredException([body]) : super._(428, body ?? 'Precondition Required'); 155 | } 156 | 157 | class HttpTooManyRequestsException extends HttpException { 158 | HttpTooManyRequestsException([body]) : super._(429, body ?? 'Too Many Requests'); 159 | } 160 | 161 | class HttpRequestHeaderFieldsTooLargeException extends HttpException { 162 | HttpRequestHeaderFieldsTooLargeException([body]) : super._(431, body ?? 'Request Header Fields Too Large'); 163 | } 164 | 165 | class HttpInternalServerErrorException extends HttpException { 166 | HttpInternalServerErrorException([body]) : super._(500, body ?? 'Internal Server Error'); 167 | } 168 | 169 | class HttpNotImplementedException extends HttpException { 170 | HttpNotImplementedException([body]) : super._(501, body ?? 'Not Implemented'); 171 | } 172 | 173 | class HttpBadGatewayException extends HttpException { 174 | HttpBadGatewayException([body]) : super._(502, body ?? 'Bad Gateway'); 175 | } 176 | 177 | class HttpServiceUnavailableException extends HttpException { 178 | HttpServiceUnavailableException([body]) : super._(503, body ?? 'Service Unavailable'); 179 | } 180 | 181 | class HttpGatewayTimeoutException extends HttpException { 182 | HttpGatewayTimeoutException([body]) : super._(504, body ?? 'Gateway Timeout'); 183 | } 184 | 185 | class HttpVersionNotSupportedException extends HttpException { 186 | HttpVersionNotSupportedException([body]) : super._(505, body ?? 'HTTP Version Not Supported'); 187 | } 188 | 189 | class HttpVariantAlsoNegotiatesException extends HttpException { 190 | HttpVariantAlsoNegotiatesException([body]) : super._(506, body ?? 'Variant Also Negotiates'); 191 | } 192 | 193 | class HttpInsufficientStorageException extends HttpException { 194 | HttpInsufficientStorageException([body]) : super._(507, body ?? 'Insufficient Storage'); 195 | } 196 | 197 | class HttpLoopDetectedException extends HttpException { 198 | HttpLoopDetectedException([body]) : super._(508, body ?? 'Loop Detected'); 199 | } 200 | 201 | class HttpNotExtendedException extends HttpException { 202 | HttpNotExtendedException([body]) : super._(510, body ?? 'Not Extended'); 203 | } 204 | 205 | class HttpNetworkAuthenticationRequiredException extends HttpException { 206 | HttpNetworkAuthenticationRequiredException([body]) : super._(511, body ?? 'Network Authentication Required'); 207 | } 208 | -------------------------------------------------------------------------------- /lib/src/http/middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:shelf/shelf.dart' as shelf hide Request, Response; 4 | 5 | import 'http_exceptions.dart'; 6 | import 'response_maker.dart'; 7 | 8 | export 'http_exceptions.dart'; 9 | export 'request_response.dart'; 10 | import 'request_response.dart'; 11 | 12 | import 'context.dart' as http_context; 13 | 14 | class Middleware { 15 | final ResponseMaker _responseMaker = new ResponseMaker(); 16 | shelf.Handler _innerHandler; 17 | 18 | http_context.HttpContext get context => http_context.context; 19 | 20 | abort([int statusCode = 500, body = 'Something went wrong']) { 21 | throw new HttpException(statusCode, body); 22 | } 23 | 24 | abortAuthenticationTimeout([body = 'Authentication Timeout']) { 25 | throw new HttpAuthenticationTimeoutException(body); 26 | } 27 | 28 | abortBadGateway([body = 'Bad Gateway']) { 29 | throw new HttpBadGatewayException(body); 30 | } 31 | 32 | abortBadRequest([body = 'Bad Request']) { 33 | throw new HttpBadRequestException(body); 34 | } 35 | 36 | abortConflict([body = 'Conflict']) { 37 | throw new HttpConflictException(body); 38 | } 39 | 40 | abortExpectationFailed([body = 'Expectation Failed']) { 41 | throw new HttpExpectationFailedException(body); 42 | } 43 | 44 | abortFailedDependency([body = 'Failed Dependency']) { 45 | throw new HttpFailedDependencyException(body); 46 | } 47 | 48 | abortForbidden([body = 'Forbidden']) { 49 | throw new HttpForbiddenException(body); 50 | } 51 | 52 | abortGatewayTimeout([body = 'Gateway Timeout']) { 53 | throw new HttpGatewayTimeoutException(body); 54 | } 55 | 56 | abortGone([body = 'Gone']) { 57 | throw new HttpGoneException(body); 58 | } 59 | 60 | abortImATeapot([body = 'I\'m a Teapot']) { 61 | throw new HttpImATeapotException(body); 62 | } 63 | 64 | abortInsufficientStorage([body = 'Insufficient Storage']) { 65 | throw new HttpInsufficientStorageException(body); 66 | } 67 | 68 | abortInternalServerError([body = 'Internal Server Error']) { 69 | throw new HttpInternalServerErrorException(body); 70 | } 71 | 72 | abortLengthRequired([body = 'Length Required']) { 73 | throw new HttpLengthRequiredException(body); 74 | } 75 | 76 | abortLocked([body = 'Locked']) { 77 | throw new HttpLockedException(body); 78 | } 79 | 80 | abortLoopDetected([body = 'Loop Detected']) { 81 | throw new HttpLoopDetectedException(body); 82 | } 83 | 84 | abortMethodNotAllowed([body = 'Method Not Allowed']) { 85 | throw new HttpMethodNotAllowedException(body); 86 | } 87 | 88 | abortMisdirectedRequest([body = 'Misdirected Request']) { 89 | throw new HttpMisdirectedRequestException(body); 90 | } 91 | 92 | abortNetworkAuthenticationRequired( 93 | [body = 'Network Authentication Required']) { 94 | throw new HttpNetworkAuthenticationRequiredException(body); 95 | } 96 | 97 | abortNotAcceptable([body = 'Not Acceptable']) { 98 | throw new HttpNotAcceptableException(body); 99 | } 100 | 101 | abortNotExtended([body = 'Not Extended']) { 102 | throw new HttpNotExtendedException(body); 103 | } 104 | 105 | abortNotFound([body = 'Not Found']) { 106 | throw new HttpNotFoundException(body); 107 | } 108 | 109 | abortNotImplemented([body = 'Not Implemented']) { 110 | throw new HttpNotImplementedException(body); 111 | } 112 | 113 | abortPayloadTooLarge([body = 'Payload Too Large']) { 114 | throw new HttpPayloadTooLargeException(body); 115 | } 116 | 117 | abortPaymentRequired([body = 'Payment Required']) { 118 | throw new HttpPaymentRequiredException(body); 119 | } 120 | 121 | abortPreconditionFailed([body = 'Precondition Failed']) { 122 | throw new HttpPreconditionFailedException(body); 123 | } 124 | 125 | abortPreconditionRequired([body = 'Precondition Required']) { 126 | throw new HttpPreconditionRequiredException(body); 127 | } 128 | 129 | abortProxyAuthenticationRequired([body = 'Proxy Authentication Required']) { 130 | throw new HttpProxyAuthenticationRequiredException(body); 131 | } 132 | 133 | abortRangeNotSatisfiable([body = 'Range Not Satisfiable']) { 134 | throw new HttpRangeNotSatisfiableException(body); 135 | } 136 | 137 | abortRequestHeaderFieldsTooLarge([body = 'Request Header Fields Too Large']) { 138 | throw new HttpRequestHeaderFieldsTooLargeException(body); 139 | } 140 | 141 | abortRequestTimeout([body = 'Request Timeout']) { 142 | throw new HttpRequestTimeoutException(body); 143 | } 144 | 145 | abortServiceUnavailable([body = 'Service Unavailable']) { 146 | throw new HttpServiceUnavailableException(body); 147 | } 148 | 149 | abortTooManyRequests([body = 'Too Many Requests']) { 150 | throw new HttpTooManyRequestsException(body); 151 | } 152 | 153 | abortUnauthorized([body = 'Unauthorized']) { 154 | throw new HttpUnauthorizedException(body); 155 | } 156 | 157 | abortUnprocessableEntity([body = 'Unprocessable Entity']) { 158 | throw new HttpUnprocessableEntityException(body); 159 | } 160 | 161 | abortUnsupportedMediaType([body = 'Unsupported Media Type']) { 162 | throw new HttpUnsupportedMediaTypeException(body); 163 | } 164 | 165 | abortUpgradeRequired([body = 'Upgrade Required']) { 166 | throw new HttpUpgradeRequiredException(body); 167 | } 168 | 169 | abortURITooLong([body = 'URI Too Long']) { 170 | throw new HttpURITooLongException(body); 171 | } 172 | 173 | abortVariantAlsoNegotiates([body = 'Variant Also Negotiates']) { 174 | throw new HttpVariantAlsoNegotiatesException(body); 175 | } 176 | 177 | abortVersionNotSupported([body = 'HTTP Version Not Supported']) { 178 | throw new HttpVersionNotSupportedException(body); 179 | } 180 | 181 | shelf.Handler call(shelf.Handler innerHandler) { 182 | _innerHandler = innerHandler; 183 | return handle; 184 | } 185 | 186 | Future handle(Request request) async { 187 | return await _innerHandler(request); 188 | } 189 | 190 | Response ok(anything) { 191 | return _responseMaker.parse(anything).status(200); 192 | } 193 | 194 | Response redirect(String location) { 195 | return new Response.found(location); 196 | } 197 | 198 | Response redirectPermanently(String location) { 199 | return new Response.movedPermanently(location); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/src/http/middleware/conditional_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import '../middleware.dart'; 3 | import '../pipeline.dart'; 4 | import '../request_response.dart'; 5 | 6 | class ConditionalMiddleware extends Middleware { 7 | final Function condition; 8 | final PipelineFactory _pipeline; 9 | Pipeline __pipeline; 10 | 11 | Pipeline get pipeline => __pipeline ??= _pipeline(); 12 | 13 | ConditionalMiddleware(this.condition, this._pipeline); 14 | 15 | @override Future handle(Request request) async { 16 | try { 17 | if (await context.container.resolve(condition)) { 18 | return await pipeline(request); 19 | } else { 20 | return await super.handle(request); 21 | } 22 | } on NoResponseFromPipelineException { 23 | return await super.handle(request); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/http/middleware/error_handler_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:mirrors'; 3 | 4 | import 'package:stack_trace/stack_trace.dart'; 5 | 6 | import '../../util/nothing.dart'; 7 | import '../middleware.dart'; 8 | import '../pipeline.dart'; 9 | import '../request_response.dart'; 10 | import 'handler_middleware.dart'; 11 | import '../../http/error_template.dart'; 12 | 13 | class BadErrorHandlerOrderException implements Exception { 14 | final String message; 15 | 16 | BadErrorHandlerOrderException(this.message); 17 | 18 | String toString() => 'BadErrorHandlerOrderException: $message'; 19 | } 20 | 21 | class ErrorHandlerCollection extends Middleware { 22 | final ErrorTemplate _errorTemplate = new ErrorTemplate(); 23 | final Map _catches; 24 | 25 | ErrorHandlerCollection([this._catches = const {}]); 26 | 27 | Future handle(Request request) { 28 | return super.handle(request).catchError((e, s) => _catch(request, e, s)) as Future; 29 | } 30 | 31 | ErrorHandlerCollection on(Type errorType, middlewareA, [middlewareB = nothing, middlewareC = nothing, 32 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 33 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 34 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 35 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 36 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 37 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 38 | final Iterable middlewareTokens = [middlewareA, middlewareB, 39 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 40 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 41 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 42 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ 43 | ].where((m) => m != nothing); 44 | for (final type in _catches.keys) { 45 | if (reflectType(type).isSubtypeOf(reflectType(errorType))) { 46 | throw new BadErrorHandlerOrderException( 47 | "$type is a subtype of $errorType and should therefore be " 48 | "added after $errorType in the handler chain.\n\n" 49 | " ErrorHandlerMiddleware\n" 50 | " .on($errorType, _handle$errorType)\n" 51 | " .on($type, _handle$type);\n" 52 | ); 53 | } 54 | } 55 | return new ErrorHandlerCollection( 56 | {} 57 | ..addAll(_catches) 58 | ..addAll({errorType: pipeActual(resolveMiddleware(middlewareTokens))}) 59 | ); 60 | } 61 | 62 | Future _catch(Request request, error, StackTrace stack) async { 63 | final mirror = reflect(error); 64 | for (final type in _catches.keys) { 65 | if (mirror.type.isAssignableTo(reflectType(type))) { 66 | context.container = context.container 67 | .bind(error.runtimeType, to: error) 68 | .bind(type, to: error) 69 | .bind(StackTrace, to: stack) 70 | .bind(Chain, to: new Chain.forTrace(stack)); 71 | 72 | return _catches[type](request); 73 | } 74 | } 75 | return _errorTemplate.catchError(error, stack); 76 | } 77 | } 78 | 79 | class ErrorHandlerMiddleware extends Middleware { 80 | final ErrorHandlerCollection _emptyCollection = new ErrorHandlerCollection(); 81 | 82 | Future handle(Request request) async { 83 | return await _emptyCollection.call(super.handle)(request); 84 | } 85 | 86 | static ErrorHandlerCollection catchAll(Function handler) { 87 | return new ErrorHandlerCollection({dynamic: pipeActual(resolveMiddleware([ 88 | new HandlerMiddleware(handler) 89 | ]))}); 90 | } 91 | 92 | static ErrorHandlerCollection on(Type errorType, middlewareA, [middlewareB = nothing, middlewareC = nothing, 93 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 94 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 95 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 96 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 97 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 98 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 99 | final Iterable middlewareTokens = [middlewareA, middlewareB, 100 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 101 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 102 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 103 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ 104 | ].where((m) => m != nothing); 105 | return new ErrorHandlerCollection({errorType: pipeActual(resolveMiddleware(middlewareTokens))}); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/http/middleware/forwarder_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import '../middleware.dart' hide HttpException; 5 | 6 | class ForwarderMiddleware extends Middleware { 7 | final String prefix; 8 | 9 | ForwarderMiddleware({String to}) 10 | : prefix = to ?? 'http://localhost:8080'; 11 | 12 | Uri _url(String path) { 13 | final url = prefix.replaceFirst(new RegExp(r'\/$'), '') 14 | + '/' 15 | + path.replaceFirst(new RegExp(r'^\/'), ''); 16 | 17 | return Uri.parse(url); 18 | } 19 | 20 | Future handle(Request request) async { 21 | final url = _url(request.requestedUri.path); 22 | final httpClient = new HttpClient(); 23 | 24 | try { 25 | final forwardRequest = await httpClient.openUrl(request.method, url); 26 | 27 | request.headers.forEach(forwardRequest.headers.add); 28 | 29 | await forwardRequest.addStream(request.read()); 30 | 31 | final response = await forwardRequest.close(); 32 | 33 | final responseHeaders = {}; 34 | 35 | response.headers.forEach((k, v) => responseHeaders[k] = v.join(';')); 36 | 37 | if (response.headers['content-encoding']?.indexOf('gzip') == 0) { 38 | responseHeaders.remove('content-encoding'); 39 | } 40 | 41 | return new Response( 42 | response.statusCode, 43 | body: response, 44 | headers: responseHeaders 45 | ); 46 | } on SocketException { 47 | abortBadGateway('Could not forward to $url'); 48 | } on HttpException catch(e) { 49 | abortInternalServerError(e.message); 50 | } finally { 51 | httpClient.close(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/http/middleware/handler_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../middleware.dart'; 4 | import '../context.dart'; 5 | import '../request_response.dart'; 6 | 7 | class HandlerMiddleware extends Middleware { 8 | final Function handler; 9 | 10 | HandlerMiddleware(this.handler); 11 | 12 | @override Future handle(Request request) async { 13 | final returnValue = await context.container.resolve(handler); 14 | if (returnValue is Response) return returnValue; 15 | return ok(returnValue); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/http/middleware/input_parser_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io' show ContentType; 4 | 5 | import '../middleware.dart'; 6 | import '../request_response.dart'; 7 | 8 | class Input { 9 | final dynamic body; 10 | 11 | Input(this.body); 12 | 13 | dynamic toJson() => body; 14 | 15 | String toString() { 16 | return 'Input($body)'; 17 | } 18 | } 19 | 20 | abstract class InputParser { 21 | Future parse(Stream> body, Encoding encoding); 22 | } 23 | 24 | class InputParserMiddleware extends Middleware { 25 | final RawInputParser _raw = new RawInputParser(); 26 | final UrlEncodedInputParser _urlencoded = new UrlEncodedInputParser(); 27 | final MultipartInputParser _multipart = new MultipartInputParser(); 28 | final JsonInputParser _json = new JsonInputParser(); 29 | 30 | @override Future handle(Request request) async { 31 | context.container = context.container 32 | .bind(Input, to: await _getInput(request)); 33 | 34 | return await super.handle(request.change(body: null)); 35 | } 36 | 37 | ContentType _contentType(Request request) { 38 | if (!request.headers.containsKey('Content-Type')) { 39 | return ContentType.TEXT; 40 | } 41 | return ContentType.parse(request.headers['Content-Type']); 42 | } 43 | 44 | Future _getInput(Request request) async { 45 | if (['GET', 'HEAD'].contains(request.method)) { 46 | return new Input(request.url.queryParameters); 47 | } 48 | 49 | final contentType = _contentType(request); 50 | final parser = _parser(contentType); 51 | 52 | return new Input(await parser.parse(request.read(), request.encoding ?? UTF8)); 53 | } 54 | 55 | InputParser _parser(ContentType contentType) { 56 | if (contentType.mimeType == 'application/json') { 57 | return _json; 58 | } 59 | if (contentType.mimeType == 'application/x-www-form-urlencoded') { 60 | return _urlencoded; 61 | } 62 | if (contentType.mimeType == 'application/multipart/form-data') { 63 | return _multipart; 64 | } 65 | return _raw; 66 | } 67 | } 68 | 69 | class JsonInputParser extends InputParser { 70 | Future parse(Stream> body, Encoding encoding) async { 71 | final asString = await body.map(encoding.decode).join('\n'); 72 | final output = JSON.decode(asString); 73 | if (output is Map) { 74 | return new Map.unmodifiable(output); 75 | } else if (output is Iterable) { 76 | return new List.unmodifiable(output); 77 | } 78 | return output; 79 | } 80 | } 81 | 82 | class MultipartInputParser extends InputParser { 83 | Future parse(Stream> body, Encoding encoding) { 84 | throw new UnimplementedError('Multipart format yet to be implemented'); 85 | } 86 | } 87 | 88 | class RawInputParser extends InputParser { 89 | Future parse(Stream> body, Encoding encoding) async { 90 | return parseString(await body.map(encoding.decode).join('\n')); 91 | } 92 | 93 | dynamic parseString(String value) { 94 | if (new RegExp(r'^(?:\d+\.?\d*|\.\d+)$').hasMatch(value)) { 95 | return num.parse(value); 96 | } 97 | if (new RegExp(r'^true$').hasMatch(value)) { 98 | return true; 99 | } 100 | if (new RegExp(r'^false$').hasMatch(value)) { 101 | return false; 102 | } 103 | return value == '' ? null : value; 104 | } 105 | } 106 | 107 | class UrlEncodedInputParser extends InputParser { 108 | final RawInputParser _raw = new RawInputParser(); 109 | 110 | Future> parse(Stream> body, Encoding encoding) async { 111 | final value = await body.map(encoding.decode).join('\n'); 112 | return parseQueryString(value); 113 | } 114 | 115 | // This is absolutely horrendous, but works 116 | Map parseQueryString(String query) { 117 | _verifyQueryString(query); 118 | 119 | final parts = query.split('&'); 120 | final Iterable rawKeys = parts.map((s) => s.split('=').map(Uri.decodeComponent).first); 121 | final List values = parts.map((s) => s.split('=').map(Uri.decodeComponent).last).toList(); 122 | final map = {}; 123 | final rootNamePattern = new RegExp(r'^([^\[]+)(.*)$'); 124 | final contPattern = new RegExp(r'^\[(.*?)\](.*)$'); 125 | dynamic nextValue() { 126 | return _raw.parseString(values.removeAt(0)); 127 | } 128 | for (var restOfKey in rawKeys) { 129 | final rootMatch = rootNamePattern.firstMatch(restOfKey); 130 | final rootKey = rootMatch[1]; 131 | final rootCont = rootMatch[2]; 132 | if (rootCont == '') { 133 | map[rootKey] = nextValue(); 134 | continue; 135 | } 136 | dynamic target = map; 137 | dynamic targetKey = rootKey; 138 | 139 | restOfKey = rootCont; 140 | 141 | while (contPattern.hasMatch(restOfKey)) { 142 | final contMatch = contPattern.firstMatch(restOfKey); 143 | final keyName = contMatch[1]; 144 | if (keyName == '') { 145 | target[targetKey] ??= []; 146 | (target[targetKey] as List).add(null); 147 | target = target[targetKey]; 148 | targetKey = target.length - 1; 149 | } else if (new RegExp(r'^\d+$').hasMatch(keyName)) { 150 | final List targetList = target[targetKey] ??= []; 151 | final index = int.parse(keyName); 152 | if (targetList.length == index) { 153 | targetList.add(null); 154 | } else { 155 | targetList[index] ??= null; 156 | } 157 | target = targetList; 158 | targetKey = index; 159 | } else { 160 | target[targetKey] ??= {}; 161 | (target[targetKey] as Map)[keyName] ??= null; 162 | target = target[targetKey]; 163 | targetKey = keyName; 164 | } 165 | restOfKey = contMatch[2]; 166 | } 167 | target[targetKey] = nextValue(); 168 | } 169 | return new Map.unmodifiable(map); 170 | } 171 | 172 | void _verifyQueryString(String query) { 173 | final pattern = new RegExp(r'^(?:[^\[]+(?:\[[^\[\]]*\])*(?:\=.*?)?)$'); 174 | if (!pattern.hasMatch(query)) { 175 | throw new Exception('$query is not a valid query string'); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/src/http/middleware/logger_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../middleware.dart'; 4 | import '../request_response.dart'; 5 | import '../pipeline.dart'; 6 | 7 | class LoggerMiddleware extends Middleware { 8 | @override Future handle(Request request) async { 9 | final beforeTime = new DateTime.now(); 10 | duration() => beforeTime.difference(new DateTime.now()); 11 | try { 12 | final response = await super.handle(request); 13 | final controller = new StreamController>(); 14 | var errored = false; 15 | response.read().listen(controller.add, onDone: () { 16 | controller.close(); 17 | if (errored) return; 18 | _log(request, response.statusCode, duration()); 19 | }, onError: (e, s) { 20 | _log(request, response.statusCode, duration(), failed: true); 21 | errored = true; 22 | controller.addError(e, s); 23 | }); 24 | return response.change(body: controller.stream); 25 | } on NoResponseFromPipelineException { 26 | _log(request, 404, duration()); 27 | rethrow; 28 | } on HttpException catch(e) { 29 | _log(request, e.statusCode, duration()); 30 | rethrow; 31 | } catch(e) { 32 | _log(request, 500, duration()); 33 | rethrow; 34 | } 35 | } 36 | 37 | void _log(Request request, int statusCode, Duration time, {bool failed: false}) { 38 | final url = request.handlerPath + request.url.path; 39 | final statusColor = () { 40 | if (statusCode >= 200 && statusCode < 300) { 41 | return 'green'; 42 | } 43 | if (statusCode >= 300 && statusCode < 400) { 44 | return 'magenta'; 45 | } 46 | if (statusCode >= 400 && statusCode < 500) { 47 | return 'yellow'; 48 | } 49 | if (statusCode >= 500 && statusCode < 600) { 50 | return 'red'; 51 | } 52 | return 'black'; 53 | }(); 54 | 55 | final timeInMilliseconds = time.abs().inMicroseconds / 1000; 56 | final timeColor = () { 57 | if (timeInMilliseconds > 800) { 58 | return 'red'; 59 | } 60 | if (timeInMilliseconds > 400) { 61 | return 'yellow'; 62 | } 63 | return 'gray'; 64 | }(); 65 | 66 | final suffix = failed ? 'THREW AFTER HEADERS WAS SENT' : ''; 67 | 68 | print('${new DateTime.now()} ' 69 | '<$statusColor>$statusCode ' 70 | '${request.method} ' 71 | '$url ' 72 | '<$timeColor>$timeInMilliseconds ms ' 73 | '$suffix'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/http/middleware/pub_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import '../../../http.dart'; 5 | import 'forwarder_middleware.dart'; 6 | import 'static_files_middleware.dart'; 7 | 8 | /// Enables integration with pub workflow. If the application is in development mode, 9 | /// requests will be forwarded to `pub serve`. If not in dev mode, *build/web* will be 10 | /// used for static assets. 11 | class PubMiddleware extends Middleware { 12 | final bool developmentMode; 13 | final int servePort; 14 | final String buildDir; 15 | 16 | Middleware __static; 17 | Middleware __forward; 18 | 19 | Middleware get _static => __static 20 | ??= new StaticFilesMiddleware(fileSystemPath: '$buildDir/web', defaultDocument: 'index.html'); 21 | Middleware get _forward => __forward 22 | ??= new ForwarderMiddleware(to: 'http://localhost:$servePort'); 23 | 24 | /// [developmentMode] will determine whether or not the app is in dev mode. If omitted, 25 | /// a check for an environment variable called `APP_ENV` having value 'development' will 26 | /// be the default check. 27 | /// 28 | /// [servePort] and [buildDir] can be used if the default serve and build options are not 29 | /// used with pub. 30 | PubMiddleware({ 31 | bool developmentMode, 32 | this.servePort: 8080, 33 | this.buildDir: 'build' 34 | }) : developmentMode = developmentMode ?? Platform.environment['APP_ENV'] == 'development'; 35 | 36 | @override Future handle(Request request) async { 37 | final response = developmentMode 38 | ? await _forward.handle(request) 39 | : await _static.handle(request); 40 | 41 | if (response.statusCode == 404) { 42 | abortNotFound(await response.readAsString()); 43 | } 44 | 45 | return response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/http/middleware/remove_trailing_slash_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../middleware.dart'; 4 | 5 | class RemoveTrailingSlashMiddleware extends Middleware { 6 | @override Future handle(Request request) async { 7 | if (request.url.path.endsWith('/')) { 8 | final url = request.handlerPath + request.url.path; 9 | return redirectPermanently('/' + url.split('/').where((s) => s.isNotEmpty).join('/')); 10 | } 11 | return await super.handle(request); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/http/middleware/static_files_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:shelf/shelf.dart' as shelf; 4 | import 'package:shelf_static/shelf_static.dart' as shelf_static; 5 | 6 | import '../middleware.dart'; 7 | 8 | class StaticFilesMiddleware extends Middleware { 9 | final String fileSystemPath; 10 | final bool serveFilesOutsidePath; 11 | final String defaultDocument; 12 | final bool listDirectories; 13 | shelf.Handler _handler; 14 | shelf.Handler get handler => _handler ??= shelf_static.createStaticHandler( 15 | fileSystemPath, 16 | serveFilesOutsidePath: serveFilesOutsidePath, 17 | defaultDocument: defaultDocument, 18 | listDirectories: listDirectories 19 | ); 20 | 21 | StaticFilesMiddleware({ 22 | this.fileSystemPath: 'web', 23 | this.serveFilesOutsidePath: false, 24 | this.defaultDocument, 25 | this.listDirectories: false 26 | }); 27 | 28 | @override Future handle(Request request) async { 29 | final Response response = await handler(request); 30 | if (response.statusCode == 404) { 31 | throw new HttpNotFoundException('No file found at ${fileSystemPath}/${request.url.path}'); 32 | } 33 | return response; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/http/pipeline.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:mirrors'; 3 | 4 | import 'package:shelf/shelf.dart' as shelf; 5 | 6 | import '../util/nothing.dart'; 7 | import 'middleware.dart'; 8 | import 'middleware/conditional_middleware.dart'; 9 | import 'middleware/handler_middleware.dart'; 10 | import 'request_response.dart'; 11 | import 'context.dart'; 12 | import '../../container.dart'; 13 | 14 | PipelineFactory pipe( 15 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 16 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 17 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 18 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 19 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 20 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 21 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 22 | final Iterable middlewareTokens = [middlewareA, middlewareB, 23 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 24 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 25 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 26 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ 27 | ].where((m) => m != nothing); 28 | return ([IoCContainer container]) => pipeActual(resolveMiddleware(middlewareTokens, container)); 29 | } 30 | 31 | Pipeline pipeActual(Iterable middleware) { 32 | final shelf.Pipeline pipe = middleware.fold/**/( 33 | const shelf.Pipeline(), 34 | (shelf.Pipeline pipeline, shelf.Middleware middleware) { 35 | return pipeline.addMiddleware(middleware); 36 | }); 37 | final shelf.Handler handler = pipe.addHandler((Request request) { 38 | throw new NoResponseFromPipelineException(); 39 | }); 40 | 41 | Future pipeline(Request request) async => handler(request); 42 | return pipeline; 43 | } 44 | 45 | Iterable resolveMiddleware(Iterable tokens, [IoCContainer container]) sync* { 46 | final ioc = container ?? context.container; 47 | for (final token in tokens) { 48 | if (token is shelf.Middleware) { 49 | yield token; 50 | } else if (token is Function) { 51 | yield handler(token); 52 | } else if (token is Type) { 53 | if (!reflectType(token).isAssignableTo(reflectType(Middleware))) { 54 | throw new ArgumentError('[$token] must be assignable to [Middleware]'); 55 | } 56 | yield ioc.make(token); 57 | } else if (token is Iterable) { 58 | yield* resolveMiddleware(token); 59 | } 60 | } 61 | } 62 | 63 | typedef Future Pipeline(Request request); 64 | 65 | typedef Pipeline PipelineFactory([IoCContainer container]); 66 | 67 | class NoResponseFromPipelineException implements Exception {} 68 | 69 | Middleware handler(Function handler) => new HandlerMiddleware(handler); 70 | 71 | shelf.Middleware middleware(shelf.Middleware middleware) { 72 | return (shelf.Handler innerHandler) { 73 | return middleware(innerHandler); 74 | }; 75 | } 76 | 77 | Middleware pipeIf(Function condition, [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 78 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 79 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 80 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 81 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 82 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 83 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 84 | return new ConditionalMiddleware(condition, pipe(middlewareA, middlewareB, 85 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 86 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 87 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 88 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/http/request_response.dart: -------------------------------------------------------------------------------- 1 | export 'package:shelf/shelf.dart' show Request, Response; 2 | -------------------------------------------------------------------------------- /lib/src/http/response_maker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:mirrors'; 3 | import 'dart:io'; 4 | import 'package:shelf/shelf.dart'; 5 | import 'dart:async'; 6 | 7 | class ResponseMaker { 8 | DataResponse parse(anything) { 9 | if (anything == null || 10 | anything is String || 11 | anything is bool || 12 | anything is num) { 13 | return new DataResponse((anything ?? '').toString(), ContentType.HTML); 14 | } 15 | 16 | if (anything is Stream) { 17 | final typeArgument = reflect(anything).type.typeArguments[0]; 18 | final json = typeArgument.reflectedType == dynamic 19 | || !typeArgument.isSubtypeOf(reflectType(String)); 20 | if (json) { 21 | return new DataResponse(_serialize(anything), ContentType.JSON); 22 | } 23 | return new DataResponse(_serialize(anything), ContentType.HTML); 24 | } 25 | 26 | return new DataResponse(_serialize(anything), ContentType.JSON); 27 | } 28 | 29 | _serialize(anything) { 30 | if (anything == null || 31 | anything is String || 32 | anything is bool || 33 | anything is num) { 34 | return anything; 35 | } 36 | 37 | if (anything is DateTime) { 38 | return anything.toUtc().toIso8601String(); 39 | } 40 | 41 | if (anything is Stream) { 42 | return anything.map((o) => _serialize(o)); 43 | } 44 | 45 | if (anything is Iterable) { 46 | return new List.unmodifiable(anything.toList().map(_serialize)); 47 | } 48 | 49 | if (anything is Map) { 50 | return new Map.unmodifiable( 51 | new Map.fromIterables( 52 | anything.keys.map((k) => '$k'), 53 | anything.values.map(_serialize) 54 | ) 55 | ); 56 | } 57 | 58 | final mirror = reflect(anything); 59 | if (mirror.type.instanceMembers.keys.contains(#toJson)) { 60 | return _serialize(mirror.reflectee.toJson()); 61 | } 62 | 63 | final members = mirror.type.instanceMembers.values 64 | .where((m) => m.owner is! ClassMirror || (m.owner as ClassMirror).reflectedType != Object) 65 | .where((m) => m.isGetter) 66 | .where((m) => !m.isPrivate); 67 | return new Map.unmodifiable(new Map.fromIterables( 68 | members.map((m) => m.simpleName).map(MirrorSystem.getName).map(_toSnakeCase), 69 | members.map((m) => _serialize(mirror.getField(m.simpleName).reflectee)) 70 | )); 71 | } 72 | 73 | String _toSnakeCase(String input) { 74 | return input 75 | .split('_') 76 | .expand((p) => p.split(new RegExp(r'(?=[A-Z])'))) 77 | .map((s) => s.toLowerCase()) 78 | .join('_'); 79 | } 80 | } 81 | 82 | class DataResponse { 83 | final body; 84 | final ContentType contentType; 85 | 86 | DataResponse(this.body, this.contentType); 87 | 88 | Response status(int statusCode) { 89 | final outputBody = () { 90 | if (body is Stream) { 91 | return jsonStream(body).map/*>*/(UTF8.encode); 92 | } else if (body is String) { 93 | return body; 94 | } else { 95 | return JSON.encode(body); 96 | } 97 | }(); 98 | return new Response(statusCode, body: outputBody, headers: { 99 | 'Content-Type': contentType.toString() 100 | }); 101 | } 102 | 103 | Stream jsonStream(Stream stream) async* { 104 | bool jsonTarget = contentType == ContentType.JSON; 105 | bool first = true; 106 | if (jsonTarget) yield '['; 107 | await for (final item in stream) { 108 | if (first) { 109 | first = false; 110 | } else { 111 | if (jsonTarget) yield ','; 112 | } 113 | if (jsonTarget) yield JSON.encode(item); 114 | else yield item; 115 | } 116 | if (jsonTarget) yield ']'; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/http/route_expander.dart: -------------------------------------------------------------------------------- 1 | class RouteExpander { 2 | 3 | String expand(String input, {bool excludeStar: false}) { 4 | return [input] 5 | .map(_normalizeSlashes) 6 | .map((_) => _expandStars(_, excludeStar)) 7 | .map(_expandWildcards) 8 | .map((_) => _exact(_, excludeStar)) 9 | .first; 10 | } 11 | 12 | String _exact(String input, bool excludeStar) { 13 | if (excludeStar) 14 | return '^$input'; 15 | return '^$input\$'; 16 | } 17 | 18 | String _expandWildcards(String input) { 19 | return input.replaceAll(new RegExp(r':\w+'), r'([^/]+)'); 20 | } 21 | 22 | String _normalizeSlashes(String input) { 23 | return input.split('/').where((s) => s.isNotEmpty).join(r'\/'); 24 | } 25 | 26 | String _expandStars(String input, bool exclude) { 27 | return input 28 | .replaceAll(new RegExp(r'^\*$'), exclude ? '' : r'(\/.*)?') 29 | .replaceAll(new RegExp(r'\\\/\*$'), exclude ? '' : r'(\/.*)?'); 30 | } 31 | 32 | String prefix(String pattern, String path) { 33 | final regex = new RegExp(expand(pattern)); 34 | final regexWithoutStar = new RegExp(expand(pattern, excludeStar: true)); 35 | final match = regex.firstMatch(path); 36 | if (match == null) throw new Exception('The route doesn\'t match'); 37 | return regexWithoutStar.firstMatch(path)[0]; 38 | } 39 | 40 | Map parseWildcards(String pattern, String url) { 41 | final patterns = pattern.split('/').iterator; 42 | final parts = url.split('/').iterator; 43 | final output = {}; 44 | while(patterns.moveNext()) { 45 | if (!parts.moveNext()) break; 46 | if (!patterns.current.startsWith(':')) continue; 47 | 48 | output[patterns.current.substring(1)] = parts.current; 49 | } 50 | return new Map.unmodifiable(output); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/http/routing.dart: -------------------------------------------------------------------------------- 1 | import 'middleware.dart'; 2 | import 'pipeline.dart'; 3 | import 'request_response.dart'; 4 | import 'dart:async'; 5 | import 'dart:mirrors'; 6 | import '../../http_annotations.dart'; 7 | import '../util/nothing.dart'; 8 | import 'route_expander.dart'; 9 | 10 | abstract class Controller extends Middleware { 11 | Pipeline __pipeline; 12 | Pipeline get _pipeline => __pipeline ??= _buildPipeline(); 13 | 14 | @override Future handle(Request request) async { 15 | try { 16 | return await _pipeline(request); 17 | } on NoResponseFromPipelineException { 18 | return await super.handle(request); 19 | } 20 | } 21 | 22 | Pipeline _buildPipeline() { 23 | final mirror = reflect(this); 24 | final routeMethods = mirror.type 25 | .instanceMembers.values 26 | .where((m) => m.metadata 27 | .any((i) => i.reflectee is RouteHandler)); 28 | return pipeActual(_resolveRoutes(mirror, routeMethods)); 29 | } 30 | 31 | Iterable _resolveRoutes( 32 | InstanceMirror mirror, 33 | Iterable routeMethods 34 | ) sync* { 35 | for (final routeMethod in routeMethods) { 36 | final annotations = routeMethod.metadata 37 | .where((i) => i.reflectee is RouteHandler) 38 | .map((i) => i.reflectee as RouteHandler); 39 | for (final annotation in annotations) { 40 | final Function method = mirror.getField(routeMethod.simpleName).reflectee; 41 | final fallback = MirrorSystem.getName(routeMethod.simpleName); 42 | yield annotation.toHandler(method, fallback == 'index' ? '' : fallback); 43 | } 44 | } 45 | } 46 | } 47 | 48 | class Route extends Middleware { 49 | final Iterable methods; 50 | final String path; 51 | final PipelineFactory pipeline; 52 | final RouteExpander _expander = new RouteExpander(); 53 | 54 | Pipeline __pipeline; 55 | 56 | factory Route( 57 | Iterable methods, 58 | String path, 59 | PipelineFactory pipeline) => 60 | new Route._( 61 | methods.map((m) => m.toUpperCase()), 62 | path.split('/').where((s) => s != '').join('/'), 63 | pipeline 64 | ); 65 | 66 | Route._(this.methods, this.path, this.pipeline); 67 | 68 | RegExp get regexPath => new RegExp(_expander.expand(path)); 69 | Pipeline get _pipeline => __pipeline ??= pipeline(); 70 | 71 | @override Future handle(Request request) async { 72 | if (!methods.contains(request.method)) { 73 | return await super.handle(request); 74 | } 75 | final url = request.url.path.split('/').where((s) => s.isNotEmpty).join('/'); 76 | final wildcards = _expander.parseWildcards(path, url); 77 | if (regexPath.hasMatch(url)) { 78 | try { 79 | for (final wc in wildcards.keys) { 80 | context.container = context.container 81 | .bindName(wc, to: wildcards[wc]); 82 | if (new RegExp(r'^\d+$').hasMatch(wildcards[wc])) { 83 | context.container = context.container 84 | .bindName(wc, to: int.parse(wildcards[wc])); 85 | } 86 | if (new RegExp(r'^\d*\.\d+$').hasMatch(wildcards[wc])) { 87 | context.container = context.container 88 | .bindName(wc, to: double.parse(wildcards[wc])); 89 | } 90 | } 91 | return await pipeline()(request.change( 92 | path: _expander.prefix(path, url) 93 | )); 94 | } on NoResponseFromPipelineException { 95 | return await super.handle(request); 96 | } 97 | } 98 | 99 | return await super.handle(request); 100 | } 101 | 102 | static Route all(String path, 103 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 104 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 105 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 106 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 107 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 108 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 109 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 110 | return new Route(['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'UPDATE', 'DELETE'], 111 | path, pipe(middlewareA, middlewareB, 112 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 113 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 114 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 115 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 116 | } 117 | 118 | static Route delete(String path, 119 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 120 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 121 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 122 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 123 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 124 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 125 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 126 | return new Route(['DELETE'], path, pipe(middlewareA, middlewareB, 127 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 128 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 129 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 130 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 131 | } 132 | 133 | static Route get(String path, 134 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 135 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 136 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 137 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 138 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 139 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 140 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 141 | return new Route(['GET', 'HEAD'], path, pipe(middlewareA, middlewareB, 142 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 143 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 144 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 145 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 146 | } 147 | 148 | static Route match(Iterable methods, String path, 149 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 150 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 151 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 152 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 153 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 154 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 155 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 156 | return new Route(methods, path, pipe(middlewareA, middlewareB, 157 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 158 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 159 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 160 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 161 | } 162 | 163 | static Route options(String path, 164 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 165 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 166 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 167 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 168 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 169 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 170 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 171 | return new Route(['OPTIONS'], path, pipe(middlewareA, middlewareB, 172 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 173 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 174 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 175 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 176 | } 177 | 178 | static Route patch(String path, 179 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 180 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 181 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 182 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 183 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 184 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 185 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 186 | return new Route(['PATCH'], path, pipe(middlewareA, middlewareB, 187 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 188 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 189 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 190 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 191 | } 192 | 193 | static Route post(String path, 194 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 195 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 196 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 197 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 198 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 199 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 200 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 201 | return new Route(['POST'], path, pipe(middlewareA, middlewareB, 202 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 203 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 204 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 205 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 206 | } 207 | 208 | static Route put(String path, 209 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 210 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 211 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 212 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 213 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 214 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 215 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 216 | return new Route(['PUT'], path, pipe(middlewareA, middlewareB, 217 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 218 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 219 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 220 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 221 | } 222 | 223 | static Route update(String path, 224 | [middlewareA = nothing, middlewareB = nothing, middlewareC = nothing, 225 | middlewareD = nothing, middlewareE = nothing, middlewareF = nothing, middlewareG = nothing, 226 | middlewareH = nothing, middlewareI = nothing, middlewareJ = nothing, middlewareK = nothing, 227 | middlewareL = nothing, middlewareM = nothing, middlewareN = nothing, middlewareO = nothing, 228 | middlewareP = nothing, middlewareQ = nothing, middlewareR = nothing, middlewareS = nothing, 229 | middlewareT = nothing, middlewareU = nothing, middlewareV = nothing, middlewareW = nothing, 230 | middlewareX = nothing, middlewareY = nothing, middlewareZ = nothing]) { 231 | return new Route(['UPDATE'], path, pipe(middlewareA, middlewareB, 232 | middlewareC, middlewareD, middlewareE, middlewareF, middlewareG, middlewareH, 233 | middlewareI, middlewareJ, middlewareK, middlewareL, middlewareM, middlewareN, 234 | middlewareO, middlewareP, middlewareQ, middlewareR, middlewareS, middlewareT, 235 | middlewareU, middlewareV, middlewareW, middlewareX, middlewareY, middlewareZ)); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lib/src/util/concat.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | Map concatMaps/**/( 4 | Map a, 5 | Map b 6 | ) { 7 | return new Map.unmodifiable({}..addAll(a)..addAll(b)); 8 | } 9 | 10 | Iterable concatIterables/**/( 11 | Iterable a, 12 | Iterable b 13 | ) { 14 | return new _ConcatinatedIterable(a, b); 15 | } 16 | 17 | class _ConcatinatedIterable extends IterableBase implements Iterable { 18 | final Iterable a; 19 | final Iterable b; 20 | 21 | Iterator get iterator { 22 | return new _ConcatinatedIterator(a.iterator, b.iterator); 23 | } 24 | 25 | _ConcatinatedIterable(this.a, this.b); 26 | } 27 | 28 | class _ConcatinatedIterator implements Iterator { 29 | final Iterator a; 30 | final Iterator b; 31 | bool _aIsNotDone = true; 32 | 33 | _ConcatinatedIterator(this.a, this.b); 34 | 35 | E get current { 36 | if (_aIsNotDone) { 37 | return a.current; 38 | } else { 39 | return b.current; 40 | } 41 | } 42 | 43 | bool moveNext() { 44 | if (_aIsNotDone && a.moveNext()) { 45 | return true; 46 | } else { 47 | _aIsNotDone = false; 48 | } 49 | if (b.moveNext()) { 50 | return true; 51 | } 52 | return false; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/util/container_state.dart: -------------------------------------------------------------------------------- 1 | import '../../container.dart'; 2 | 3 | class ContainerState { 4 | IoCContainer _state; 5 | 6 | ContainerState(this._state); 7 | 8 | set state(IoCContainer state) { 9 | _state = _state.apply(state); 10 | } 11 | 12 | IoCContainer get state => _state; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/util/nothing.dart: -------------------------------------------------------------------------------- 1 | const nothing = const _Nothing(); 2 | 3 | class _Nothing { 4 | const _Nothing(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/util/process_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:stack_trace/stack_trace.dart'; 3 | import 'dart:io'; 4 | import 'trace_formatting.dart'; 5 | import 'terminal.dart'; 6 | 7 | class ProcessHandler { 8 | final Function init; 9 | final Function deinit; 10 | final Terminal terminal = new Terminal(); 11 | final Completer _performShutdown = new Completer(); 12 | final Completer _performTeardown = new Completer(); 13 | 14 | ProcessHandler({this.init, this.deinit}); 15 | 16 | var initResult; 17 | StreamSubscription _sigintSub; 18 | Timer _timer; 19 | 20 | Future run() async { 21 | runZoned(() { 22 | return Chain.capture(() async { 23 | initResult = await init(); 24 | 25 | var firstQuit = true; 26 | _sigintSub = ProcessSignal.SIGINT.watch().listen((_) { 27 | print(' --> Shutting down...'); 28 | if (firstQuit) { 29 | firstQuit = false; 30 | if (stdout.hasTerminal) 31 | _timer = new Timer(const Duration(milliseconds: 300), () { 32 | print( 33 | 'Exiting gently... Press ^C again to force exit'); 34 | }); 35 | _performTeardown.complete(null); 36 | return; 37 | } 38 | _performShutdown.complete(null); 39 | }); 40 | 41 | _performTeardown.future.then((_) async { 42 | await deinit(initResult); 43 | _performShutdown.complete(null); 44 | }); 45 | }, onError: TraceFormatter.print); 46 | }, zoneSpecification: new ZoneSpecification(print: terminal.print)); 47 | await _performShutdown.future; 48 | _sigintSub?.cancel(); 49 | _timer?.cancel(); 50 | } 51 | 52 | Future interrupt() async { 53 | if (_performTeardown.isCompleted) return; 54 | terminal.print(null, null, null, 'Reloading...'); 55 | _performTeardown.complete(null); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/util/stylizer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | const Map _colors = const { 4 | 'reset': const _StyleTag(0, 0), 5 | 'bold': const _StyleTag(1, 22), 6 | 'dim': const _StyleTag(2, 22), 7 | 'italic': const _StyleTag(3, 23), 8 | 'underline': const _StyleTag(4, 24), 9 | 'inverse': const _StyleTag(7, 27), 10 | 'hidden': const _StyleTag(8, 28), 11 | 'strikethrough': const _StyleTag(9, 29), 12 | 'black': const _StyleTag(30, 39), 13 | 'red': const _StyleTag(31, 39), 14 | 'green': const _StyleTag(32, 39), 15 | 'yellow': const _StyleTag(33, 39), 16 | 'blue': const _StyleTag(34, 39), 17 | 'magenta': const _StyleTag(35, 39), 18 | 'cyan': const _StyleTag(36, 39), 19 | 'white': const _StyleTag(37, 39), 20 | 'gray': const _StyleTag(90, 39), 21 | 'black-background': const _StyleTag(40, 49), 22 | 'red-background': const _StyleTag(41, 49), 23 | 'green-background': const _StyleTag(42, 49), 24 | 'yellow-background': const _StyleTag(43, 49), 25 | 'blue-background': const _StyleTag(44, 49), 26 | 'magenta-background': const _StyleTag(45, 49), 27 | 'cyan-background': const _StyleTag(46, 49), 28 | 'white-background': const _StyleTag(47, 49) 29 | }; 30 | 31 | class _StyleTag { 32 | final int _open; 33 | final int _close; 34 | 35 | const _StyleTag(this._open, this._close); 36 | 37 | String get open => stdout.hasTerminal ? '\u001b[${_open}m' : ''; 38 | String get close => stdout.hasTerminal ? '\u001b[${_close}m' : ''; 39 | } 40 | 41 | class Stylizer { 42 | String parse(String input) { 43 | for (final tagName in _colors.keys) { 44 | input = input.replaceAll('<$tagName>', _colors[tagName].open); 45 | input = input.replaceAll('', _colors[tagName].close); 46 | } 47 | return input; 48 | } 49 | 50 | String strip(String input) { 51 | for (final tagName in _colors.keys) { 52 | input = input.replaceAll('<$tagName>', ''); 53 | input = input.replaceAll('', ''); 54 | } 55 | return input; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/util/terminal.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'stylizer.dart'; 4 | 5 | class Terminal { 6 | final Stylizer stylizer = new Stylizer(); 7 | 8 | void print(Zone self, ZoneDelegate parent, Zone zone, String line) { 9 | stdout.writeln(stylizer.parse(line)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/util/trace_formatting.dart: -------------------------------------------------------------------------------- 1 | import 'package:stack_trace/stack_trace.dart'; 2 | import 'dart:math' show max; 3 | import 'dart:core' as core show print; 4 | import 'dart:core' hide print; 5 | 6 | traceIdentifier_PJ9ZCKjkkKPFYjgH3jkW(body()) { 7 | return body(); 8 | } 9 | 10 | class TraceFormatter { 11 | final Chain chain; 12 | 13 | TraceFormatter(this.chain); 14 | 15 | factory TraceFormatter.forTrace(StackTrace trace) => 16 | new TraceFormatter(new Chain.forTrace(trace)); 17 | 18 | Iterable get unfilteredFrames { 19 | final frames = chain.terse.traces 20 | .expand((t) => t.frames) 21 | .toList() 22 | .reversed; 23 | 24 | if (frames.any(_isTraceIdentifier)) { 25 | return frames.skipWhile((f) => !_isTraceIdentifier(f)).skip(1); 26 | } 27 | 28 | return frames; 29 | } 30 | 31 | List get frames { 32 | final frames = unfilteredFrames 33 | .where((f) => !f.isCore) 34 | .where((f) => !f.member.split('.').last.startsWith('_')) 35 | .where((f) => !f.member.startsWith('_')) 36 | .where((f) => !new RegExp('|').hasMatch(f.member)).toList(); 37 | 38 | if (frames.length == 0) { 39 | return frames; 40 | } 41 | 42 | if (frames.last != unfilteredFrames.last) 43 | frames.add(unfilteredFrames.last); 44 | 45 | return frames; 46 | } 47 | 48 | int get locationColumnWidth => frames.fold(0, (int previousMax, Frame frame) => 49 | max(previousMax, frame.location.length)) + 3; 50 | 51 | String _formatFrame(Frame frame, {bool error: false}) { 52 | final locationTag = error ? 'red' : 'blue'; 53 | final location = frame.location.padRight(locationColumnWidth).replaceFirstMapped(new RegExp(r'(\d+:\d+)(\s*)$'), (m) => '<$locationTag>${m[1]}${m[2]}'); 54 | final member = frame.member.replaceAll(new RegExp(r'(?:\.<(?:fn|async)>)+'), ' anonymous'); 55 | final memberTag = error ? 'red' : 'green'; 56 | return '$location<$memberTag>$member'; 57 | } 58 | 59 | String get formatted { 60 | if (frames.length == 0) return ''; 61 | return frames.take(frames.length - 1).map(_formatFrame).join('\n') 62 | + '\n${_formatFrame(frames.last, error: true)}'; 63 | } 64 | 65 | static void print(error, StackTrace trace) { 66 | final fmt = new TraceFormatter.forTrace(trace); 67 | core.print('\n${(' ' * (fmt.locationColumnWidth)) + 'init'}'); 68 | core.print(fmt.formatted); 69 | core.print('\n\n ${new DateTime.now()}\n${ 70 | error.toString().split('\n').map((s) => ' $s').join('\n') 71 | }\n'); 72 | } 73 | 74 | bool _isTraceIdentifier(Frame frame) { 75 | return frame.member == 'traceIdentifier_PJ9ZCKjkkKPFYjgH3jkW'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: embla 2 | description: A friendly web server transport layer for server side Dart apps 3 | version: 0.2.2 4 | author: Emil Persson 5 | homepage: https://embla.io 6 | 7 | executables: 8 | embla: 9 | 10 | environment: 11 | sdk: ">=1.12.0 <2.0.0" 12 | 13 | dependencies: 14 | stack_trace: ^1.6.0 15 | shelf: ^0.6.4 16 | shelf_static: ^0.2.3 17 | watcher: ^0.9.7 18 | 19 | dev_dependencies: 20 | test: any 21 | quark: any 22 | -------------------------------------------------------------------------------- /resources/error-page/.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | index.css 3 | index.css.map 4 | -------------------------------------------------------------------------------- /resources/error-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 – HttpNotFoundException 7 | 8 | 9 | 12 | 13 | 14 | 15 |
16 |
17 | Error 404 18 |

HttpNotFoundException

19 |
20 |
21 | 68 |
69 |
import 'dart:async';
 70 | 
 71 | import '../middleware.dart';
 72 | 
 73 | class RemoveTrailingSlashMiddleware extends Middleware {
 74 |   @override
 75 |   Future handle(Request request) async {
 76 |     if (request.url.path.endsWith('/')) {
 77 |       final url = request.handlerPath + request.url.path;
 78 |       return redirectPermanently('/' + url.split('/').where((s) => s.isNotEmpty).join('/'));
 79 |     }
 80 |     return await super.handle(request);
 81 |   }
 82 | }
 83 | 
84 |
import 'dart:async';
 85 | 
 86 | import '../middleware.dart';
 87 | 
 88 | class RemoveTrailingSlashMiddleware extends Middleware {
 89 | @override
 90 | Future handle(Request request) async {
 91 | if (request.url.path.endsWith('/')) {
 92 |       final url = request.handlerPath + request.url.path;
 93 | return redirectPermanently('/' + url.split('/').where((s) => s.isNotEmpty).join('/'));
 94 | }
 95 | return await super.handle(request);
 96 | }
 97 | }
 98 | 
99 |
import 'dart:async';
100 | 
101 | import '../middleware.dart';
102 | 
103 | class RemoveTrailingSlashMiddleware extends Middleware {
104 | @override
105 | Future handle(Request request) async {
106 | if (request.url.path.endsWith('/')) {
107 |       final url = request.handlerPath + request.url.path;
108 | return redirectPermanently('/' + url.split('/').where((s) => s.isNotEmpty).join('/'));
109 | }
110 | return await super.handle(request);
111 | }
112 | }
113 | 
114 |
import 'dart:async';
115 | 
116 | import '../middleware.dart';
117 | 
118 | class RemoveTrailingSlashMiddleware extends Middleware {
119 | @override
120 | Future handle(Request request) async {
121 | if (request.url.path.endsWith('/')) {
122 |       final url = request.handlerPath + request.url.path;
123 | return redirectPermanently('/' + url.split('/').where((s) => s.isNotEmpty).join('/'));
124 | }
125 | return await super.handle(request);
126 | }
127 | }
128 | 
129 |
130 |
131 |
132 | 133 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /resources/error-page/index.scss: -------------------------------------------------------------------------------- 1 | $font-standard: 'Roboto Slab', sans-serif; 2 | $font-monospace: 'Cousine', monospace; 3 | 4 | $color-white: #fefefe; 5 | $color-light: #f8f8f8; 6 | $color-gray: #555; 7 | $color-black: #333; 8 | $color-blue: #3385ff; 9 | $color-red: rgb(222, 69, 48); 10 | 11 | body { 12 | background: $color-light; 13 | margin: 0; 14 | font-family: $font-standard; 15 | color: $color-black; 16 | } 17 | .container { 18 | background: $color-white; 19 | display: flex; 20 | flex-direction: column; 21 | max-width: 80em; 22 | margin: auto; 23 | height: 100vh; 24 | box-shadow: 0 -5em 6em -5em black; 25 | 26 | &__header { 27 | padding: 1rem; 28 | 29 | &__code { 30 | color: $color-gray; 31 | font-size: 0.9em; 32 | font-family: $font-monospace; 33 | } 34 | &__heading { 35 | margin: 0; 36 | } 37 | } 38 | 39 | &__content { 40 | flex: 1; 41 | display: flex; 42 | } 43 | } 44 | 45 | .source { 46 | flex: 2; 47 | display: flex; 48 | 49 | &__pre { 50 | overflow: auto; 51 | padding: 1em; 52 | border-top: 1px solid transparentize($color-black, 0.9); 53 | font-family: $font-monospace; 54 | line-height: 1.4; 55 | font-size: 1.1em; 56 | margin: 0; 57 | background: $color-light; 58 | flex: 1; 59 | 60 | &:not(.active) { 61 | display: none; 62 | } 63 | } 64 | 65 | &__highlighted-line { 66 | background: $color-red; 67 | display: inline-block; 68 | width: 100%; 69 | box-shadow: -1em 0 0 0.3em $color-red, 1em 0 0 0.3em $color-red; 70 | color: white; 71 | * { 72 | color: white !important; 73 | } 74 | } 75 | 76 | &__line-number .hljs-number { 77 | color: lighten($color-gray, 30%); 78 | } 79 | } 80 | 81 | .stack { 82 | flex: 1; 83 | overflow: auto; 84 | 85 | &__list { 86 | margin: 0; 87 | list-style: none; 88 | padding: 0; 89 | font-family: $font-monospace; 90 | cursor: pointer; 91 | } 92 | 93 | &__list__item { 94 | width: 100%; 95 | border: 0; 96 | background: $color-white; 97 | margin: 0; 98 | font: inherit; 99 | text-align: left; 100 | -webkit-appearance: none; 101 | -moz-appearance: none; 102 | -ms-appearance: none; 103 | -o-appearance: none; 104 | appearance: none; 105 | padding: 1em; 106 | cursor: pointer; 107 | border-top: 1px solid transparentize($color-black, 0.9); 108 | 109 | &__file { 110 | overflow: hidden; 111 | white-space: nowrap; 112 | text-overflow: ellipsis; 113 | position: relative; 114 | display: block; 115 | margin-bottom: 0.2em; 116 | } 117 | 118 | .active & { 119 | border: 1px solid transparentize($color-black, 0.9); 120 | background-color: $color-blue; 121 | color: $color-white; 122 | 123 | &:active { 124 | color: $color-white; 125 | } 126 | } 127 | 128 | .active + li & { 129 | border-top: 0; 130 | } 131 | 132 | &:active { 133 | color: $color-gray; 134 | } 135 | 136 | &__function { 137 | margin: 0; 138 | } 139 | } 140 | } 141 | 142 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 143 | 144 | /* Tomorrow Comment */ 145 | .hljs-comment, 146 | .hljs-quote { 147 | color: #8e908c; 148 | } 149 | 150 | /* Tomorrow Red */ 151 | .hljs-variable, 152 | .hljs-template-variable, 153 | .hljs-tag, 154 | .hljs-name, 155 | .hljs-selector-id, 156 | .hljs-selector-class, 157 | .hljs-regexp, 158 | .hljs-deletion { 159 | color: #c82829; 160 | } 161 | 162 | /* Tomorrow Orange */ 163 | .hljs-number, 164 | .hljs-built_in, 165 | .hljs-builtin-name, 166 | .hljs-literal, 167 | .hljs-type, 168 | .hljs-params, 169 | .hljs-meta, 170 | .hljs-link { 171 | color: #f5871f; 172 | } 173 | 174 | /* Tomorrow Yellow */ 175 | .hljs-attribute { 176 | color: #eab700; 177 | } 178 | 179 | /* Tomorrow Green */ 180 | .hljs-string, 181 | .hljs-symbol, 182 | .hljs-bullet, 183 | .hljs-addition { 184 | color: #718c00; 185 | } 186 | 187 | /* Tomorrow Blue */ 188 | .hljs-title, 189 | .hljs-section { 190 | color: #4271ae; 191 | } 192 | 193 | /* Tomorrow Purple */ 194 | .hljs-keyword, 195 | .hljs-selector-tag { 196 | color: #8959a8; 197 | } 198 | 199 | .hljs-emphasis { 200 | font-style: italic; 201 | } 202 | 203 | .hljs-strong { 204 | font-weight: bold; 205 | } 206 | -------------------------------------------------------------------------------- /test/integration/bootstrap_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | export 'package:quark/init.dart'; 3 | import 'package:embla/application.dart'; 4 | 5 | class BootstrapTest extends UnitTest { 6 | Application app; 7 | TestBootstrapper bootstrapper; 8 | 9 | @before 10 | setUp() async { 11 | app = await Application.boot([ 12 | new TestBootstrapper() 13 | ]); 14 | bootstrapper = app.bootstrappers[0]; 15 | } 16 | 17 | @after 18 | tearDown() async { 19 | await app.exit(); 20 | } 21 | 22 | @test 23 | itInstantiatesBootstrappers() { 24 | expect(bootstrapper, new isInstanceOf()); 25 | } 26 | 27 | @test 28 | itInitializesBootstrappers() { 29 | bootstrapper.verify(); 30 | } 31 | } 32 | 33 | class TestBootstrapper extends Bootstrapper { 34 | final List history = []; 35 | 36 | @Hook.init 37 | runInit() => history.add('init'); 38 | 39 | @Hook.init 40 | runInit2() => history.add('init2'); 41 | 42 | @Hook.afterInit 43 | runAfterInit() => history.add('afterInit'); 44 | 45 | @Hook.beforeBindings 46 | runBeforeBindings() => history.add('beforeBindings'); 47 | 48 | @Hook.bindings 49 | runBindings() => history.add('bindings'); 50 | 51 | @Hook.afterBindings 52 | runAfterBindings() => history.add('afterBindings'); 53 | 54 | @Hook.beforeInteraction 55 | runBeforeInteraction() => history.add('beforeInteraction'); 56 | 57 | @Hook.interaction 58 | runInteraction() => history.add('interaction'); 59 | 60 | @Hook.afterInteraction 61 | runAfterInteraction() => history.add('afterInteraction'); 62 | 63 | @Hook.beforeReaction 64 | runBeforeReaction() => history.add('beforeReaction'); 65 | 66 | @Hook.reaction 67 | runReaction() => history.add('reaction'); 68 | 69 | @Hook.afterReaction 70 | runAfterReaction() => history.add('afterReaction'); 71 | 72 | @Hook.beforeExit 73 | runBeforeTeardown() => history.add('beforeExit'); 74 | 75 | @Hook.exit 76 | runTeardown() => history.add('exit'); 77 | 78 | void verify() { 79 | expect(history, [ 80 | 'init', 81 | 'init2', 82 | 'afterInit', 83 | 'beforeBindings', 84 | 'bindings', 85 | 'afterBindings', 86 | 'beforeInteraction', 87 | 'interaction', 88 | 'afterInteraction', 89 | 'beforeReaction', 90 | 'reaction', 91 | 'afterReaction', 92 | ]); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/unit/bootstrappers/http_bootstrapper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:quark/test_double.dart'; 3 | import 'package:embla/http.dart'; 4 | import 'dart:io'; 5 | import 'dart:async'; 6 | import 'package:embla/container.dart'; 7 | export 'package:quark/init.dart'; 8 | 9 | class HttpBootstrapperTest extends UnitTest { 10 | HttpBootstrapper bootstrapper(HttpServerDouble server, PipelineFactory pipeline) { 11 | return new HttpBootstrapper.internal((host, int port) async => server, 'localhost', 1337, pipeline)..attach(); 12 | } 13 | 14 | dynamic silent(body()) { 15 | return runZoned(body, zoneSpecification: new ZoneSpecification(print: (a, b, c, d) => null)); 16 | } 17 | 18 | @test 19 | itWorks() async { 20 | final server = new HttpServerDouble(); 21 | 22 | final boot = bootstrapper(server, pipe()); 23 | 24 | await boot.bindings(); 25 | } 26 | 27 | @test 28 | itStartsAServer() async { 29 | final server = new HttpServerDouble(); 30 | 31 | final boot = bootstrapper(server, pipe()); 32 | 33 | when(server.port).thenReturn(1337); 34 | when(server.address).thenReturn(new InternetAddress('127.0.0.1')); 35 | 36 | await boot.bindings(); 37 | await silent(() async => await boot.start(server)); 38 | 39 | verify(server.listen(null)).wasCalled(); 40 | } 41 | 42 | @test 43 | itHandlesRequests() async { 44 | final server = new HttpServerDouble(); 45 | final PipelineFactory pipelineFactory = pipe( 46 | () => 'response' 47 | ); 48 | final boot = bootstrapper(server, pipelineFactory); 49 | final Pipeline pipeline = pipelineFactory(new IoCContainer()); 50 | 51 | final request = new Request('GET', new Uri.http('localhost', '/')); 52 | final response = await boot.handleRequest(request, pipeline); 53 | expect(await response.readAsString(), 'response'); 54 | } 55 | } 56 | 57 | class HttpServerDouble extends TestDouble implements HttpServer {} 58 | -------------------------------------------------------------------------------- /test/unit/container_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/container.dart'; 3 | export 'package:quark/init.dart'; 4 | 5 | class IoCContainerTest extends UnitTest { 6 | IoCContainer get c => new IoCContainer(); 7 | 8 | Matcher get throwsInjectionException => throwsA(new isInstanceOf()); 9 | Matcher get throwsBindingException => throwsA(new isInstanceOf()); 10 | 11 | @test 12 | itInstantiatesClasses() { 13 | expect(c.make(SimpleClass), new isInstanceOf()); 14 | } 15 | 16 | @test 17 | itResolvesFunctions() { 18 | expect(c.resolve((SimpleClass s) => s), new isInstanceOf()); 19 | } 20 | 21 | @test 22 | itCannotInstantiateATypedef() { 23 | expect(() => c.make(Typedef), throwsInjectionException); 24 | } 25 | 26 | @test 27 | itThrowsWhenAClassIsAbstract() { 28 | expect(() => c.make(String), throwsInjectionException); 29 | } 30 | 31 | @test 32 | itInstantiatesTheDependencies() { 33 | final ClassWithDependency i = c.make(ClassWithDependency); 34 | expect(i, new isInstanceOf()); 35 | expect(i.dep, new isInstanceOf()); 36 | } 37 | 38 | @test 39 | itCanBindATypeToAnother() { 40 | expect(c.bind(AbstractClass, to: ConcreteClass).make(AbstractClass), new isInstanceOf()); 41 | expect(c.bind(AbstractClass, to: new ConcreteClass()).make(AbstractClass), new isInstanceOf()); 42 | } 43 | 44 | @test 45 | itThrowsIfBindingIsIncompatible() { 46 | expect(() => c.bind(AbstractClass, to: SimpleClass), throwsBindingException); 47 | expect(() => c.bind(AbstractClass, to: ""), throwsBindingException); 48 | } 49 | 50 | @test 51 | bindingsPropagate() { 52 | expect(() => c.make(ClassWithNestedAbstractDependency), throwsInjectionException); 53 | expect( 54 | c.bind(AbstractClass, to: ConcreteClass).make(ClassWithNestedAbstractDependency), 55 | new isInstanceOf() 56 | ); 57 | } 58 | 59 | @test 60 | itTriesToResolveNamedArgumentsByDefault() { 61 | expect(c.resolve(({SimpleClass c, String s}) => '$c,$s'), "Instance of 'SimpleClass',null"); 62 | } 63 | 64 | @test 65 | itCanBindNamedArguments() { 66 | expect(c.bindName("s", to: "string").resolve(({s}) => '$s'), "string"); 67 | 68 | final boundC = c.bindName("x", to: 123) 69 | .bindName("x", to: "string") 70 | .bindName("x", to: ConcreteClass); 71 | 72 | boundC.resolve(({num x}) { 73 | expect(x, 123); 74 | }); 75 | boundC.resolve(({String x}) { 76 | expect(x, "string"); 77 | }); 78 | boundC.resolve(({SimpleClass x}) { 79 | expect(x, new isInstanceOf()); 80 | }); 81 | boundC.resolve(({SimpleClass x: const DefaultValueSimpleClass()}) { 82 | expect(x, new isInstanceOf()); 83 | }); 84 | boundC.resolve(({AbstractClass x}) { 85 | expect(x, new isInstanceOf()); 86 | }); 87 | boundC.resolve(({x}) { 88 | expect(x, 123); 89 | }); 90 | } 91 | 92 | @test 93 | itUsesDefaultValueIfNoBindingExists() { 94 | expect(c.resolve(({s: 'default'}) => s), 'default'); 95 | } 96 | 97 | @test 98 | itThrowsIfAMoreGeneralNamedBindingHasAlreadyBeenMade() { 99 | expect( 100 | () => c.bindName('x', to: ConcreteClass).bindName('x', to: ConcreteSubClass), 101 | throwsBindingException 102 | ); 103 | expect( 104 | () => c.bindName('x', to: dynamic).bindName('x', to: SimpleClass), 105 | throwsBindingException 106 | ); 107 | } 108 | 109 | @test 110 | itStillAppliesOrdinaryBindingRulesWhenInjectingTypeBoundByName() { 111 | expect( 112 | c.bindName('x', to: AbstractClass).bind(AbstractClass, to: ConcreteClass).resolve(({x}) => x), 113 | new isInstanceOf() 114 | ); 115 | expect( 116 | c.bind(AbstractClass, to: ConcreteClass).bindName('x', to: AbstractClass).resolve(({x}) => x), 117 | new isInstanceOf() 118 | ); 119 | } 120 | 121 | @test 122 | itCanCurryFunctions() { 123 | final curried = c.curry((SimpleClass s) => s); 124 | expect(curried(), new isInstanceOf()); 125 | } 126 | 127 | @test 128 | curriedFunctionsCanBeSuppliedArguments() { 129 | final curried = c.curry((SimpleClass c, String s) => '$c,$s'); 130 | expect(curried, throwsInjectionException); 131 | expect(curried("x"), "Instance of 'SimpleClass',x"); 132 | 133 | expect( 134 | c.curry((p, {SimpleClass x, y}) => '$p,$x,$y')(123, y: 456), 135 | "123,Instance of 'SimpleClass',456" 136 | ); 137 | } 138 | 139 | @test 140 | itCanRegisterDecorators() { 141 | final Cat cat = c 142 | .decorate(Cat, withDecorator: ScreamDecorator) 143 | .decorate(Cat, withDecorator: ExclamationDecorator) 144 | .decorate(Cat, withDecorator: ExclamationDecorator) 145 | .make(Cat); 146 | 147 | expect(cat.meow, 'MEOW!!'); 148 | } 149 | 150 | @test 151 | itThrowsWhenTryingToDecorateWithClassThatDoesntImplementDecoratee() { 152 | expect(() => c.decorate(Cat, withDecorator: String), throwsBindingException); 153 | } 154 | 155 | @test 156 | itThrowsWhenTryingToDecorateWithClassThatDoesntInjectDecoratee() { 157 | expect(() => c.decorate(Cat, withDecorator: InvalidDecorator), throwsBindingException); 158 | } 159 | 160 | @test 161 | itChecksForNotProvidedArguments() { 162 | c.bind(String, to: null); 163 | expect(() => c.bind(String), throwsArgumentError); 164 | expect(() => c.bind(null, to: null), throwsArgumentError); 165 | 166 | c.bindName('x', to: null); 167 | expect(() => c.bindName('x'), throwsArgumentError); 168 | expect(() => c.bindName(null, to: null), throwsArgumentError); 169 | 170 | expect(() => c.curry(null), throwsArgumentError); 171 | expect(() => c.resolve(null), throwsArgumentError); 172 | expect(() => c.make(null), throwsArgumentError); 173 | 174 | expect(() => c.decorate(null), throwsArgumentError); 175 | expect(() => c.decorate(null, withDecorator: null), throwsArgumentError); 176 | expect(() => c.decorate(String), throwsArgumentError); 177 | expect(() => c.decorate(String, withDecorator: null), throwsArgumentError); 178 | expect(() => c.decorate(null, withDecorator: String), throwsArgumentError); 179 | } 180 | 181 | @test 182 | itCanCombineItselfWithAnotherContainer() { 183 | final ca = c.bind(String, to: 'x').bind(int, to: 1); 184 | final cb = c.bind(String, to: 'y'); 185 | final cc = ca.apply(cb); 186 | expect(cc.make(String), 'y'); 187 | expect(cc.make(int), 1); 188 | } 189 | } 190 | 191 | typedef Typedef(); 192 | 193 | class SimpleClass { 194 | const SimpleClass(); 195 | } 196 | 197 | class DefaultValueSimpleClass implements SimpleClass { 198 | const DefaultValueSimpleClass() : super(); 199 | } 200 | 201 | abstract class AbstractClass {} 202 | 203 | class ClassWithDependency { 204 | final SimpleClass dep; 205 | ClassWithDependency(this.dep); 206 | } 207 | 208 | class ConcreteClass implements AbstractClass {} 209 | class ConcreteSubClass extends ConcreteClass {} 210 | 211 | class ClassWithAbstractDependency { 212 | final AbstractClass dep; 213 | ClassWithAbstractDependency(this.dep); 214 | } 215 | 216 | class ClassWithNestedAbstractDependency { 217 | final ClassWithAbstractDependency dep; 218 | ClassWithNestedAbstractDependency(this.dep); 219 | } 220 | 221 | class Cat { 222 | String get meow => 'meow'; 223 | } 224 | 225 | class ScreamDecorator implements Cat { 226 | final Cat cat; 227 | ScreamDecorator(this.cat); 228 | String get meow => cat.meow.toUpperCase(); 229 | } 230 | 231 | class ExclamationDecorator implements Cat { 232 | final Cat cat; 233 | ExclamationDecorator(this.cat); 234 | String get meow => cat.meow + '!'; 235 | } 236 | 237 | class InvalidDecorator implements Cat { 238 | String get meow => "doesn't decorate!"; 239 | } 240 | -------------------------------------------------------------------------------- /test/unit/http/middleware/conditional_middleware_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/http_basic_middleware.dart'; 3 | import 'package:embla/src/http/pipeline.dart'; 4 | import 'middleware_call.dart'; 5 | import 'package:embla/src/http/context.dart'; 6 | export 'package:quark/init.dart'; 7 | 8 | class ConditionalMiddlewareTest extends UnitTest { 9 | @before 10 | setUp() { 11 | setUpContextForTesting(); 12 | } 13 | 14 | @test 15 | itWorks() async { 16 | final middleware = new ConditionalMiddleware(() => true, pipe(() => 'x')); 17 | await expectMiddlewareResponseBody(middleware, 'x'); 18 | } 19 | 20 | @test 21 | itPassesOnIfConditionFails() async { 22 | final middleware = new ConditionalMiddleware(() => false, pipe(() => 'x')); 23 | expect(middlewareCall(middleware), throwsA(new isInstanceOf())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/unit/http/middleware/error_handler_middleware_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | export 'package:quark/init.dart'; 3 | import 'package:embla/src/http/middleware/error_handler_middleware.dart'; 4 | import 'middleware_call.dart'; 5 | import 'package:stack_trace/stack_trace.dart'; 6 | import 'package:embla/src/http/context.dart'; 7 | 8 | class ErrorHandlerMiddlewareTest extends UnitTest { 9 | @before 10 | setUp() { 11 | setUpContextForTesting(); 12 | } 13 | 14 | @test 15 | itDoesNothingWithoutRegisteringHandlers() async { 16 | final middleware = new ErrorHandlerMiddleware(); 17 | 18 | final response = await middlewareCall(middleware); 19 | 20 | expect(response.statusCode, 500); 21 | } 22 | 23 | @test 24 | itCanRegisterHandlersForTypes() async { 25 | final middleware = ErrorHandlerMiddleware 26 | .on(String, () => "response"); 27 | 28 | final response = await middlewareCall(middleware, null, (r) => throw ""); 29 | 30 | expect(await response.readAsString(), 'response'); 31 | expect(response.statusCode, 200); 32 | } 33 | 34 | @test 35 | itInjectsErrorStackAndChain() async { 36 | final middleware = ErrorHandlerMiddleware 37 | .on(String, (String s, StackTrace t, Chain c) => s); 38 | 39 | final response = await middlewareCall(middleware, null, (r) => throw "message"); 40 | 41 | expect(await response.readAsString(), 'message'); 42 | expect(response.statusCode, 200); 43 | } 44 | 45 | @test 46 | itWarnsWhenALessSpecificHandlerIsAddedLater() async { 47 | ErrorHandlerMiddleware 48 | .on(SuperClass, () => null) 49 | .on(SubClass, () => null); 50 | 51 | expect(() { 52 | ErrorHandlerMiddleware 53 | .on(SubClass, () => null) 54 | .on(SuperClass, () => null); 55 | }, throwsA(new isInstanceOf())); 56 | } 57 | 58 | @test 59 | itCanCatchAll() async { 60 | final middleware = ErrorHandlerMiddleware 61 | .catchAll((error) => '$error'); 62 | 63 | final response = await middlewareCall(middleware, null, (r) => throw "message"); 64 | 65 | expect(await response.readAsString(), 'message'); 66 | expect(response.statusCode, 200); 67 | } 68 | } 69 | 70 | abstract class SuperClass {} 71 | 72 | class SubClass extends SuperClass {} 73 | -------------------------------------------------------------------------------- /test/unit/http/middleware/handler_middleware_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | export 'package:quark/init.dart'; 3 | import 'package:embla/src/http/middleware/handler_middleware.dart'; 4 | import 'dart:async'; 5 | import 'middleware_call.dart'; 6 | import 'package:embla/src/http/context.dart'; 7 | 8 | class HandlerMiddlewareTest extends UnitTest { 9 | @before 10 | setUp() { 11 | setUpContextForTesting(); 12 | } 13 | 14 | Future expectResponse(Function handler, String expectedBody) async { 15 | final middleware = new HandlerMiddleware(handler); 16 | await expectMiddlewareResponseBody(middleware, expectedBody); 17 | } 18 | 19 | @test 20 | itWorks() async { 21 | await expectResponse(() => 'x', 'x'); 22 | } 23 | 24 | @test 25 | itEncodesJson() async { 26 | await expectResponse(() => {'key': 'value'}, '{"key":"value"}'); 27 | } 28 | 29 | @test 30 | itEncodesClasses() async { 31 | await expectResponse(() => new ValueObject('x'), '{"property":"x"}'); 32 | } 33 | 34 | @test 35 | itSupportsInjection() async { 36 | final middleware = new HandlerMiddleware((ValueObject obj) => obj); 37 | middleware.context.container = middleware.context.container 38 | .bind(ValueObject, to: new ValueObject('y')); 39 | await expectMiddlewareResponseBody(middleware, '{"property":"y"}'); 40 | } 41 | } 42 | 43 | class ValueObject { 44 | final String property; 45 | 46 | ValueObject([this.property]); 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/http/middleware/input_parser_middleware_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/http_basic_middleware.dart'; 3 | import 'package:embla/src/http/request_response.dart'; 4 | import 'middleware_call.dart'; 5 | import 'dart:convert'; 6 | import 'package:embla/src/http/context.dart'; 7 | export 'package:quark/init.dart'; 8 | 9 | class InputParserMiddlewareTest extends UnitTest { 10 | @before 11 | setUp() { 12 | setUpContextForTesting(); 13 | } 14 | 15 | Response printJsonHandler(Request request) { 16 | return new Response.ok(JSON.encode(context.container.make(Input).toJson())); 17 | } 18 | 19 | @test 20 | itWorksForGetRequests() async { 21 | final middleware = new InputParserMiddleware(); 22 | final request = new Request('GET', new Uri.http('localhost', '/', {'k': 'v'})); 23 | await expectMiddlewareResponseBody( 24 | middleware, 25 | '{"k":"v"}', 26 | request, 27 | printJsonHandler 28 | ); 29 | } 30 | 31 | @test 32 | itWorksForPostRequests() async { 33 | final middleware = new InputParserMiddleware(); 34 | final request = new Request( 35 | 'POST', 36 | new Uri.http('localhost', '/'), 37 | body: JSON.encode({'k': 'v'}), 38 | headers: {'Content-Type': 'application/json'} 39 | ); 40 | await expectMiddlewareResponseBody( 41 | middleware, 42 | '{"k":"v"}', 43 | request, 44 | printJsonHandler 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/http/middleware/input_parsers/input_parser_expectation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:embla/src/http/middleware/input_parser_middleware.dart'; 3 | import 'package:test/test.dart'; 4 | import 'dart:convert'; 5 | 6 | Future expectParserOutput(InputParser parser, String input, dynamic expectedOutput) async { 7 | expect(await parser.parse(_stringToCharStream(input), UTF8), expectedOutput); 8 | } 9 | 10 | Stream> _stringToCharStream(String input) { 11 | final Stream lines = new Stream.fromIterable(input.split('\n')); 12 | return lines.map/*>*/(UTF8.encode); 13 | } 14 | -------------------------------------------------------------------------------- /test/unit/http/middleware/input_parsers/json_input_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/src/http/middleware/input_parser_middleware.dart'; 3 | export 'package:quark/init.dart'; 4 | import 'input_parser_expectation.dart'; 5 | 6 | class JsonInputParserTest extends UnitTest { 7 | @test 8 | itWorks() async { 9 | final parser = new JsonInputParser(); 10 | await expectParserOutput(parser, '"x"', 'x'); 11 | await expectParserOutput(parser, '3', 3); 12 | await expectParserOutput(parser, '3.2', 3.2); 13 | await expectParserOutput(parser, '0.2', 0.2); 14 | await expectParserOutput(parser, 'true', true); 15 | await expectParserOutput(parser, '{"x":"y"}', {'x': 'y'}); 16 | await expectParserOutput(parser, '["x","y"]', ['x', 'y']); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/http/middleware/input_parsers/raw_input_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/src/http/middleware/input_parser_middleware.dart'; 3 | export 'package:quark/init.dart'; 4 | import 'input_parser_expectation.dart'; 5 | 6 | class RawInputParserTest extends UnitTest { 7 | @test 8 | itWorks() async { 9 | final parser = new RawInputParser(); 10 | await expectParserOutput(parser, 'x', 'x'); 11 | await expectParserOutput(parser, '3', 3); 12 | await expectParserOutput(parser, '3.2', 3.2); 13 | await expectParserOutput(parser, '.2', 0.2); 14 | await expectParserOutput(parser, 'true', true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/http/middleware/input_parsers/url_encoded_input_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/src/http/middleware/input_parser_middleware.dart'; 3 | export 'package:quark/init.dart'; 4 | import 'input_parser_expectation.dart'; 5 | 6 | class UrlEncodedInputParserTest extends UnitTest { 7 | @test 8 | itWorks() async { 9 | final parser = new UrlEncodedInputParser(); 10 | await expectParserOutput(parser, 'y=x', {'y': 'x'}); 11 | await expectParserOutput(parser, 'y=x&a=b', {'y': 'x', 'a': 'b'}); 12 | await expectParserOutput(parser, 'y[]=1&y[]=2', {'y': [1, 2]}); 13 | await expectParserOutput(parser, 'y[0][key]=1&y[0][key2]=2', {'y': [{'key': 1, 'key2': 2}]}); 14 | await expectParserOutput(parser, 'y[0][key][]=1&y[0][key][]=2', {'y': [{'key': [1, 2]}]}); 15 | await expectParserOutput(parser, 'urlencoded%20key=urlencoded%20value', {'urlencoded key': 'urlencoded value'}); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/http/middleware/middleware_call.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:embla/http.dart'; 3 | import 'package:test/test.dart'; 4 | import 'package:shelf/shelf.dart' as shelf; 5 | 6 | Future middlewareCall(Middleware middleware, [Request request, shelf.Handler handler]) async { 7 | return await middleware 8 | .call(handler ?? (_) => throw new NoResponseFromPipelineException()) 9 | (request ?? new Request('GET', new Uri.http('localhost', '/'))); 10 | } 11 | 12 | Future expectMiddlewareResponseBody(Middleware middleware, String expectedBody, [Request request, shelf.Handler handler]) async { 13 | final response = await middlewareCall(middleware, request, handler); 14 | expect(await response.readAsString(), expectedBody); 15 | } 16 | -------------------------------------------------------------------------------- /test/unit/http/pipeline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | export 'package:quark/init.dart'; 3 | import 'package:embla/http.dart'; 4 | import 'package:embla/http_annotations.dart'; 5 | import 'dart:async'; 6 | import 'package:embla/src/http/context.dart'; 7 | import 'package:shelf/shelf.dart' as shelf; 8 | 9 | class PipelineTest extends UnitTest { 10 | @before 11 | setUp() { 12 | setUpContextForTesting(); 13 | } 14 | 15 | Request request(String path, String method) { 16 | final request = new Request(method, new Uri.http('localhost', path)); 17 | context.container = context.container.bind(Request, to: request); 18 | return request; 19 | } 20 | 21 | Future expectResponse(String method, String path, PipelineFactory pipeline, String body) async { 22 | expect( 23 | await (await pipeline()(request(path, method))).readAsString(), 24 | await new Response.ok(body).readAsString() 25 | ); 26 | } 27 | 28 | Future expectThrows(String method, String path, PipelineFactory pipeline, Matcher matcher) async { 29 | expect(pipeline()(request(path, method)), throwsA(matcher)); 30 | } 31 | 32 | @test 33 | itCreatesAnHttpPipeline() async { 34 | await expectResponse('GET', '/', pipe(MyMiddleware), 'response'); 35 | } 36 | 37 | @test 38 | itThrowsA404WithoutMiddleware() async { 39 | await expectThrows('GET', '/', pipe(), new isInstanceOf()); 40 | } 41 | 42 | @test 43 | itPipesThroughMultipleMiddleware() async { 44 | await expectResponse('GET', '/', pipe( 45 | MyPassMiddleware, 46 | MyPassMiddleware, 47 | MyPassMiddleware, 48 | MyMiddleware 49 | ), 'response'); 50 | } 51 | 52 | @test 53 | aRouteIsMiddleware() async { 54 | final pipeline = pipe( 55 | Route.get('/', MyMiddleware) 56 | ); 57 | 58 | await expectResponse('GET', '/', pipeline, 'response'); 59 | 60 | await expectThrows('GET', 'endpoint', pipeline, 61 | new isInstanceOf()); 62 | 63 | await expectThrows('POST', '/', pipeline, 64 | new isInstanceOf()); 65 | } 66 | 67 | @test 68 | aHandlerIsMiddleware() async { 69 | await expectResponse('GET', '/', 70 | pipe( 71 | handler(() => 'x') 72 | ), 73 | 'x' 74 | ); 75 | } 76 | 77 | @test 78 | itHandlesWildcards() async { 79 | await expectResponse('GET', '/foo/a/b', 80 | pipe( 81 | Route.get('foo/:x/:y', 82 | ({String x, String y}) => x + y 83 | ) 84 | ), 85 | 'ab' 86 | ); 87 | } 88 | 89 | @test 90 | aControllerIsMiddleware() async { 91 | await expectResponse('GET', '/', 92 | pipe(MyController), 93 | 'index' 94 | ); 95 | 96 | await expectResponse('GET', 'x', 97 | pipe(MyController), 98 | '{"property":"x"}' 99 | ); 100 | } 101 | 102 | @test 103 | aShelfMiddlewareCanBeUsedDirectly() async { 104 | bool wasCalled = false; 105 | 106 | await expectResponse('GET', '/', 107 | pipe( 108 | (shelf.Handler innerHandler) { 109 | return (Request request) { 110 | wasCalled = true; 111 | return innerHandler(request); 112 | }; 113 | }, 114 | (Request request) => '$wasCalled,${request.url}' 115 | ), 116 | 'true,' 117 | ); 118 | } 119 | } 120 | 121 | class MyMiddleware extends Middleware { 122 | @override Future handle(Request request) async { 123 | return ok('response'); 124 | } 125 | } 126 | 127 | class MyPassMiddleware extends Middleware { 128 | } 129 | 130 | class MyController extends Controller { 131 | @Get('/') 132 | index() { 133 | return 'index'; 134 | } 135 | 136 | @Get(null) 137 | x() { 138 | return new MyDataClass('x'); 139 | } 140 | } 141 | 142 | class MyDataClass { 143 | final String property; 144 | 145 | MyDataClass(this.property); 146 | } 147 | -------------------------------------------------------------------------------- /test/unit/http/response_maker_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/src/http/response_maker.dart'; 3 | import 'dart:io'; 4 | import 'dart:async'; 5 | export 'package:quark/init.dart'; 6 | 7 | class ResponseMakerTest extends UnitTest { 8 | final ResponseMaker responseMaker = new ResponseMaker(); 9 | 10 | void parses(input, body, ContentType contentType) { 11 | final r = responseMaker.parse(input); 12 | 13 | expect(r.body, body); 14 | expect(r.contentType, contentType); 15 | expect(r, new isInstanceOf()); 16 | } 17 | 18 | @test 19 | itMakesDataResponseObjects() { 20 | expect(responseMaker.parse(null), new isInstanceOf()); 21 | } 22 | 23 | @test 24 | itTurnsSimpleDataIntoAnHtmlResponse() { 25 | parses(null, '', ContentType.HTML); 26 | parses('', '', ContentType.HTML); 27 | parses('x', 'x', ContentType.HTML); 28 | parses(1, '1', ContentType.HTML); 29 | parses(1.2, '1.2', ContentType.HTML); 30 | parses(true, 'true', ContentType.HTML); 31 | } 32 | 33 | @test 34 | itTurnsAListIntoJson() { 35 | parses([], [], ContentType.JSON); 36 | parses([null], [null], ContentType.JSON); 37 | parses([null, 'x'], [null, 'x'], ContentType.JSON); 38 | } 39 | 40 | @test 41 | itTurnsAMapIntoJson() { 42 | parses({}, {}, ContentType.JSON); 43 | parses({'k': 'v'}, {'k': 'v'}, ContentType.JSON); 44 | } 45 | 46 | @test 47 | itTurnsAClassIntoJson() { 48 | parses(new MyClass('x'), {'property': 'x'}, ContentType.JSON); 49 | parses( 50 | new MyNestingClass(new MyClass('x')), 51 | {'nested': {'property': 'x'}}, 52 | ContentType.JSON 53 | ); 54 | } 55 | 56 | @test 57 | itTurnsAStreamOfObjectsIntoJsonStream() async { 58 | final r = responseMaker.parse(new Stream.fromIterable([new MyClass('x'), 2, 3])); 59 | 60 | expect(r, new isInstanceOf()); 61 | expect(r.contentType, ContentType.JSON); 62 | expect(await r.jsonStream(r.body).toList(), ['[', '{"property":"x"}', ',', '2', ',', '3', ']']); 63 | } 64 | 65 | @test 66 | itTurnsAStreamOfStringsIntoHtmlOutputStream() async { 67 | final r = responseMaker.parse(new Stream.fromIterable(['a', 'b', 'c'])); 68 | 69 | expect(r, new isInstanceOf()); 70 | expect(r.contentType, ContentType.HTML); 71 | expect(await r.body.toList(), ['a', 'b', 'c']); 72 | } 73 | 74 | @test 75 | itConvertsTheCaseInObjectJsonOutput() async { 76 | parses( 77 | new WeirdCase(), 78 | { 79 | 'camel_case': null, 80 | 'snake_case': null, 81 | 'upper_camel_case': null, 82 | 'weird_one1': null, 83 | 'super_weird_number_2': null, 84 | }, 85 | ContentType.JSON 86 | ); 87 | } 88 | } 89 | 90 | class MyClass { 91 | final String property; 92 | 93 | MyClass(this.property); 94 | } 95 | 96 | class MyNestingClass { 97 | final MyClass nested; 98 | 99 | MyNestingClass(this.nested); 100 | } 101 | 102 | class WeirdCase { 103 | final camelCase = null; 104 | final snake_case = null; 105 | final UpperCamelCase = null; 106 | final Weird_One1 = null; 107 | final superWeird_Number_2 = null; 108 | } 109 | -------------------------------------------------------------------------------- /test/unit/http/route_expander_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:quark/unit.dart'; 2 | import 'package:embla/src/http/route_expander.dart'; 3 | export 'package:quark/init.dart'; 4 | 5 | class RouteExpanderTest extends UnitTest { 6 | final RouteExpander expander = new RouteExpander(); 7 | 8 | void expands(String input, String output) { 9 | expect(expander.expand(input), output); 10 | } 11 | 12 | void prefixes(String pattern, String input, String output) { 13 | expect(expander.prefix(pattern, input), output); 14 | } 15 | 16 | @test 17 | itTurnsAStringIntoARegex() { 18 | expands('foo', r'^foo$'); 19 | } 20 | 21 | @test 22 | itNormalizesSlashes() { 23 | expands('/', r'^$'); 24 | expands('/foo', r'^foo$'); 25 | expands('/foo/', r'^foo$'); 26 | expands('/foo//', r'^foo$'); 27 | expands('/foo//bar', r'^foo\/bar$'); 28 | expands('foo/bar//', r'^foo\/bar$'); 29 | } 30 | 31 | @test 32 | itExpandsWildcards() { 33 | expands(':foo', r'^([^/]+)$'); 34 | expands('foo/:bar', r'^foo\/([^/]+)$'); 35 | } 36 | 37 | @test 38 | itExpandsAStar() { 39 | expands('foo/*', r'^foo(\/.*)?$'); 40 | expands('/*', r'^(\/.*)?$'); 41 | expands('*', r'^(\/.*)?$'); 42 | } 43 | 44 | @test 45 | itDeterminesThePrefixOfAPathExpression() { 46 | prefixes('/', '', ''); 47 | prefixes('/foo', 'foo', 'foo'); 48 | prefixes('/foo/:wildcard', 'foo/bar', 'foo/bar'); 49 | prefixes('/foo/:wildcard/*', 'foo/bar/more/things', 'foo/bar'); 50 | } 51 | 52 | @test 53 | itThrowsWhenThePathDoesntMatch() { 54 | expect(() => expander.prefix('/', 'foo'), throws); 55 | } 56 | 57 | @test 58 | itParsesWildcards() { 59 | expect(expander.parseWildcards('foo/:bar', 'foo/x'), {'bar': 'x'}); 60 | } 61 | } 62 | --------------------------------------------------------------------------------