├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── api_routes.dart ├── dart_express_example.dart ├── view_routes.dart └── views │ ├── about.mustache │ ├── example.md │ └── index.mustache ├── lib ├── dart_express.dart └── src │ ├── app.dart │ ├── dart_express_base.dart │ ├── engines │ ├── engine.dart │ ├── html.dart │ ├── markdown.dart │ └── mustache.dart │ ├── exceptions │ └── view_exception.dart │ ├── http_methods.dart │ ├── layer.dart │ ├── middleware │ ├── body_parser.dart │ ├── cors.dart │ ├── init.dart │ └── logger.dart │ ├── repositories │ └── file_repository.dart │ ├── request.dart │ ├── response.dart │ ├── route.dart │ ├── router.dart │ └── view.dart ├── pubspec.yaml ├── renovate.json └── test ├── dart_express_test.dart └── engines ├── html_test.dart ├── markdown_test.dart └── mustache_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.buymeacoffee.com/deriegle 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | container: 9 | image: google/dart:latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: pub get 13 | - run: dart test 14 | - run: dart analyze 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | - Initial version of Dart express with basic functionality for routing and middleware 4 | 5 | ## 0.1.1 6 | 7 | - Add dependencies for Jael (will be breaking this out to a separate package later) 8 | 9 | ## 0.2.0 10 | 11 | - Add `path_to_regexp` package to improve route matching. 12 | - Add support for route parameters using `path_to_regexp` package. 13 | - Improve README to include additional documentation 14 | - Add `res.end()` and `res.location()` methods to Response class. 15 | 16 | ## 0.2.1 17 | 18 | - Clean up Request & Response classes 19 | - Export HTTPStatus class from dart-express directly 20 | 21 | ## 0.2.2 22 | 23 | - Fix issue with relative paths for views 24 | - Add app.set('views', './path'); 25 | - Add app.set('view engine', './path') 26 | 27 | ## 0.2.3 28 | 29 | - Throw error when providing an invalid setting key 30 | 31 | ## 0.2.4 32 | 33 | - Add View Engine tests to ensure view rendering is working as expected 34 | - Add Markdown View Engine to parse and display Markdown files 35 | - Add documentation for public API and clean up interfaces 36 | 37 | ## 0.3.0 38 | 39 | - Add ability to listen on address other than loopback address (Thanks @jeffmikels) 40 | 41 | ## 0.4.0 42 | 43 | - Add all files as part of `dart_express` library 44 | - Change visibility of certain classes 45 | - Defer loading of View template libraries 46 | 47 | ## 0.5.0 48 | 49 | - Add CORS middleware 50 | 51 | ## 0.5.1 52 | 53 | - Add basic Logger middleware 54 | - Fix middleware not loading issue 55 | 56 | ## 0.5.2 57 | 58 | - Fix Body parser middleware 59 | 60 | ## 0.5.3 61 | 62 | - Allow separate routers and view routes option 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Devin Riegle 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dart Express ![Dart CI](https://github.com/deriegle/dart-express/workflows/Dart%20CI/badge.svg?branch=master) 2 | 3 | An express-like web server framework for Dart developers. 4 | 5 | ## Usage 6 | 7 | A simple usage example: 8 | 9 | ```dart 10 | import 'package:dart_express/dart_express.dart'; 11 | 12 | main() { 13 | final app = express(); 14 | 15 | app.get('/', (req, res) { 16 | res.json({ 17 | 'hello': 'world', 18 | 'test': true, 19 | }); 20 | }); 21 | 22 | app.listen(3000, (port) => print('Listening on port $port'); 23 | } 24 | ``` 25 | 26 | Example with route parameters 27 | 28 | ```dart 29 | import 'package:dart_express/dart_express.dart'; 30 | 31 | main() { 32 | final app = express(); 33 | 34 | app.get('/users/:userId/posts/:postId', (req, res) { 35 | res.json({ 36 | 'userId': req.params['userId'], 37 | 'postId': req.params['postId'], 38 | }); 39 | }); 40 | 41 | app.listen(3000, (port) => print('Listening on port $port'); 42 | } 43 | ``` 44 | 45 | With Body parsing Middleware: 46 | 47 | ```dart 48 | import 'package:dart_express/dart_express.dart'; 49 | 50 | main() { 51 | final app = express(); 52 | 53 | app.use(BodyParser.json()); 54 | 55 | app.post('/post', (req, res) { 56 | print(req.body); 57 | 58 | res.send({ 59 | 'request_body': req.body, 60 | }); 61 | }); 62 | 63 | app.listen(3000, (port) => print('Listening on port $port'); 64 | } 65 | ``` 66 | 67 | Using the mustache templating engine 68 | 69 | ```dart 70 | import 'package:dart_express/dart_express.dart'; 71 | 72 | main() { 73 | final app = express(); 74 | 75 | app.use(BodyParser.json()); 76 | app.engine(MustacheEngine.use()); 77 | 78 | app.settings 79 | ..viewsPath = 'custom_views_path' 80 | ..viewEngine = 'mustache'; 81 | 82 | app.get('/', (req, res) { 83 | res.render('index', { 84 | 'app_name': 'My Test App', 85 | }); 86 | }); 87 | 88 | app.listen(3000, (port) => print('Listening on port $port'); 89 | } 90 | ``` 91 | 92 | 93 | 94 | Listening to Https requests 95 | 96 | ```dart 97 | //listen for http requests 98 | app.listen(port: 80, cb: (port) => print('listening for http on port $port')); 99 | 100 | //assign certificate 101 | var context = SecurityContext(); 102 | final chain = Platform.script.resolve('certificates/chain.pem').toFilePath(); 103 | final key = Platform.script.resolve('certificates/key.pem').toFilePath(); 104 | 105 | context.useCertificateChain(chain); 106 | context.usePrivateKey(key); 107 | 108 | //listen for https requests 109 | app.listenHttps( 110 | context, 111 | port: 443, 112 | cb: (port) => print('Listening for https on port $port'), 113 | ); 114 | ``` 115 | 116 | 117 | 118 | ### Currently supported View Engines 119 | 120 | - Basic HTML 121 | - Mustache 122 | - Markdown 123 | - Jael 124 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml -------------------------------------------------------------------------------- /example/api_routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_express/dart_express.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class User { 5 | final int id; 6 | final String email; 7 | 8 | User({@required this.id, @required this.email}); 9 | 10 | Map toJson() { 11 | return { 12 | 'id': id, 13 | 'email': email, 14 | }; 15 | } 16 | } 17 | 18 | Router apiRouter() { 19 | final router = Router(); 20 | final users = [ 21 | User(id: 1, email: 'test@example.com'), 22 | User(id: 2, email: 'test2@example.com'), 23 | ]; 24 | 25 | router.get('/', (req, res) { 26 | res.status(200).json({ 27 | 'hello': 'world', 28 | 'age': 25, 29 | }); 30 | }); 31 | 32 | router.get('/users', (req, res) { 33 | res.status(200).json({ 34 | 'users': users.map((u) => u.toJson()).toList(), 35 | }); 36 | }); 37 | 38 | router.post('/users', (req, res) { 39 | final int id = req.body['id']; 40 | final String email = req.body['email']?.trim(); 41 | 42 | if (id == null) { 43 | res.status(400).json({ 44 | 'errors': [ 45 | {'key': 'id', 'message': 'ID is required'} 46 | ] 47 | }); 48 | return; 49 | } 50 | 51 | if (users.firstWhere((u) => u.id == id, orElse: () => null) != null) { 52 | res.status(400).json({ 53 | 'errors': [ 54 | {'key': 'id', 'message': 'ID must be unique'} 55 | ] 56 | }); 57 | return; 58 | } 59 | 60 | if (email == null || email.isEmpty) { 61 | res.status(400).json({ 62 | 'errors': [ 63 | {'key': 'email', 'message': 'Email is required'} 64 | ] 65 | }); 66 | return; 67 | } 68 | 69 | final user = User( 70 | id: req.body['id'], 71 | email: req.body['email'], 72 | ); 73 | 74 | users.add(user); 75 | 76 | res.status(201).json({ 77 | 'user': user.toJson(), 78 | }); 79 | }); 80 | 81 | return router; 82 | } 83 | -------------------------------------------------------------------------------- /example/dart_express_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_express/dart_express.dart'; 2 | import 'package:path/path.dart' as path; 3 | import './api_routes.dart'; 4 | import './view_routes.dart'; 5 | 6 | const int port = 5000; 7 | 8 | void main() { 9 | final app = express(); 10 | 11 | app.use(BodyParser.json()); 12 | app.use(CorsMiddleware.use()); 13 | app.use(LoggerMiddleware.use(includeImmediate: true)); 14 | 15 | app.engine(MarkdownEngine.use()); 16 | app.engine(MustacheEngine.use()); 17 | 18 | app.set('print routes', true); 19 | app.set('views', path.join(path.current, 'example/views')); 20 | app.set('view engine', 'mustache'); 21 | 22 | app.useRouter('/api/', apiRouter()); 23 | app.useRouter('/', viewRouter()); 24 | 25 | app.listen(port: port, cb: (int port) => print('Listening on port $port')); 26 | } 27 | -------------------------------------------------------------------------------- /example/view_routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_express/dart_express.dart'; 2 | 3 | Router viewRouter() { 4 | final router = Router(); 5 | 6 | router.get('/example', (req, res) { 7 | res.render('example.md'); 8 | }); 9 | 10 | router.all('/secret', (req, res) { 11 | print('Accessing the secret section'); 12 | req.next(); 13 | }); 14 | 15 | router.get('/secret', (req, res) { 16 | res.send('Secret Home Page'); 17 | }); 18 | 19 | router.get('/secret/2', (req, res) { 20 | res.send('Secret Home Page'); 21 | }); 22 | 23 | return router; 24 | } 25 | -------------------------------------------------------------------------------- /example/views/about.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | About 8 | 9 | 10 |

