diff --git a/README.md b/README.md index c74e2da..ed2345f 100644 --- a/README.md +++ b/README.md @@ -4,120 +4,8 @@ An express-like web server framework for Dart developers. ## Usage -A simple usage example: - -```dart -import 'package:dart_express/dart_express.dart'; - -main() { - final app = express(); - - app.get('/', (req, res) { - res.json({ - 'hello': 'world', - 'test': true, - }); - }); - - app.listen(3000, (port) => print('Listening on port $port'); -} -``` - -Example with route parameters - -```dart -import 'package:dart_express/dart_express.dart'; - -main() { - final app = express(); - - app.get('/users/:userId/posts/:postId', (req, res) { - res.json({ - 'userId': req.params['userId'], - 'postId': req.params['postId'], - }); - }); - - app.listen(3000, (port) => print('Listening on port $port'); -} -``` - -With Body parsing Middleware: - -```dart -import 'package:dart_express/dart_express.dart'; - -main() { - final app = express(); - - app.use(BodyParser.json()); - - app.post('/post', (req, res) { - print(req.body); - - res.send({ - 'request_body': req.body, - }); - }); - - app.listen(3000, (port) => print('Listening on port $port'); -} -``` - -Using the mustache templating engine - -```dart -import 'package:dart_express/dart_express.dart'; - -main() { - final app = express(); - - app.use(BodyParser.json()); - app.engine(MustacheEngine.use()); - - app.settings - ..viewsPath = 'custom_views_path' - ..viewEngine = 'mustache'; - - app.get('/', (req, res) { - res.render('index', { - 'app_name': 'My Test App', - }); - }); - - app.listen(3000, (port) => print('Listening on port $port'); -} -``` - - - -Listening to Https requests - -```dart - //listen for http requests - app.listen(port: 80, cb: (port) => print('listening for http on port $port')); - - //assign certificate - var context = SecurityContext(); - final chain = Platform.script.resolve('certificates/chain.pem').toFilePath(); - final key = Platform.script.resolve('certificates/key.pem').toFilePath(); - - context.useCertificateChain(chain); - context.usePrivateKey(key); - - //listen for https requests - app.listenHttps( - context, - port: 443, - cb: (port) => print('Listening for https on port $port'), - ); -``` - - +Check out the examples on [Github](https://github.com/deriegle/dart-express/tree/main/example). ### Currently supported View Engines - - Basic HTML -- Mustache - Markdown -- Jael diff --git a/example/api_routes.dart b/example/api_routes.dart deleted file mode 100644 index 4704e2c..0000000 --- a/example/api_routes.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:dart_express/dart_express.dart'; -import 'package:meta/meta.dart'; - -class User { - final int id; - final String email; - - User({@required this.id, @required this.email}); - - Map toJson() { - return { - 'id': id, - 'email': email, - }; - } -} - -Router apiRouter() { - final router = Router(); - final users = [ - User(id: 1, email: 'test@example.com'), - User(id: 2, email: 'test2@example.com'), - ]; - - router.get('/', (req, res) { - res.status(200).json({ - 'hello': 'world', - 'age': 25, - }); - }); - - router.get('/users', (req, res) { - res.status(200).json({ - 'users': users.map((u) => u.toJson()).toList(), - }); - }); - - router.post('/users', (req, res) { - final int id = req.body['id']; - final String email = req.body['email']?.trim(); - - if (id == null) { - res.status(400).json({ - 'errors': [ - {'key': 'id', 'message': 'ID is required'} - ] - }); - return; - } - - if (users.firstWhere((u) => u.id == id, orElse: () => null) != null) { - res.status(400).json({ - 'errors': [ - {'key': 'id', 'message': 'ID must be unique'} - ] - }); - return; - } - - if (email == null || email.isEmpty) { - res.status(400).json({ - 'errors': [ - {'key': 'email', 'message': 'Email is required'} - ] - }); - return; - } - - final user = User( - id: req.body['id'], - email: req.body['email'], - ); - - users.add(user); - - res.status(201).json({ - 'user': user.toJson(), - }); - }); - - return router; -} diff --git a/example/body_parser.dart b/example/body_parser.dart new file mode 100644 index 0000000..e0b2983 --- /dev/null +++ b/example/body_parser.dart @@ -0,0 +1,18 @@ +import 'package:dart_express/dart_express.dart'; + +main() { + final app = express(); + + app.use(BodyParser.json()); + + app.post('/post', (req, res) { + res.send({ + 'body': req.body, + }); + }); + + app.listen( + port: 3000, + cb: (port) => print('Listening on port $port'), + ); +} diff --git a/example/dart_express_example.dart b/example/dart_express_example.dart deleted file mode 100644 index c5d7d90..0000000 --- a/example/dart_express_example.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:dart_express/dart_express.dart'; -import 'package:path/path.dart' as path; -import './api_routes.dart'; -import './view_routes.dart'; - -const int port = 5000; - -void main() { - final app = express(); - - app.use(BodyParser.json()); - app.use(CorsMiddleware.use()); - app.use(LoggerMiddleware.use(includeImmediate: true)); - - app.engine(MarkdownEngine.use()); - app.engine(MustacheEngine.use()); - - app.set('print routes', true); - app.set('views', path.join(path.current, 'example/views')); - app.set('view engine', 'mustache'); - - app.useRouter('/api/', apiRouter()); - app.useRouter('/', viewRouter()); - - app.listen(port: port, cb: (int port) => print('Listening on port $port')); -} diff --git a/example/https_requests.dart b/example/https_requests.dart new file mode 100644 index 0000000..0d0181f --- /dev/null +++ b/example/https_requests.dart @@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:dart_express/dart_express.dart'; + +main() { + final app = express(); + + app.get('/', (req, res) { + res.json({ + 'hello': 'world', + 'test': true, + }); + }); + + final chain = Platform.script.resolve('certificates/chain.pem').toFilePath(); + final key = Platform.script.resolve('certificates/key.pem').toFilePath(); + final context = SecurityContext() + ..useCertificateChain(chain) + ..usePrivateKey(key); + + app.listen( + port: 80, + cb: (port) => print('listening for http on port $port'), + ); + + //listen for https requests + app.listenHttps( + context, + port: 443, + cb: (port) => print('Listening for https on port $port'), + ); +} diff --git a/example/more_advanced_api.dart b/example/more_advanced_api.dart new file mode 100644 index 0000000..277b17a --- /dev/null +++ b/example/more_advanced_api.dart @@ -0,0 +1,211 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:dart_express/dart_express.dart'; + +const int port = 5000; + +class User { + final int id; + final String? email; + + User({ + required this.id, + required this.email, + }); + + User copyWith({String? email}) { + return User(id: id, email: email ?? this.email); + } + + Map toJson() { + return { + 'id': id, + 'email': email, + }; + } +} + +Router apiRouter() { + final router = Router(); + final users = [ + User(id: 1, email: 'test@example.com'), + User(id: 2, email: 'test2@example.com'), + ]; + + router.get('/users', (req, res) { + res.status(200).json({ + 'users': users.map((u) => u.toJson()).toList(), + }); + }); + + router.post('/users', (req, res) { + final int? id = req.body['id']; + final String? email = req.body['email']?.trim(); + final Map> errors = {}; + + if (id == null) { + final errorMessage = 'ID is required'; + + if (errors.containsKey('id')) { + errors['id']!.add(errorMessage); + } else { + errors['id'] = [errorMessage]; + } + } + + if (users.firstWhereOrNull((u) => u.id == id) != null) { + final errorMessage = 'ID must be unique.'; + + if (errors.containsKey('id')) { + errors['id']!.add(errorMessage); + } else { + errors['id'] = [errorMessage]; + } + } + + if (email == null || email.isEmpty) { + final errorMessage = 'Email is required'; + + if (errors.containsKey('email')) { + errors['email']!.add(errorMessage); + } else { + errors['email'] = [errorMessage]; + } + } + + if (email != null && !email.contains('@')) { + final errorMessage = 'Email is not valid.'; + + if (errors.containsKey('email')) { + errors['email']!.add(errorMessage); + } else { + errors['email'] = [errorMessage]; + } + } + + if (errors.keys.isNotEmpty) { + final List> errorJson = []; + + for (final key in errors.keys) { + for (final errorMessage in errors[key]!) { + errorJson.add({'key': key, 'message': errorMessage}); + } + } + + return res.json({ + 'errors': errorJson, + }); + } + + final user = User(id: id!, email: email!); + + users.add(user); + + res.status(201).json({ + 'user': user.toJson(), + }); + }); + + router.get('/users/:userId', (req, res) { + final String userId = req.params['userId']; + final id = int.tryParse(userId); + + if (id == null) { + return res.status(404).end(); + } + + final user = users.firstWhereOrNull((element) => element.id == id); + + if (user == null) { + return res.status(404).end(); + } + + return res.json({ + 'user': user.toJson(), + }); + }); + + router.post('/users/:userId', (req, res) { + final String userId = req.params['userId']; + final id = int.tryParse(userId); + final email = req.body['email']; + final Map> errors = {}; + + if (email == null || email.isEmpty) { + final errorMessage = 'Email is required'; + + if (errors.containsKey('email')) { + errors['email']!.add(errorMessage); + } else { + errors['email'] = [errorMessage]; + } + } + + if (email != null && !email.contains('@')) { + final errorMessage = 'Email is not valid.'; + + if (errors.containsKey('email')) { + errors['email']!.add(errorMessage); + } else { + errors['email'] = [errorMessage]; + } + } + + if (errors.keys.isNotEmpty) { + final List> errorJson = []; + + for (final key in errors.keys) { + for (final errorMessage in errors[key]!) { + errorJson.add({'key': key, 'message': errorMessage}); + } + } + + return res.json({ + 'errors': errorJson, + }); + } + + final user = users.firstWhereOrNull((element) => element.id == id); + + if (user == null) { + return res.status(404).end(); + } + + final index = users.indexWhere((element) => element.id == id); + users[index] = user.copyWith(email: email); + + return res.json({ + 'user': user.toJson(), + }); + }); + + router.delete('/users/:userId', (req, res) { + final String userId = req.params['userId']; + final id = int.tryParse(userId); + final user = users.firstWhereOrNull((element) => element.id == id); + + if (user == null) { + return res.status(404).end(); + } + + users.remove(user); + + return res.status(200).end(); + }); + + return router; +} + +void main() { + final app = express(); + + app.use(BodyParser.json()); + app.use(CorsMiddleware.use()); + app.use(LoggerMiddleware.use(includeImmediate: true)); + + app.useRouter('/api/', apiRouter()); + + app.listen( + port: port, + cb: (int port) => print('Listening on port $port'), + ); +} diff --git a/example/route_parameters.dart b/example/route_parameters.dart new file mode 100644 index 0000000..4b6681c --- /dev/null +++ b/example/route_parameters.dart @@ -0,0 +1,17 @@ +import 'package:dart_express/dart_express.dart'; + +main() { + final app = express(); + + app.get('/users/:userId/posts/:postId', (req, res) { + res.json({ + 'userId': req.params['userId'], + 'postId': req.params['postId'], + }); + }); + + app.listen( + port: 3000, + cb: (port) => print('Listening on port $port'), + ); +} diff --git a/example/simple.dart b/example/simple.dart new file mode 100644 index 0000000..2c8cb7e --- /dev/null +++ b/example/simple.dart @@ -0,0 +1,17 @@ +import 'package:dart_express/dart_express.dart'; + +main() { + final app = express(); + + app.get('/', (req, res) { + res.json({ + 'hello': 'world', + 'test': true, + }); + }); + + app.listen( + port: 3000, + cb: (port) => print('Listening on port $port'), + ); +} diff --git a/example/view_routes.dart b/example/view_routes.dart deleted file mode 100644 index 0aa73c3..0000000 --- a/example/view_routes.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:dart_express/dart_express.dart'; - -Router viewRouter() { - final router = Router(); - - router.get('/example', (req, res) { - res.render('example.md'); - }); - - router.all('/secret', (req, res) { - print('Accessing the secret section'); - req.next(); - }); - - router.get('/secret', (req, res) { - res.send('Secret Home Page'); - }); - - router.get('/secret/2', (req, res) { - res.send('Secret Home Page'); - }); - - return router; -} diff --git a/example/views/about.mustache b/example/views/about.mustache deleted file mode 100644 index 3bb2c57..0000000 --- a/example/views/about.mustache +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - About - - -