About Page

11 | 12 | {{#person}} 13 |

{{ first_name }}

14 | {{/person}} 15 | 16 | -------------------------------------------------------------------------------- /example/views/example.md: -------------------------------------------------------------------------------- 1 | # About Dart Express 2 | Dart Express is a library built in the dart programming language for building quick and easy web servers. 3 | 4 | ## TO DO List 5 | 6 | - Check it out 7 | - On Github 8 | - Or pub.dev 9 | - to get started 10 | -------------------------------------------------------------------------------- /example/views/index.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My Index Page 8 | 9 | 10 |

Index Page

11 |

Does this work?

12 | 13 | -------------------------------------------------------------------------------- /lib/dart_express.dart: -------------------------------------------------------------------------------- 1 | /// Support for doing something awesome. 2 | /// 3 | /// More dartdocs go here. 4 | library dart_express; 5 | 6 | import 'package:path/path.dart' as path 7 | show absolute, extension, join, isAbsolute; 8 | import 'dart:convert' as convert; 9 | import 'dart:async'; 10 | import 'dart:io'; 11 | import 'package:markdown/markdown.dart' deferred as markdown; 12 | import 'package:mustache4dart/mustache4dart.dart' deferred as mustache; 13 | import 'package:path_to_regexp/path_to_regexp.dart'; 14 | 15 | export 'dart:io' show HttpStatus; 16 | 17 | /// Top level classes 18 | part 'src/dart_express_base.dart'; 19 | part 'src/route.dart'; 20 | part 'src/app.dart'; 21 | part 'src/view.dart'; 22 | part 'src/layer.dart'; 23 | part 'src/router.dart'; 24 | part 'src/request.dart'; 25 | part 'src/response.dart'; 26 | part 'src/http_methods.dart'; 27 | 28 | /// Repositories 29 | part 'src/repositories/file_repository.dart'; 30 | 31 | /// Middleware 32 | part 'src/middleware/init.dart'; 33 | part 'src/middleware/body_parser.dart'; 34 | part 'src/middleware/cors.dart'; 35 | part 'src/middleware/logger.dart'; 36 | 37 | /// Exceptions 38 | part 'src/exceptions/view_exception.dart'; 39 | 40 | /// View Engines 41 | part 'src/engines/engine.dart'; 42 | part 'src/engines/mustache.dart'; 43 | part 'src/engines/html.dart'; 44 | part 'src/engines/markdown.dart'; 45 | -------------------------------------------------------------------------------- /lib/src/app.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class _AppSettings { 4 | bool cache; 5 | bool printRoutes; 6 | String viewsPath; 7 | String viewEngine; 8 | 9 | _AppSettings({ 10 | this.cache = true, 11 | String viewsPath, 12 | this.printRoutes = false, 13 | this.viewEngine = 'html', 14 | }) : viewsPath = viewsPath ?? path.absolute('views'); 15 | } 16 | 17 | class App { 18 | _AppSettings _settings; 19 | Map cache; 20 | Map _engines; 21 | HttpServer _server; 22 | Router _router; 23 | 24 | App() { 25 | _settings = _AppSettings(); 26 | cache = {}; 27 | _engines = {'html': HtmlEngine.use()}; 28 | } 29 | 30 | /// Set App configuration values 31 | /// 32 | /// Acceptable keys: 'views engine', 'views', and 'cache' 33 | /// 34 | /// Cache [Default: true] 35 | /// 36 | /// This key takes a boolean value to determine whether we should cache pages or not 37 | /// 38 | /// View Engine [Default: html] 39 | /// 40 | /// This key takes a string value of the extension of the default view engine you'd like to use. 41 | /// For Example: Provide "jael" for Jael templates as your default 42 | /// 43 | /// Views [Default: ./views] 44 | /// 45 | /// This key takes a string value of the file path to the views folder 46 | void set(String key, dynamic value) { 47 | switch (key.toLowerCase()) { 48 | case 'views engine': 49 | case 'view engine': 50 | _settings.viewEngine = value; 51 | break; 52 | case 'views': 53 | _settings.viewsPath = value; 54 | break; 55 | case 'cache': 56 | _settings.cache = !!value; 57 | break; 58 | case 'print routes': 59 | case 'view routes': 60 | case 'show routes': 61 | _settings.printRoutes = value; 62 | break; 63 | default: 64 | throw ArgumentError('Invalid key "$key" for settings.'); 65 | } 66 | } 67 | 68 | /// Add a middleware callback for every request using this command 69 | /// 70 | /// Useful for parsing JSON, form data, logging requests, etc. 71 | App use(Function cb) { 72 | _lazyRouter(); 73 | 74 | _router.use(cb); 75 | 76 | return this; 77 | } 78 | 79 | App useRouter(String path, Router router) { 80 | final currentRouter = _lazyRouter(); 81 | 82 | currentRouter.stack.addAll( 83 | router.stack.map((layer) => layer.withPathPrefix(path)), 84 | ); 85 | 86 | return this; 87 | } 88 | 89 | /// Adds new View Engine to App 90 | /// 91 | /// Examples: 92 | /// 93 | /// app.engine(JaelEngine.use()); 94 | /// 95 | /// app.engine(MustacheEngine.use()); 96 | /// 97 | /// app.engine(MarkdownEngine.use()); 98 | App engine(Engine engine) { 99 | if (engine.ext == null) { 100 | throw Error.safeToString('Engine extension must be defined.'); 101 | } 102 | 103 | if (_engines[engine.ext] != null) { 104 | throw Error.safeToString( 105 | 'A View engine for the ${engine.ext} extension has already been defined.', 106 | ); 107 | } 108 | 109 | _engines[engine.ext] = engine; 110 | 111 | return this; 112 | } 113 | 114 | /// Handles DELETE requests to the specified path 115 | _Route delete(String path, Function cb) => 116 | _buildRoute(path, _HTTPMethods.delete, cb); 117 | 118 | /// Handles GET requests to the specified path 119 | _Route get(String path, RouteMethod cb) => 120 | _buildRoute(path, _HTTPMethods.get, cb); 121 | 122 | /// Handles HEAD requests to the specified path 123 | _Route head(String path, RouteMethod cb) => 124 | _buildRoute(path, _HTTPMethods.head, cb); 125 | 126 | /// Handles PATCH requests to the specified path 127 | _Route patch(String path, RouteMethod cb) => 128 | _buildRoute(path, _HTTPMethods.patch, cb); 129 | 130 | /// Handles POST requests to the specified path 131 | _Route post(String path, RouteMethod cb) => 132 | _buildRoute(path, _HTTPMethods.post, cb); 133 | 134 | /// Handles PUT requests to the specified path 135 | _Route put(String path, RouteMethod cb) => 136 | _buildRoute(path, _HTTPMethods.put, cb); 137 | 138 | /// Handles ALL requests to the specified path 139 | List<_Route> all(String path, RouteMethod cb) { 140 | final routes = <_Route>[]; 141 | 142 | for (final method in _HTTPMethods.all) { 143 | routes.add(_buildRoute(path, method, cb)); 144 | } 145 | 146 | return routes; 147 | } 148 | 149 | /// Starts the HTTP server listening on the specified port 150 | /// 151 | /// All Request and Response objects will be wrapped and handled by the Router 152 | Future listen( 153 | {InternetAddress address, int port, Function(int) cb}) async { 154 | _server = await HttpServer.bind( 155 | address ?? InternetAddress.loopbackIPv4, 156 | port, 157 | ); 158 | 159 | _mapToRoutes(cb); 160 | } 161 | 162 | /// Starts the HTTPS server listening on the specified port 163 | /// 164 | /// All Request and Response objects will be wrapped and handled by the Router 165 | /// 166 | /// You can add Certifications to the [SecurityContext] 167 | Future listenHttps( 168 | SecurityContext securityContext, { 169 | InternetAddress address, 170 | int port, 171 | Function(int) cb, 172 | }) async { 173 | _server = await HttpServer.bindSecure( 174 | address ?? InternetAddress.loopbackIPv4, port, securityContext); 175 | 176 | _mapToRoutes(cb); 177 | } 178 | 179 | void _mapToRoutes(Function(int) cb) { 180 | _server.listen((HttpRequest req) { 181 | final request = Request(req); 182 | final response = Response(req.response, this); 183 | 184 | _router.handle(request, response); 185 | }); 186 | 187 | if (_settings.printRoutes) { 188 | _printRoutes(); 189 | } 190 | 191 | if (cb != null) { 192 | cb(_server.port); 193 | } 194 | } 195 | 196 | /// Render a template using the given filename and options 197 | /// 198 | /// Uses the extension "html" or the "view engine" default when not given an extension 199 | /// You can override the default by providing an extension 200 | /// Provide a Map of local variables to the template 201 | void render( 202 | String fileName, 203 | Map locals, 204 | Function callback, 205 | ) { 206 | _settings.cache ??= true; 207 | 208 | final view = _getViewFromFileName(fileName); 209 | 210 | view.render(locals, callback); 211 | } 212 | 213 | void _printRoutes() { 214 | _router.stack.where((layer) => layer.route != null).forEach((layer) { 215 | print('[${layer.method}] ${layer.path}'); 216 | }); 217 | } 218 | 219 | _Route _buildRoute(String path, String method, RouteMethod cb) => 220 | _lazyRouter().route(path, method, cb); 221 | 222 | Router _lazyRouter() => _router ??= Router().use(_InitMiddleware.init); 223 | 224 | _View _getViewFromFileName(String fileName) { 225 | _View view; 226 | 227 | if (_settings.cache) { 228 | view = cache[fileName]; 229 | } 230 | 231 | if (view == null) { 232 | view = _View( 233 | fileName, 234 | defaultEngine: _settings.viewEngine, 235 | engines: _engines, 236 | rootPath: _settings.viewsPath, 237 | ); 238 | 239 | if (view.filePath == null) { 240 | String dirs; 241 | 242 | if (view.rootPath is List) { 243 | dirs = 244 | 'directories "${view.rootPath.join(', ')}" or "${view.rootPath[view.rootPath.length - 1]}"'; 245 | } else { 246 | dirs = 'directory "${view.rootPath}"'; 247 | } 248 | 249 | throw _ViewException(view, dirs); 250 | } 251 | 252 | if (_settings.cache) { 253 | cache[fileName] = view; 254 | } 255 | } 256 | 257 | return view; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /lib/src/dart_express_base.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | /// Creates a new instance of App 4 | /// 5 | /// This is equivalent to calling App() directly 6 | App express() => App(); 7 | -------------------------------------------------------------------------------- /lib/src/engines/engine.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | typedef HandlerCallback = Function(dynamic e, String rendered); 4 | typedef Handler = Function( 5 | String filePath, Map locals, HandlerCallback cb); 6 | 7 | class Engine { 8 | final String ext; 9 | final Handler handler; 10 | 11 | const Engine(this.ext, this.handler); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/engines/html.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class HtmlEngine { 4 | static String ext = '.html'; 5 | 6 | /// Called when rendering an HTML file in the Response 7 | /// 8 | /// [locals] is ignored for HTML files 9 | static Future handler( 10 | String filePath, 11 | Map locals, 12 | HandlerCallback callback, [ 13 | FileRepository fileRepository = const _RealFileRepository(), 14 | ]) async { 15 | try { 16 | var uri = Uri.file(filePath); 17 | final rendered = await fileRepository.readAsString(uri); 18 | callback(null, rendered); 19 | return rendered; 20 | } catch (e) { 21 | callback(e, null); 22 | return null; 23 | } 24 | } 25 | 26 | /// Call this method to add the HtmlEngine to your app (This is added by default) 27 | /// 28 | /// app.engine(HtmlEngine.use()); 29 | static Engine use() => Engine(HtmlEngine.ext, HtmlEngine.handler); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/engines/markdown.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class MarkdownEngine { 4 | static String ext = '.md'; 5 | 6 | /// Called when rendering Markdown files in a Response 7 | /// 8 | /// Renders Markdown file into HTML. Use [locals] to configure the rendered HTML. 9 | /// 10 | /// locals['title'] will set the title. 11 | /// 12 | /// locals['head'] will be rendered after the element 13 | /// 14 | /// locals['beforeMarkdown'] will be rendered at the top of the body before the markdown. 15 | /// 16 | /// locals['afterMarkdown'] will be rendered at the bottom of the body after the markdown 17 | static Future<String> handler( 18 | String filePath, 19 | Map<String, dynamic> locals, 20 | HandlerCallback callback, [ 21 | FileRepository fileRepository = const _RealFileRepository(), 22 | ]) async { 23 | await markdown.loadLibrary(); 24 | 25 | try { 26 | final fileContents = 27 | await fileRepository.readAsString(Uri.file(filePath)); 28 | final rendered = 29 | _wrapInHTMLTags(markdown.markdownToHtml(fileContents), locals); 30 | callback(null, rendered); 31 | return rendered; 32 | } catch (e) { 33 | callback(e, null); 34 | return null; 35 | } 36 | } 37 | 38 | static String _wrapInHTMLTags(String html, Map<String, dynamic> options) { 39 | return ''' 40 | <html> 41 | <head> 42 | <title>${options['title'] ?? ''} 43 | ${options['head']} 44 | 45 | 46 | ${options['beforeMarkdown']} 47 | $html 48 | ${options['afterMarkdown']} 49 | 50 | 51 | '''; 52 | } 53 | 54 | /// Call this method to add the MarkdownEngine to your app 55 | /// 56 | /// app.engine(MarkdownEngine.use()); 57 | /// 58 | /// If you're going to use markdown as your default, you can set your default view engine using app.set('views engine', 'md'). 59 | /// This will allow you to not use the ".md" extension when rendering your markdown files. 60 | static Engine use() => Engine(MarkdownEngine.ext, MarkdownEngine.handler); 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/engines/mustache.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class MustacheEngine { 4 | static String ext = '.mustache'; 5 | 6 | static Future handler( 7 | String filePath, 8 | Map options, 9 | HandlerCallback callback, [ 10 | FileRepository fileRepository = const _RealFileRepository(), 11 | ]) async { 12 | await mustache.loadLibrary(); 13 | 14 | try { 15 | final fileContents = 16 | await fileRepository.readAsString(Uri.file(filePath)); 17 | final rendered = mustache.render(fileContents, options ?? {}); 18 | 19 | return callback(null, rendered); 20 | } catch (e) { 21 | callback(e, null); 22 | return; 23 | } 24 | } 25 | 26 | static Engine use() => Engine(MustacheEngine.ext, MustacheEngine.handler); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/exceptions/view_exception.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class _ViewException implements Error { 4 | final _View view; 5 | final String directory; 6 | 7 | _ViewException(this.view, this.directory); 8 | 9 | String get message => 10 | 'ViewException(Failed to find ${view.name}${view.ext} in $directory)'; 11 | 12 | @override 13 | String toString() => message; 14 | 15 | @override 16 | StackTrace get stackTrace => StackTrace.fromString('ViewException'); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/http_methods.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class _HTTPMethods { 4 | static const get = 'GET'; 5 | static const post = 'POST'; 6 | static const delete = 'DELETE'; 7 | static const head = 'HEAD'; 8 | static const patch = 'PATCH'; 9 | static const put = 'PUT'; 10 | static const options = 'OPTIONS'; 11 | 12 | static const all = [ 13 | get, 14 | post, 15 | delete, 16 | head, 17 | patch, 18 | put, 19 | options, 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/layer.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class _Layer { 4 | final String _path; 5 | String method; 6 | RouteMethod handle; 7 | _Route route; 8 | String name; 9 | RegExp regExp; 10 | List parameters; 11 | Map routeParams; 12 | 13 | String get path => _path ?? route.path; 14 | 15 | _Layer(this._path, {this.method, this.handle, this.route, this.name}) { 16 | name = name ?? ''; 17 | parameters = []; 18 | regExp = pathToRegExp(path, parameters: parameters); 19 | routeParams = {}; 20 | } 21 | 22 | bool match(String pathToCheck, String methodToCheck) { 23 | if (_pathMatches(pathToCheck) && 24 | method != null && 25 | method.toUpperCase() == methodToCheck.toUpperCase()) { 26 | if (parameters.isNotEmpty) { 27 | final match = regExp.matchAsPrefix(pathToCheck); 28 | routeParams.addAll(extract(parameters, match)); 29 | } 30 | 31 | return true; 32 | } else if (name == _InitMiddleware.name) { 33 | return true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | void handleRequest(Request req, Response res) => handle(req, res); 40 | 41 | _Layer withPathPrefix(String pathPrefix) { 42 | return _Layer( 43 | _joinPath(pathPrefix, path), 44 | method: method, 45 | handle: handle, 46 | route: route, 47 | name: name, 48 | ); 49 | } 50 | 51 | String _joinPath(String pathPrefix, String path) { 52 | if (pathPrefix.endsWith('/') && path.startsWith('/')) { 53 | return pathPrefix + path.substring(1); 54 | } else if (pathPrefix.endsWith('/') || path.startsWith('/')) { 55 | return pathPrefix + path; 56 | } else { 57 | return pathPrefix + '/' + path; 58 | } 59 | } 60 | 61 | @override 62 | String toString() { 63 | return 'Layer: { path: $path }'; 64 | } 65 | 66 | bool _pathMatches(String pathToCheck) { 67 | if (route == null || path == null) { 68 | return false; 69 | } 70 | 71 | if (regExp.hasMatch(pathToCheck)) { 72 | return true; 73 | } else if (path == pathToCheck) { 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/middleware/body_parser.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class BodyParser { 4 | static RouteMethod json() { 5 | return (Request req, Response res) { 6 | final contentType = req.headers.contentType; 7 | 8 | if (req.method == 'POST' && 9 | contentType != null && 10 | contentType.mimeType == 'application/json') { 11 | convertBodyToJson(req).then((Map json) { 12 | if (json != null) { 13 | req.body = json; 14 | } 15 | 16 | req.next(); 17 | }); 18 | } else { 19 | req.next(); 20 | } 21 | }; 22 | } 23 | 24 | static Future> convertBodyToJson(Request request) async { 25 | try { 26 | final content = await convert.utf8.decoder.bind(request.request).join(); 27 | 28 | return convert.jsonDecode(content) as Map; 29 | } catch (e) { 30 | print(e); 31 | 32 | return null; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/middleware/cors.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class CorsOptions { 4 | final dynamic origin; 5 | final List methods; 6 | final bool preflightContinue; 7 | final int optionsSuccessStatus; 8 | final bool credentials; 9 | final List allowedHeaders; 10 | final List exposedHeaders; 11 | final int maxAge; 12 | 13 | const CorsOptions({ 14 | this.origin = '*', 15 | this.methods = const [ 16 | _HTTPMethods.get, 17 | _HTTPMethods.head, 18 | _HTTPMethods.put, 19 | _HTTPMethods.patch, 20 | _HTTPMethods.post, 21 | _HTTPMethods.delete, 22 | ], 23 | this.preflightContinue = false, 24 | this.optionsSuccessStatus = 204, 25 | this.credentials = false, 26 | this.allowedHeaders = const [], 27 | this.exposedHeaders = const [], 28 | this.maxAge, 29 | }); 30 | 31 | CorsOptions copyWith({ 32 | dynamic origin, 33 | List methods, 34 | bool preflightContinue, 35 | int optionsSuccessStatus, 36 | bool credentials, 37 | List allowedHeaders, 38 | List exposedHeaders, 39 | int maxAge, 40 | }) { 41 | return CorsOptions( 42 | origin: origin ?? this.origin, 43 | methods: methods ?? this.methods, 44 | preflightContinue: preflightContinue ?? this.preflightContinue, 45 | optionsSuccessStatus: optionsSuccessStatus ?? this.optionsSuccessStatus, 46 | credentials: credentials ?? this.credentials, 47 | allowedHeaders: allowedHeaders ?? this.allowedHeaders, 48 | exposedHeaders: exposedHeaders ?? this.exposedHeaders, 49 | maxAge: maxAge ?? this.maxAge, 50 | ); 51 | } 52 | } 53 | 54 | class CorsMiddleware { 55 | static RouteMethod use({ 56 | dynamic origin = '*', 57 | List methods = const [ 58 | _HTTPMethods.get, 59 | _HTTPMethods.head, 60 | _HTTPMethods.put, 61 | _HTTPMethods.patch, 62 | _HTTPMethods.post, 63 | _HTTPMethods.delete, 64 | ], 65 | bool preflightContinue = false, 66 | int optionsSuccessStatus = 204, 67 | bool credentials = false, 68 | List allowedHeaders = const [], 69 | List exposedHeaders = const [], 70 | int maxAge, 71 | }) { 72 | final options = CorsOptions( 73 | origin: origin, 74 | methods: methods, 75 | preflightContinue: preflightContinue, 76 | optionsSuccessStatus: optionsSuccessStatus, 77 | credentials: credentials, 78 | allowedHeaders: allowedHeaders, 79 | exposedHeaders: exposedHeaders, 80 | maxAge: maxAge, 81 | ); 82 | 83 | return (Request req, Response res) { 84 | final headers = []; 85 | 86 | if (req.method == _HTTPMethods.options) { 87 | headers.addAll(configureOrigin(options, req)); 88 | headers.add(configureCredentials(options)); 89 | headers.add(configureMethods(options)); 90 | headers.addAll(configureAllowedHeaders(options, req)); 91 | headers.add(configureMaxAge(options)); 92 | headers.add(configureExposedHeaders(options)); 93 | _applyHeaders(res, headers); 94 | 95 | if (options.preflightContinue) { 96 | req.next(); 97 | } else { 98 | res.statusCode = options.optionsSuccessStatus; 99 | res.headers.contentLength = 0; 100 | res.end(); 101 | } 102 | } else { 103 | headers.addAll(configureOrigin(options, req)); 104 | headers.add(configureCredentials(options)); 105 | headers.add(configureExposedHeaders(options)); 106 | _applyHeaders(res, headers); 107 | 108 | req.next(); 109 | } 110 | }; 111 | } 112 | 113 | static void _applyHeaders(Response res, List headers) { 114 | headers 115 | .where((mapEntry) => mapEntry != null) 116 | .forEach((mapEntry) => res.headers.add(mapEntry.key, mapEntry.value)); 117 | } 118 | 119 | static bool isOriginAllowed(String origin, dynamic allowedOrigin) { 120 | if (allowedOrigin is List) { 121 | for (var i = 0; i < allowedOrigin.length; ++i) { 122 | if (isOriginAllowed(origin, allowedOrigin[i])) { 123 | return true; 124 | } 125 | } 126 | return false; 127 | } else if (allowedOrigin is String) { 128 | return origin == allowedOrigin; 129 | } else if (allowedOrigin is RegExp) { 130 | return allowedOrigin.hasMatch(origin); 131 | } else { 132 | return allowedOrigin != null; 133 | } 134 | } 135 | 136 | static List configureOrigin(CorsOptions options, Request req) { 137 | final requestOrigin = req.headers.value('origin'); 138 | final headers = []; 139 | bool isAllowed; 140 | 141 | if (options.origin != null && options.origin == '*') { 142 | // allow any origin 143 | headers.add( 144 | MapEntry('Access-Control-Allow-Origin', '*'), 145 | ); 146 | } else if (options.origin is String) { 147 | // fixed origin 148 | headers.add( 149 | MapEntry('Access-Control-Allow-Origin', options.origin), 150 | ); 151 | headers.add( 152 | MapEntry('Vary', 'Origin'), 153 | ); 154 | } else { 155 | isAllowed = isOriginAllowed(requestOrigin, options.origin); 156 | // reflect origin 157 | headers.add( 158 | MapEntry( 159 | 'Access-Control-Allow-Origin', 160 | isAllowed ? requestOrigin : false, 161 | ), 162 | ); 163 | headers.add(MapEntry('Vary', 'Origin')); 164 | } 165 | 166 | return headers; 167 | } 168 | 169 | static MapEntry configureMethods(CorsOptions options) { 170 | return MapEntry( 171 | 'Access-Control-Allow-Methods', 172 | options.methods.join(','), 173 | ); 174 | } 175 | 176 | static MapEntry configureCredentials(CorsOptions options) { 177 | if (options.credentials) { 178 | return MapEntry('Access-Control-Allow-Credentials', 'true'); 179 | } 180 | 181 | return null; 182 | } 183 | 184 | static List configureAllowedHeaders( 185 | CorsOptions options, Request req) { 186 | String allowedHeaders; 187 | final headers = []; 188 | 189 | if (options.allowedHeaders == null) { 190 | allowedHeaders = req.headers.value('access-control-request-headers'); 191 | 192 | headers.add( 193 | MapEntry('Vary', 'Access-Control-Request-Headers'), 194 | ); 195 | } else { 196 | allowedHeaders = options.allowedHeaders.join(','); 197 | } 198 | 199 | if (allowedHeaders != null && allowedHeaders.isNotEmpty) { 200 | headers.add(MapEntry('Access-Control-Allow-Headers', allowedHeaders)); 201 | } 202 | 203 | return headers; 204 | } 205 | 206 | static MapEntry configureMaxAge(CorsOptions options) { 207 | if (options.maxAge != null) { 208 | return MapEntry( 209 | 'Access-Control-Max-Age', 210 | options.maxAge.toString(), 211 | ); 212 | } 213 | 214 | return null; 215 | } 216 | 217 | static MapEntry configureExposedHeaders(CorsOptions options) { 218 | String headers; 219 | 220 | if (headers == null) { 221 | return null; 222 | } else if (headers is List) { 223 | headers = options.exposedHeaders.join(','); 224 | } 225 | 226 | if (headers != null && headers.isNotEmpty) { 227 | return MapEntry('Access-Control-Expose-Headers', headers); 228 | } 229 | 230 | return null; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /lib/src/middleware/init.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class _InitMiddleware { 4 | static final String name = 'EXPRESS_INIT'; 5 | 6 | static void init(Request req, Response res) { 7 | req?.next(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/middleware/logger.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class LoggerMiddleware { 4 | static RouteMethod use({bool includeImmediate = false}) { 5 | return (Request req, Response res) { 6 | if (includeImmediate) { 7 | _logRequest(req, res); 8 | } 9 | 10 | res.isDone.then((successful) { 11 | _logRequest(req, res); 12 | }); 13 | 14 | req.next(); 15 | }; 16 | } 17 | 18 | static void _logRequest(Request req, Response res) { 19 | print(_formatLine(req, res)); 20 | } 21 | 22 | static String _formatLine(Request req, Response res) { 23 | final address = _getIpAddress(req); 24 | final method = req.method; 25 | final url = req.requestedUri; 26 | final status = res.statusCode; 27 | final contentLength = req.contentLength; 28 | final referrer = 29 | req.headers.value('referrer') ?? req.headers.value('referer') ?? ''; 30 | final userAgent = req.headers.value('user-agent'); 31 | 32 | return '$address "$method $url" $status $contentLength ${referrer.isNotEmpty ? '"$referrer"' : ''}"$userAgent"'; 33 | } 34 | 35 | static String _getIpAddress(Request req) => 36 | req.connectionInfo.remoteAddress.address.toString(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/repositories/file_repository.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | abstract class FileRepository { 4 | const FileRepository(); 5 | 6 | Future readAsString(Uri uri); 7 | } 8 | 9 | class _RealFileRepository extends FileRepository { 10 | const _RealFileRepository(); 11 | 12 | @override 13 | Future readAsString(Uri uri) { 14 | return File.fromUri(uri).readAsString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/request.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class Request { 4 | final HttpRequest _request; 5 | Next next; 6 | Map body; 7 | Map params; 8 | 9 | Request(this._request) { 10 | body = {}; 11 | 12 | if (request != null) { 13 | params = Map.from(request.requestedUri.queryParameters); 14 | } 15 | } 16 | 17 | HttpRequest get request => _request; 18 | 19 | X509Certificate get certificate => request.certificate; 20 | 21 | HttpConnectionInfo get connectionInfo => request.connectionInfo; 22 | 23 | int get contentLength => request.contentLength; 24 | 25 | List get cookies => request.cookies; 26 | 27 | HttpHeaders get headers => request.headers; 28 | 29 | Future get isEmpty => request.isEmpty; 30 | 31 | Future get length => request.length; 32 | 33 | String get method => request.method; 34 | 35 | Uri get requestedUri => request.requestedUri; 36 | 37 | HttpResponse get response => request.response; 38 | 39 | HttpSession get session => request.session; 40 | 41 | Uri get uri => request.uri; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/response.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class Response { 4 | HttpResponse response; 5 | App app; 6 | 7 | Response(this.response, this.app); 8 | 9 | Response send(dynamic body) { 10 | if (body is Map) { 11 | json(body); 12 | } else if (body is String) { 13 | if (headers.contentType == null) { 14 | headers.add('Content-Type', 'text/plain'); 15 | } 16 | 17 | encoding = convert.Encoding.getByName('utf-8'); 18 | write(body); 19 | close(); 20 | } 21 | 22 | return this; 23 | } 24 | 25 | void render(String viewName, [Map locals]) { 26 | app.render(viewName, locals, (err, data) { 27 | if (err != null) { 28 | print(err); 29 | 30 | response.close(); 31 | return; 32 | } 33 | 34 | html(data); 35 | }); 36 | } 37 | 38 | Response html(String html) { 39 | headers.contentType = ContentType.html; 40 | send(html); 41 | return this; 42 | } 43 | 44 | Response json(Map body) { 45 | headers.contentType = ContentType.json; 46 | return send(convert.json.encode(body)); 47 | } 48 | 49 | Response set(String headerName, dynamic headerContent) { 50 | headers.add(headerName, headerContent); 51 | return this; 52 | } 53 | 54 | Response status(int code) { 55 | statusCode = code; 56 | return this; 57 | } 58 | 59 | convert.Encoding encoding; 60 | 61 | int get statusCode => response.statusCode; 62 | set statusCode(int newCode) => response.statusCode = newCode; 63 | 64 | Future close() => response.close(); 65 | 66 | HttpConnectionInfo get connectionInfo => response.connectionInfo; 67 | 68 | List get cookies => response.cookies; 69 | 70 | Future get isDone async => 71 | response.done.then((d) => true).catchError((e) => false); 72 | 73 | Future flush() => response.flush(); 74 | 75 | HttpHeaders get headers => response.headers; 76 | 77 | Future redirect( 78 | String location, { 79 | int status = HttpStatus.movedTemporarily, 80 | }) => 81 | response.redirect(Uri.tryParse(location), status: status); 82 | 83 | void write(Object obj) => response.write(obj); 84 | void location(String path) => headers.add('Location', path); 85 | 86 | Future end() => close(); 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/route.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | typedef Next = Function(); 4 | typedef RouteMethod = Function(Request req, Response res); 5 | 6 | class _Route { 7 | final String path; 8 | final List<_Layer> stack = []; 9 | 10 | _Route(this.path); 11 | 12 | void delete(RouteMethod cb) => _setLayer('delete', cb); 13 | void get(RouteMethod cb) => _setLayer('get', cb); 14 | void head(RouteMethod cb) => _setLayer('head', cb); 15 | void patch(RouteMethod cb) => _setLayer('patch', cb); 16 | void post(RouteMethod cb) => _setLayer('post', cb); 17 | void put(RouteMethod cb) => _setLayer('put', cb); 18 | 19 | void _setLayer(String method, RouteMethod cb) => 20 | stack.add(_Layer(null, method: method, handle: cb, route: this)); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/router.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class RouterOptions { 4 | final bool caseSensitive; 5 | final bool mergeParams; 6 | final bool strict; 7 | 8 | const RouterOptions({ 9 | this.caseSensitive = false, 10 | this.mergeParams = false, 11 | this.strict = false, 12 | }); 13 | } 14 | 15 | class Router { 16 | Map params = const {}; 17 | List<_Layer> stack = []; 18 | RouterOptions options; 19 | 20 | Router({this.options = const RouterOptions()}); 21 | 22 | _Route route(String path, String method, RouteMethod handle) { 23 | final route = _Route(path); 24 | 25 | stack.add( 26 | _Layer( 27 | path, 28 | method: method, 29 | handle: handle ?? (req, res) {}, 30 | route: route, 31 | ), 32 | ); 33 | 34 | return route; 35 | } 36 | 37 | /// Handles DELETE requests to the specified path 38 | _Route delete(String path, Function cb) => 39 | route(path, _HTTPMethods.delete, cb); 40 | 41 | /// Handles GET requests to the specified path 42 | _Route get(String path, RouteMethod cb) => route(path, _HTTPMethods.get, cb); 43 | 44 | /// Handles HEAD requests to the specified path 45 | _Route head(String path, RouteMethod cb) => 46 | route(path, _HTTPMethods.head, cb); 47 | 48 | /// Handles PATCH requests to the specified path 49 | _Route patch(String path, RouteMethod cb) => 50 | route(path, _HTTPMethods.patch, cb); 51 | 52 | /// Handles POST requests to the specified path 53 | _Route post(String path, RouteMethod cb) => 54 | route(path, _HTTPMethods.post, cb); 55 | 56 | /// Handles PUT requests to the specified path 57 | _Route put(String path, RouteMethod cb) => route(path, _HTTPMethods.put, cb); 58 | 59 | /// Handles ALL requests to the specified path 60 | List<_Route> all(String path, RouteMethod cb) { 61 | final routes = <_Route>[]; 62 | 63 | for (final method in _HTTPMethods.all) { 64 | routes.add(route(path, method, cb)); 65 | } 66 | 67 | return routes; 68 | } 69 | 70 | Router use(RouteMethod handle) { 71 | final layer = _Layer( 72 | '/', 73 | handle: handle, 74 | name: _InitMiddleware.name, 75 | ); 76 | 77 | stack.add(layer); 78 | 79 | return this; 80 | } 81 | 82 | void handle(Request req, Response res) { 83 | var self = this; 84 | var stack = self.stack; 85 | var index = 0; 86 | 87 | req.next = () { 88 | final path = req.requestedUri.path; 89 | final method = req.method; 90 | 91 | // find next matching layer 92 | _Layer layer; 93 | var match = false; 94 | _Route route; 95 | 96 | while (match != true && index < stack.length) { 97 | layer = stack[index++]; 98 | match = matchLayer(layer, path, method); 99 | route = layer.route; 100 | 101 | if (!match || route is! _Route) { 102 | continue; 103 | } 104 | 105 | req.params.addAll(layer.routeParams); 106 | 107 | if (route.stack.isNotEmpty) { 108 | route.stack.first.handleRequest(req, res); 109 | } else if (layer.handle != null) { 110 | layer.handleRequest(req, res); 111 | } else { 112 | res.status(HttpStatus.notFound).close(); 113 | } 114 | } 115 | 116 | // Matched without a route (Initial Middleware) 117 | if (match && route == null) { 118 | layer.handleRequest(req, res); 119 | } 120 | }; 121 | 122 | req.next(); 123 | } 124 | 125 | bool matchLayer(_Layer layer, String path, String method) { 126 | try { 127 | return layer.match(path, method); 128 | } catch (err) { 129 | print('Error while matching route: $err'); 130 | return false; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/src/view.dart: -------------------------------------------------------------------------------- 1 | part of dart_express; 2 | 3 | class _View { 4 | dynamic rootPath; 5 | String defaultEngine; 6 | String filePath; 7 | String ext; 8 | String name; 9 | Engine engine; 10 | 11 | _View( 12 | this.name, { 13 | this.rootPath = '/', 14 | this.defaultEngine, 15 | Map engines, 16 | }) { 17 | ext = path.extension(name); 18 | 19 | if (ext == null && defaultEngine == null) { 20 | throw Error.safeToString('No default engine or extension are provided.'); 21 | } 22 | 23 | var fileName = name; 24 | 25 | if (ext == null || ext.isEmpty) { 26 | ext = defaultEngine[0] == '.' ? defaultEngine : '.$defaultEngine'; 27 | 28 | fileName += ext; 29 | } 30 | 31 | engine = engines[ext] ?? HtmlEngine.use(); 32 | filePath = lookup(fileName); 33 | } 34 | 35 | void render(Map options, Function callback) => 36 | engine.handler(filePath, options, callback); 37 | 38 | String lookup(String fileName) { 39 | String finalPath; 40 | final List roots = rootPath is List ? rootPath : [rootPath]; 41 | 42 | for (var i = 0; i < roots.length && finalPath == null; i++) { 43 | final root = roots[i]; 44 | final fullFilePath = path.join(root, fileName); 45 | 46 | final loc = path.isAbsolute(fullFilePath) 47 | ? fullFilePath 48 | : path.absolute(fullFilePath); 49 | 50 | finalPath = resolve(loc); 51 | } 52 | 53 | return finalPath; 54 | } 55 | 56 | String resolve(filePath) { 57 | if (_exists(filePath) && _isFile(filePath)) { 58 | return filePath; 59 | } else { 60 | return null; 61 | } 62 | } 63 | 64 | bool _isFile(filePath) { 65 | try { 66 | return File.fromUri(Uri.file(filePath)).statSync().type == 67 | FileSystemEntityType.file; 68 | } catch (e) { 69 | print('$filePath is not a file'); 70 | 71 | return null; 72 | } 73 | } 74 | 75 | bool _exists(filePath) { 76 | return File.fromUri(Uri.file(filePath)).existsSync(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_express 2 | description: An unopinionated express-like HTTP framework built in Dart, includes the ability to render Mustache, Jael and HTML files, and simple routing. 3 | version: 0.5.3 4 | homepage: https://github.com/deriegle/dart-express 5 | 6 | environment: 7 | sdk: ">=2.3.0 <3.0.0" 8 | 9 | dependencies: 10 | path: ^1.6.4 11 | file: ^6.0.0 12 | http: ^0.13.4 13 | meta: ^1.7.0 14 | path_to_regexp: ^0.2.1 15 | mustache4dart: ^3.0.0-dev.0.0 16 | markdown: ^4.0.1 17 | 18 | dev_dependencies: 19 | mockito: ^5.0.17 20 | test: ^1.20.1 21 | lints: ^1.0.1 22 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/dart_express_test.dart: -------------------------------------------------------------------------------- 1 | // import 'package:dart_express/dart_express.dart'; 2 | // import 'package:test/test.dart'; 3 | 4 | void main() { 5 | } 6 | -------------------------------------------------------------------------------- /test/engines/html_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_express/dart_express.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | class MockFileRepository extends Mock implements FileRepository {} 8 | 9 | final mockHtml = ''' 10 | 11 | 12 | Mock HTML Title 13 | 14 | 15 |

Hello, world

16 | 17 | 18 | '''; 19 | 20 | void main() { 21 | test('HtmlEngine has the correct extension', () { 22 | expect(HtmlEngine.ext, '.html'); 23 | }); 24 | 25 | test('HtmlEngine handles reading a file correctly', () async { 26 | final filePath = './views/index.html'; 27 | dynamic error; 28 | String rendered; 29 | 30 | dynamic callback(dynamic err, String string) { 31 | error = err; 32 | rendered = string; 33 | } 34 | 35 | final mockFileRepository = MockFileRepository(); 36 | 37 | when(mockFileRepository.readAsString(Uri.file(filePath))) 38 | .thenAnswer((_) async => mockHtml); 39 | 40 | await HtmlEngine.handler(filePath, {}, callback, mockFileRepository); 41 | 42 | expect(error, null); 43 | expect(rendered, contains('')); 44 | }); 45 | 46 | test('HtmlEngine handles exceptions correctly', () async { 47 | final filePath = './views/index.html'; 48 | dynamic error; 49 | String rendered; 50 | 51 | dynamic callback(dynamic err, String string) { 52 | error = err; 53 | rendered = string; 54 | } 55 | 56 | final mockFileRepository = MockFileRepository(); 57 | 58 | when(mockFileRepository.readAsString(Uri.file(filePath))) 59 | .thenThrow(FileSystemException('Could not find file.')); 60 | 61 | await HtmlEngine.handler(filePath, {}, callback, mockFileRepository); 62 | 63 | expect(error, isA()); 64 | expect((error as FileSystemException).message, 'Could not find file.'); 65 | expect(rendered, null); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/engines/markdown_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_express/dart_express.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | class MockFileRepository extends Mock implements FileRepository {} 8 | 9 | final mockMarkdown = ''' 10 | # Hello, world 11 | '''; 12 | 13 | void main() { 14 | test('HtmlEngine has the correct extension', () { 15 | expect(MarkdownEngine.ext, '.md'); 16 | }); 17 | 18 | test('MarkdownEngine handles reading a file correctly', () async { 19 | final filePath = './views/index.md'; 20 | dynamic error; 21 | String rendered; 22 | 23 | dynamic callback(dynamic err, String string) { 24 | error = err; 25 | rendered = string; 26 | } 27 | 28 | final mockFileRepository = MockFileRepository(); 29 | 30 | when(mockFileRepository.readAsString(Uri.file(filePath))) 31 | .thenAnswer((_) async => mockMarkdown); 32 | 33 | await MarkdownEngine.handler(filePath, {}, callback, mockFileRepository); 34 | 35 | expect(error, null); 36 | expect(rendered, contains('')); 37 | expect(rendered, contains('

Hello, world

')); 38 | }); 39 | 40 | test('MarkdownEngine handles exceptions correctly', () async { 41 | final filePath = './views/index.md'; 42 | dynamic error; 43 | String rendered; 44 | 45 | dynamic callback(dynamic err, String string) { 46 | error = err; 47 | rendered = string; 48 | } 49 | 50 | final mockFileRepository = MockFileRepository(); 51 | 52 | when(mockFileRepository.readAsString(Uri.file(filePath))) 53 | .thenThrow(FileSystemException('Could not find file.')); 54 | 55 | await MarkdownEngine.handler(filePath, {}, callback, mockFileRepository); 56 | 57 | expect(error, isA()); 58 | expect((error as FileSystemException).message, 'Could not find file.'); 59 | expect(rendered, null); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /test/engines/mustache_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_express/dart_express.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | class MockFileRepository extends Mock implements FileRepository {} 8 | 9 | final mockMustache = ''' 10 | 11 | 12 | Mock HTML Title 13 | 14 | 15 | {{#first_name}} 16 |

Hello, {{first_name}}

17 | {{/first_name}} 18 | {{^first_name}} 19 |

Hello, World

20 | {{/first_name}} 21 | 22 | 23 | '''; 24 | 25 | void main() { 26 | test('MustacheEngine has the correct extension', () { 27 | expect(MustacheEngine.ext, '.mustache'); 28 | }); 29 | 30 | test('MustacheEngine handles reading a file correctly', () async { 31 | final filePath = './views/index.mustache'; 32 | dynamic error; 33 | String rendered; 34 | 35 | dynamic callback(dynamic err, String string) { 36 | error = err; 37 | rendered = string; 38 | } 39 | 40 | final mockFileRepository = MockFileRepository(); 41 | 42 | when(mockFileRepository.readAsString(Uri.file(filePath))) 43 | .thenAnswer((_) async => mockMustache); 44 | 45 | await MustacheEngine.handler(filePath, {}, callback, mockFileRepository); 46 | 47 | expect(error, null); 48 | expect(rendered, contains('')); 49 | expect(rendered, contains('Hello, World')); 50 | }); 51 | 52 | test('MustacheEngine handles passing locals into template', () async { 53 | final filePath = './views/index.mustache'; 54 | dynamic error; 55 | String rendered; 56 | 57 | dynamic callback(dynamic err, String string) { 58 | error = err; 59 | rendered = string; 60 | } 61 | 62 | final mockFileRepository = MockFileRepository(); 63 | 64 | when(mockFileRepository.readAsString(Uri.file(filePath))) 65 | .thenAnswer((_) async => mockMustache); 66 | 67 | await MustacheEngine.handler( 68 | filePath, {'first_name': 'Devin'}, callback, mockFileRepository); 69 | 70 | expect(error, null); 71 | expect(rendered, contains('')); 72 | expect(rendered, contains('Hello, Devin')); 73 | }); 74 | 75 | test('MustacheEngine handles exceptions', () async { 76 | final filePath = './views/index.mustache'; 77 | dynamic error; 78 | String rendered; 79 | 80 | dynamic callback(dynamic err, String string) { 81 | error = err; 82 | rendered = string; 83 | } 84 | 85 | final mockFileRepository = MockFileRepository(); 86 | 87 | when(mockFileRepository.readAsString(Uri.file(filePath))) 88 | .thenThrow(FileSystemException('Could not find file')); 89 | 90 | await MustacheEngine.handler( 91 | filePath, {'first_name': 'Devin'}, callback, mockFileRepository); 92 | 93 | expect(error, isA()); 94 | expect((error as FileSystemException).message, 'Could not find file'); 95 | expect(rendered, null); 96 | }); 97 | } 98 | --------------------------------------------------------------------------------