About Page

- - {{#person}} -

{{ first_name }}

- {{/person}} - - \ No newline at end of file diff --git a/example/views/example.md b/example/views/example.md deleted file mode 100644 index 3a26b76..0000000 --- a/example/views/example.md +++ /dev/null @@ -1,9 +0,0 @@ -# About Dart Express -Dart Express is a library built in the dart programming language for building quick and easy web servers. - -## TO DO List - -- Check it out -- On Github -- Or pub.dev -- to get started diff --git a/example/views/index.mustache b/example/views/index.mustache deleted file mode 100644 index 27a323e..0000000 --- a/example/views/index.mustache +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - My Index Page - - -

Index Page

-

Does this work?

- - \ No newline at end of file diff --git a/lib/dart_express.dart b/lib/dart_express.dart index 70b7819..d6e2cc9 100644 --- a/lib/dart_express.dart +++ b/lib/dart_express.dart @@ -9,7 +9,6 @@ import 'dart:convert' as convert; import 'dart:async'; import 'dart:io'; import 'package:markdown/markdown.dart' deferred as markdown; -import 'package:mustache4dart/mustache4dart.dart' deferred as mustache; import 'package:path_to_regexp/path_to_regexp.dart'; export 'dart:io' show HttpStatus; @@ -39,6 +38,5 @@ part 'src/exceptions/view_exception.dart'; /// View Engines part 'src/engines/engine.dart'; -part 'src/engines/mustache.dart'; part 'src/engines/html.dart'; part 'src/engines/markdown.dart'; diff --git a/lib/src/app.dart b/lib/src/app.dart index ea92cc6..745e2da 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -8,18 +8,18 @@ class _AppSettings { _AppSettings({ this.cache = true, - String viewsPath, + String? viewsPath, this.printRoutes = false, this.viewEngine = 'html', }) : viewsPath = viewsPath ?? path.absolute('views'); } class App { - _AppSettings _settings; - Map cache; - Map _engines; - HttpServer _server; - Router _router; + late _AppSettings _settings; + late Map cache; + late Map _engines; + late HttpServer _server; + Router? _router; App() { _settings = _AppSettings(); @@ -71,7 +71,7 @@ class App { App use(Function cb) { _lazyRouter(); - _router.use(cb); + _router!.use(cb as dynamic Function(Request, Response)); return this; } @@ -91,18 +91,18 @@ class App { /// Examples: /// /// app.engine(JaelEngine.use()); - /// /// app.engine(MustacheEngine.use()); - /// /// app.engine(MarkdownEngine.use()); App engine(Engine engine) { - if (engine.ext == null) { - throw Error.safeToString('Engine extension must be defined.'); + if (engine.ext.isEmpty) { + throw Error.safeToString('View Engine extension must be defined.'); } - if (_engines[engine.ext] != null) { + if (_engines.containsKey(engine.ext)) { + final existingEngine = _engines[engine.ext]; + throw Error.safeToString( - 'A View engine for the ${engine.ext} extension has already been defined.', + 'A view engine has already been defined for extension ${engine.ext}: $existingEngine', ); } @@ -112,8 +112,8 @@ class App { } /// Handles DELETE requests to the specified path - _Route delete(String path, Function cb) => - _buildRoute(path, _HTTPMethods.delete, cb); + _Route delete(String path, Function cb) => _buildRoute( + path, _HTTPMethods.delete, cb as dynamic Function(Request, Response)); /// Handles GET requests to the specified path _Route get(String path, RouteMethod cb) => @@ -149,8 +149,11 @@ class App { /// Starts the HTTP server listening on the specified port /// /// All Request and Response objects will be wrapped and handled by the Router - Future listen( - {InternetAddress address, int port, Function(int) cb}) async { + Future listen({ + InternetAddress? address, + required int port, + Function(int)? cb, + }) async { _server = await HttpServer.bind( address ?? InternetAddress.loopbackIPv4, port, @@ -166,9 +169,9 @@ class App { /// You can add Certifications to the [SecurityContext] Future listenHttps( SecurityContext securityContext, { - InternetAddress address, - int port, - Function(int) cb, + InternetAddress? address, + required int port, + Function(int)? cb, }) async { _server = await HttpServer.bindSecure( address ?? InternetAddress.loopbackIPv4, port, securityContext); @@ -176,12 +179,12 @@ class App { _mapToRoutes(cb); } - void _mapToRoutes(Function(int) cb) { + void _mapToRoutes(Function(int)? cb) { _server.listen((HttpRequest req) { final request = Request(req); final response = Response(req.response, this); - _router.handle(request, response); + _router!.handle(request, response); }); if (_settings.printRoutes) { @@ -200,18 +203,15 @@ class App { /// Provide a Map of local variables to the template void render( String fileName, - Map locals, + Map? locals, Function callback, ) { - _settings.cache ??= true; - final view = _getViewFromFileName(fileName); - view.render(locals, callback); } void _printRoutes() { - _router.stack.where((layer) => layer.route != null).forEach((layer) { + _router!.stack.where((layer) => layer.route != null).forEach((layer) { print('[${layer.method}] ${layer.path}'); }); } @@ -222,36 +222,19 @@ class App { Router _lazyRouter() => _router ??= Router().use(_InitMiddleware.init); _View _getViewFromFileName(String fileName) { - _View view; - - if (_settings.cache) { - view = cache[fileName]; + if (_settings.cache && cache.containsKey(fileName)) { + return cache[fileName]!; } - if (view == null) { - view = _View( - fileName, - defaultEngine: _settings.viewEngine, - engines: _engines, - rootPath: _settings.viewsPath, - ); - - if (view.filePath == null) { - String dirs; - - if (view.rootPath is List) { - dirs = - 'directories "${view.rootPath.join(', ')}" or "${view.rootPath[view.rootPath.length - 1]}"'; - } else { - dirs = 'directory "${view.rootPath}"'; - } - - throw _ViewException(view, dirs); - } + final view = _View( + fileName, + defaultEngine: _settings.viewEngine, + engines: _engines, + rootPath: _settings.viewsPath, + ); - if (_settings.cache) { - cache[fileName] = view; - } + if (_settings.cache) { + cache[fileName] = view; } return view; diff --git a/lib/src/engines/engine.dart b/lib/src/engines/engine.dart index 2ea373e..05d716e 100644 --- a/lib/src/engines/engine.dart +++ b/lib/src/engines/engine.dart @@ -1,8 +1,8 @@ part of dart_express; -typedef HandlerCallback = Function(dynamic e, String rendered); +typedef HandlerCallback = Function(dynamic e, String? rendered); typedef Handler = Function( - String filePath, Map locals, HandlerCallback cb); + String filePath, Map? locals, HandlerCallback cb); class Engine { final String ext; diff --git a/lib/src/engines/html.dart b/lib/src/engines/html.dart index 1be0fc3..02103f4 100644 --- a/lib/src/engines/html.dart +++ b/lib/src/engines/html.dart @@ -6,9 +6,9 @@ class HtmlEngine { /// Called when rendering an HTML file in the Response /// /// [locals] is ignored for HTML files - static Future handler( + static Future handler( String filePath, - Map locals, + Map? locals, HandlerCallback callback, [ FileRepository fileRepository = const _RealFileRepository(), ]) async { diff --git a/lib/src/engines/markdown.dart b/lib/src/engines/markdown.dart index b28dbd3..3cea2ad 100644 --- a/lib/src/engines/markdown.dart +++ b/lib/src/engines/markdown.dart @@ -14,9 +14,9 @@ class MarkdownEngine { /// locals['beforeMarkdown'] will be rendered at the top of the body before the markdown. /// /// locals['afterMarkdown'] will be rendered at the bottom of the body after the markdown - static Future handler( + static Future handler( String filePath, - Map locals, + Map? locals, HandlerCallback callback, [ FileRepository fileRepository = const _RealFileRepository(), ]) async { @@ -25,8 +25,12 @@ class MarkdownEngine { try { final fileContents = await fileRepository.readAsString(Uri.file(filePath)); - final rendered = - _wrapInHTMLTags(markdown.markdownToHtml(fileContents), locals); + + final rendered = _wrapInHTMLTags( + markdown.markdownToHtml(fileContents), + locals ?? {}, + ); + callback(null, rendered); return rendered; } catch (e) { @@ -35,7 +39,10 @@ class MarkdownEngine { } } - static String _wrapInHTMLTags(String html, Map options) { + static String _wrapInHTMLTags( + String html, + Map options, + ) { return ''' diff --git a/lib/src/engines/mustache.dart b/lib/src/engines/mustache.dart deleted file mode 100644 index ff76868..0000000 --- a/lib/src/engines/mustache.dart +++ /dev/null @@ -1,27 +0,0 @@ -part of dart_express; - -class MustacheEngine { - static String ext = '.mustache'; - - static Future handler( - String filePath, - Map options, - HandlerCallback callback, [ - FileRepository fileRepository = const _RealFileRepository(), - ]) async { - await mustache.loadLibrary(); - - try { - final fileContents = - await fileRepository.readAsString(Uri.file(filePath)); - final rendered = mustache.render(fileContents, options ?? {}); - - return callback(null, rendered); - } catch (e) { - callback(e, null); - return; - } - } - - static Engine use() => Engine(MustacheEngine.ext, MustacheEngine.handler); -} diff --git a/lib/src/exceptions/view_exception.dart b/lib/src/exceptions/view_exception.dart index 731dac6..80333ec 100644 --- a/lib/src/exceptions/view_exception.dart +++ b/lib/src/exceptions/view_exception.dart @@ -1,13 +1,13 @@ part of dart_express; class _ViewException implements Error { - final _View view; + final String name; + final String ext; final String directory; - _ViewException(this.view, this.directory); + _ViewException(this.name, this.ext, this.directory); - String get message => - 'ViewException(Failed to find ${view.name}${view.ext} in $directory)'; + String get message => 'ViewException(Failed to find $name$ext in $directory)'; @override String toString() => message; diff --git a/lib/src/layer.dart b/lib/src/layer.dart index 7bcd512..c1d749f 100644 --- a/lib/src/layer.dart +++ b/lib/src/layer.dart @@ -1,31 +1,33 @@ part of dart_express; class _Layer { - final String _path; - String method; + final String? _path; + String? method; RouteMethod handle; - _Route route; - String name; - RegExp regExp; - List parameters; - Map routeParams; + _Route? route; + String? name; + List? parameters; + late Map routeParams; - String get path => _path ?? route.path; + String get path => _path ?? route?.path ?? ''; + RegExp get regExp => pathToRegExp(path, parameters: parameters); - _Layer(this._path, {this.method, this.handle, this.route, this.name}) { - name = name ?? ''; - parameters = []; - regExp = pathToRegExp(path, parameters: parameters); - routeParams = {}; - } + _Layer( + this._path, { + required this.handle, + this.route, + this.method, + this.name = '', + }) : parameters = [], + routeParams = {}; bool match(String pathToCheck, String methodToCheck) { if (_pathMatches(pathToCheck) && method != null && - method.toUpperCase() == methodToCheck.toUpperCase()) { - if (parameters.isNotEmpty) { - final match = regExp.matchAsPrefix(pathToCheck); - routeParams.addAll(extract(parameters, match)); + method!.toUpperCase() == methodToCheck.toUpperCase()) { + if (parameters!.isNotEmpty) { + final match = regExp.matchAsPrefix(pathToCheck)!; + routeParams.addAll(extract(parameters!, match)); } return true; @@ -64,7 +66,7 @@ class _Layer { } bool _pathMatches(String pathToCheck) { - if (route == null || path == null) { + if (route == null || path.isEmpty) { return false; } diff --git a/lib/src/middleware/body_parser.dart b/lib/src/middleware/body_parser.dart index 08940c4..7b7d8d9 100644 --- a/lib/src/middleware/body_parser.dart +++ b/lib/src/middleware/body_parser.dart @@ -8,7 +8,7 @@ class BodyParser { if (req.method == 'POST' && contentType != null && contentType.mimeType == 'application/json') { - convertBodyToJson(req).then((Map json) { + convertBodyToJson(req).then((Map? json) { if (json != null) { req.body = json; } @@ -21,11 +21,13 @@ class BodyParser { }; } - static Future> convertBodyToJson(Request request) async { + static Future?> convertBodyToJson( + Request request, + ) async { try { final content = await convert.utf8.decoder.bind(request.request).join(); - return convert.jsonDecode(content) as Map; + return convert.jsonDecode(content) as Map?; } catch (e) { print(e); diff --git a/lib/src/middleware/cors.dart b/lib/src/middleware/cors.dart index b3aabb4..83a96d6 100644 --- a/lib/src/middleware/cors.dart +++ b/lib/src/middleware/cors.dart @@ -8,7 +8,7 @@ class CorsOptions { final bool credentials; final List allowedHeaders; final List exposedHeaders; - final int maxAge; + final int? maxAge; const CorsOptions({ this.origin = '*', @@ -30,13 +30,13 @@ class CorsOptions { CorsOptions copyWith({ dynamic origin, - List methods, - bool preflightContinue, - int optionsSuccessStatus, - bool credentials, - List allowedHeaders, - List exposedHeaders, - int maxAge, + List? methods, + bool? preflightContinue, + int? optionsSuccessStatus, + bool? credentials, + List? allowedHeaders, + List? exposedHeaders, + int? maxAge, }) { return CorsOptions( origin: origin ?? this.origin, @@ -67,7 +67,7 @@ class CorsMiddleware { bool credentials = false, List allowedHeaders = const [], List exposedHeaders = const [], - int maxAge, + int? maxAge, }) { final options = CorsOptions( origin: origin, @@ -81,7 +81,7 @@ class CorsMiddleware { ); return (Request req, Response res) { - final headers = []; + final headers = []; if (req.method == _HTTPMethods.options) { headers.addAll(configureOrigin(options, req)); @@ -110,13 +110,13 @@ class CorsMiddleware { }; } - static void _applyHeaders(Response res, List headers) { + static void _applyHeaders(Response res, List headers) { headers .where((mapEntry) => mapEntry != null) - .forEach((mapEntry) => res.headers.add(mapEntry.key, mapEntry.value)); + .forEach((mapEntry) => res.headers.add(mapEntry!.key, mapEntry.value)); } - static bool isOriginAllowed(String origin, dynamic allowedOrigin) { + static bool isOriginAllowed(String? origin, dynamic allowedOrigin) { if (allowedOrigin is List) { for (var i = 0; i < allowedOrigin.length; ++i) { if (isOriginAllowed(origin, allowedOrigin[i])) { @@ -127,7 +127,7 @@ class CorsMiddleware { } else if (allowedOrigin is String) { return origin == allowedOrigin; } else if (allowedOrigin is RegExp) { - return allowedOrigin.hasMatch(origin); + return allowedOrigin.hasMatch(origin!); } else { return allowedOrigin != null; } @@ -173,7 +173,7 @@ class CorsMiddleware { ); } - static MapEntry configureCredentials(CorsOptions options) { + static MapEntry? configureCredentials(CorsOptions options) { if (options.credentials) { return MapEntry('Access-Control-Allow-Credentials', 'true'); } @@ -183,27 +183,24 @@ class CorsMiddleware { static List configureAllowedHeaders( CorsOptions options, Request req) { - String allowedHeaders; + String? allowedHeaders; final headers = []; - if (options.allowedHeaders == null) { - allowedHeaders = req.headers.value('access-control-request-headers'); - - headers.add( - MapEntry('Vary', 'Access-Control-Request-Headers'), - ); + if (options.allowedHeaders.isEmpty) { + allowedHeaders = req.headers.value('access-control-request-headers')!; + headers.add(MapEntry('Vary', 'Access-Control-Request-Headers')); } else { allowedHeaders = options.allowedHeaders.join(','); } - if (allowedHeaders != null && allowedHeaders.isNotEmpty) { + if (allowedHeaders.isNotEmpty) { headers.add(MapEntry('Access-Control-Allow-Headers', allowedHeaders)); } return headers; } - static MapEntry configureMaxAge(CorsOptions options) { + static MapEntry? configureMaxAge(CorsOptions options) { if (options.maxAge != null) { return MapEntry( 'Access-Control-Max-Age', @@ -214,17 +211,12 @@ class CorsMiddleware { return null; } - static MapEntry configureExposedHeaders(CorsOptions options) { - String headers; - - if (headers == null) { - return null; - } else if (headers is List) { - headers = options.exposedHeaders.join(','); - } - - if (headers != null && headers.isNotEmpty) { - return MapEntry('Access-Control-Expose-Headers', headers); + static MapEntry? configureExposedHeaders(CorsOptions options) { + if (options.exposedHeaders.isNotEmpty) { + return MapEntry( + 'Access-Control-Expose-Headers', + options.exposedHeaders.join(','), + ); } return null; diff --git a/lib/src/middleware/init.dart b/lib/src/middleware/init.dart index 08333be..3e8642d 100644 --- a/lib/src/middleware/init.dart +++ b/lib/src/middleware/init.dart @@ -4,6 +4,6 @@ class _InitMiddleware { static final String name = 'EXPRESS_INIT'; static void init(Request req, Response res) { - req?.next(); + req.next(); } } diff --git a/lib/src/middleware/logger.dart b/lib/src/middleware/logger.dart index 0bc320c..db76189 100644 --- a/lib/src/middleware/logger.dart +++ b/lib/src/middleware/logger.dart @@ -33,5 +33,5 @@ class LoggerMiddleware { } static String _getIpAddress(Request req) => - req.connectionInfo.remoteAddress.address.toString(); + req.connectionInfo!.remoteAddress.address.toString(); } diff --git a/lib/src/repositories/file_repository.dart b/lib/src/repositories/file_repository.dart index 547902a..6781947 100644 --- a/lib/src/repositories/file_repository.dart +++ b/lib/src/repositories/file_repository.dart @@ -6,7 +6,7 @@ abstract class FileRepository { Future readAsString(Uri uri); } -class _RealFileRepository extends FileRepository { +class _RealFileRepository implements FileRepository { const _RealFileRepository(); @override diff --git a/lib/src/request.dart b/lib/src/request.dart index 692f1a8..ec0cd47 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -2,23 +2,21 @@ part of dart_express; class Request { final HttpRequest _request; - Next next; - Map body; - Map params; + late Next next; + late Map body; + late Map params; Request(this._request) { body = {}; - - if (request != null) { - params = Map.from(request.requestedUri.queryParameters); - } + params = Map.from(request.requestedUri.queryParameters); + next = () {}; } HttpRequest get request => _request; - X509Certificate get certificate => request.certificate; + X509Certificate? get certificate => request.certificate; - HttpConnectionInfo get connectionInfo => request.connectionInfo; + HttpConnectionInfo? get connectionInfo => request.connectionInfo; int get contentLength => request.contentLength; diff --git a/lib/src/response.dart b/lib/src/response.dart index 55e01d0..b86d42b 100644 --- a/lib/src/response.dart +++ b/lib/src/response.dart @@ -8,7 +8,7 @@ class Response { Response send(dynamic body) { if (body is Map) { - json(body); + json(body as Map); } else if (body is String) { if (headers.contentType == null) { headers.add('Content-Type', 'text/plain'); @@ -22,7 +22,7 @@ class Response { return this; } - void render(String viewName, [Map locals]) { + void render(String viewName, [Map? locals]) { app.render(viewName, locals, (err, data) { if (err != null) { print(err); @@ -56,14 +56,14 @@ class Response { return this; } - convert.Encoding encoding; + convert.Encoding? encoding; int get statusCode => response.statusCode; set statusCode(int newCode) => response.statusCode = newCode; Future close() => response.close(); - HttpConnectionInfo get connectionInfo => response.connectionInfo; + HttpConnectionInfo? get connectionInfo => response.connectionInfo; List get cookies => response.cookies; @@ -78,7 +78,7 @@ class Response { String location, { int status = HttpStatus.movedTemporarily, }) => - response.redirect(Uri.tryParse(location), status: status); + response.redirect(Uri.tryParse(location)!, status: status); void write(Object obj) => response.write(obj); void location(String path) => headers.add('Location', path); diff --git a/lib/src/router.dart b/lib/src/router.dart index 740c8d9..6e3090b 100644 --- a/lib/src/router.dart +++ b/lib/src/router.dart @@ -26,7 +26,7 @@ class Router { _Layer( path, method: method, - handle: handle ?? (req, res) {}, + handle: handle, route: route, ), ); @@ -35,8 +35,8 @@ class Router { } /// Handles DELETE requests to the specified path - _Route delete(String path, Function cb) => - route(path, _HTTPMethods.delete, cb); + _Route delete(String path, Function cb) => route( + path, _HTTPMethods.delete, cb as dynamic Function(Request, Response)); /// Handles GET requests to the specified path _Route get(String path, RouteMethod cb) => route(path, _HTTPMethods.get, cb); @@ -80,18 +80,18 @@ class Router { } void handle(Request req, Response res) { - var self = this; - var stack = self.stack; - var index = 0; + Router self = this; + final stack = self.stack; + int index = 0; req.next = () { final path = req.requestedUri.path; final method = req.method; // find next matching layer - _Layer layer; + late _Layer layer; var match = false; - _Route route; + _Route? route; while (match != true && index < stack.length) { layer = stack[index++]; @@ -106,10 +106,8 @@ class Router { if (route.stack.isNotEmpty) { route.stack.first.handleRequest(req, res); - } else if (layer.handle != null) { - layer.handleRequest(req, res); } else { - res.status(HttpStatus.notFound).close(); + layer.handleRequest(req, res); } } diff --git a/lib/src/view.dart b/lib/src/view.dart index df74726..af4d992 100644 --- a/lib/src/view.dart +++ b/lib/src/view.dart @@ -2,29 +2,28 @@ part of dart_express; class _View { dynamic rootPath; - String defaultEngine; - String filePath; - String ext; + String? defaultEngine; + late String ext; String name; - Engine engine; + late String filePath; + late Engine engine; _View( this.name, { this.rootPath = '/', this.defaultEngine, - Map engines, + required Map engines, }) { - ext = path.extension(name); + final extension = path.extension(name); - if (ext == null && defaultEngine == null) { + if (extension.isEmpty && defaultEngine == null) { throw Error.safeToString('No default engine or extension are provided.'); } var fileName = name; - if (ext == null || ext.isEmpty) { - ext = defaultEngine[0] == '.' ? defaultEngine : '.$defaultEngine'; - + if (extension.isEmpty) { + ext = defaultEngine![0] == '.' ? defaultEngine! : '.$defaultEngine'; fileName += ext; } @@ -32,11 +31,18 @@ class _View { filePath = lookup(fileName); } - void render(Map options, Function callback) => - engine.handler(filePath, options, callback); + void render( + Map? options, + Function callback, + ) => + engine.handler( + filePath, + options, + callback as dynamic Function(dynamic, String?), + ); String lookup(String fileName) { - String finalPath; + String? finalPath; final List roots = rootPath is List ? rootPath : [rootPath]; for (var i = 0; i < roots.length && finalPath == null; i++) { @@ -50,18 +56,30 @@ class _View { finalPath = resolve(loc); } + if (finalPath == null) { + String dirs; + + if (rootPath is List) { + dirs = 'directories "${rootPath.join(', ')}" or "${rootPath.last}"'; + } else { + dirs = 'directory "$rootPath"'; + } + + throw _ViewException(name, ext, dirs); + } + return finalPath; } - String resolve(filePath) { - if (_exists(filePath) && _isFile(filePath)) { + String? resolve(filePath) { + if (_exists(filePath) && _isFile(filePath)!) { return filePath; } else { return null; } } - bool _isFile(filePath) { + bool? _isFile(filePath) { try { return File.fromUri(Uri.file(filePath)).statSync().type == FileSystemEntityType.file; diff --git a/pubspec.yaml b/pubspec.yaml index 8b3fbda..6c3168e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,18 +4,19 @@ version: 0.5.3 homepage: https://github.com/deriegle/dart-express environment: - sdk: ">=2.3.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: path: ^1.6.4 file: ^6.0.0 http: ^0.13.4 meta: ^1.7.0 - path_to_regexp: ^0.2.1 - mustache4dart: ^3.0.0-dev.0.0 + path_to_regexp: ^0.4.0 markdown: ^4.0.1 + collection: ^1.15.0-nullsafety.4 dev_dependencies: mockito: ^5.0.17 test: ^1.20.1 lints: ^1.0.1 + build_runner: ^2.1.7 diff --git a/test/dart_express_test.dart b/test/dart_express_test.dart deleted file mode 100644 index 00dcd9e..0000000 --- a/test/dart_express_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -// import 'package:dart_express/dart_express.dart'; -// import 'package:test/test.dart'; - -void main() { -} diff --git a/test/engines/html_test.dart b/test/engines/html_test.dart index 4756099..fb3af0c 100644 --- a/test/engines/html_test.dart +++ b/test/engines/html_test.dart @@ -1,10 +1,11 @@ import 'dart:io'; import 'package:dart_express/dart_express.dart'; +import 'package:mockito/annotations.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; -class MockFileRepository extends Mock implements FileRepository {} +import 'html_test.mocks.dart'; final mockHtml = ''' @@ -17,6 +18,7 @@ final mockHtml = ''' '''; +@GenerateMocks([FileRepository]) void main() { test('HtmlEngine has the correct extension', () { expect(HtmlEngine.ext, '.html'); @@ -25,9 +27,9 @@ void main() { test('HtmlEngine handles reading a file correctly', () async { final filePath = './views/index.html'; dynamic error; - String rendered; + String? rendered; - dynamic callback(dynamic err, String string) { + dynamic callback(dynamic err, String? string) { error = err; rendered = string; } @@ -46,9 +48,9 @@ void main() { test('HtmlEngine handles exceptions correctly', () async { final filePath = './views/index.html'; dynamic error; - String rendered; + String? rendered; - dynamic callback(dynamic err, String string) { + dynamic callback(dynamic err, String? string) { error = err; rendered = string; } diff --git a/test/engines/html_test.mocks.dart b/test/engines/html_test.mocks.dart new file mode 100644 index 0000000..675a1d8 --- /dev/null +++ b/test/engines/html_test.mocks.dart @@ -0,0 +1,31 @@ +// Mocks generated by Mockito 5.0.17 from annotations +// in dart_express/test/engines/html_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:dart_express/dart_express.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [FileRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileRepository extends _i1.Mock implements _i2.FileRepository { + MockFileRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future readAsString(Uri? uri) => + (super.noSuchMethod(Invocation.method(#readAsString, [uri]), + returnValue: Future.value('')) as _i3.Future); +} diff --git a/test/engines/markdown_test.dart b/test/engines/markdown_test.dart index 7e869fc..4e8c530 100644 --- a/test/engines/markdown_test.dart +++ b/test/engines/markdown_test.dart @@ -1,15 +1,17 @@ import 'dart:io'; import 'package:dart_express/dart_express.dart'; +import 'package:mockito/annotations.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; -class MockFileRepository extends Mock implements FileRepository {} +import 'markdown_test.mocks.dart'; final mockMarkdown = ''' # Hello, world '''; +@GenerateMocks([FileRepository]) void main() { test('HtmlEngine has the correct extension', () { expect(MarkdownEngine.ext, '.md'); @@ -18,9 +20,9 @@ void main() { test('MarkdownEngine handles reading a file correctly', () async { final filePath = './views/index.md'; dynamic error; - String rendered; + String? rendered; - dynamic callback(dynamic err, String string) { + dynamic callback(dynamic err, String? string) { error = err; rendered = string; } @@ -40,9 +42,9 @@ void main() { test('MarkdownEngine handles exceptions correctly', () async { final filePath = './views/index.md'; dynamic error; - String rendered; + String? rendered; - dynamic callback(dynamic err, String string) { + dynamic callback(dynamic err, String? string) { error = err; rendered = string; } diff --git a/test/engines/markdown_test.mocks.dart b/test/engines/markdown_test.mocks.dart new file mode 100644 index 0000000..20da311 --- /dev/null +++ b/test/engines/markdown_test.mocks.dart @@ -0,0 +1,31 @@ +// Mocks generated by Mockito 5.0.17 from annotations +// in dart_express/test/engines/markdown_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:dart_express/dart_express.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [FileRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileRepository extends _i1.Mock implements _i2.FileRepository { + MockFileRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future readAsString(Uri? uri) => + (super.noSuchMethod(Invocation.method(#readAsString, [uri]), + returnValue: Future.value('')) as _i3.Future); +} diff --git a/test/engines/mustache_test.dart b/test/engines/mustache_test.dart deleted file mode 100644 index a96b823..0000000 --- a/test/engines/mustache_test.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:io'; - -import 'package:dart_express/dart_express.dart'; -import 'package:test/test.dart'; -import 'package:mockito/mockito.dart'; - -class MockFileRepository extends Mock implements FileRepository {} - -final mockMustache = ''' - - - Mock HTML Title - - - {{#first_name}} -

Hello, {{first_name}}

- {{/first_name}} - {{^first_name}} -

Hello, World

- {{/first_name}} - - -'''; - -void main() { - test('MustacheEngine has the correct extension', () { - expect(MustacheEngine.ext, '.mustache'); - }); - - test('MustacheEngine handles reading a file correctly', () async { - final filePath = './views/index.mustache'; - dynamic error; - String rendered; - - dynamic callback(dynamic err, String string) { - error = err; - rendered = string; - } - - final mockFileRepository = MockFileRepository(); - - when(mockFileRepository.readAsString(Uri.file(filePath))) - .thenAnswer((_) async => mockMustache); - - await MustacheEngine.handler(filePath, {}, callback, mockFileRepository); - - expect(error, null); - expect(rendered, contains('')); - expect(rendered, contains('Hello, World')); - }); - - test('MustacheEngine handles passing locals into template', () async { - final filePath = './views/index.mustache'; - dynamic error; - String rendered; - - dynamic callback(dynamic err, String string) { - error = err; - rendered = string; - } - - final mockFileRepository = MockFileRepository(); - - when(mockFileRepository.readAsString(Uri.file(filePath))) - .thenAnswer((_) async => mockMustache); - - await MustacheEngine.handler( - filePath, {'first_name': 'Devin'}, callback, mockFileRepository); - - expect(error, null); - expect(rendered, contains('')); - expect(rendered, contains('Hello, Devin')); - }); - - test('MustacheEngine handles exceptions', () async { - final filePath = './views/index.mustache'; - dynamic error; - String rendered; - - dynamic callback(dynamic err, String string) { - error = err; - rendered = string; - } - - final mockFileRepository = MockFileRepository(); - - when(mockFileRepository.readAsString(Uri.file(filePath))) - .thenThrow(FileSystemException('Could not find file')); - - await MustacheEngine.handler( - filePath, {'first_name': 'Devin'}, callback, mockFileRepository); - - expect(error, isA()); - expect((error as FileSystemException).message, 'Could not find file'); - expect(rendered, null); - }); -}