diff --git a/packages/framework/lib/src/core/driver.dart b/packages/framework/lib/src/core/driver.dart index f1cf5773..a38f5d7c 100644 --- a/packages/framework/lib/src/core/driver.dart +++ b/packages/framework/lib/src/core/driver.dart @@ -4,6 +4,7 @@ import 'dart:io' show stderr, Cookie; import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel_route/angel_route.dart'; import 'package:combinator/combinator.dart'; +import 'package:logging/logging.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:tuple/tuple.dart'; import 'core.dart'; @@ -17,11 +18,12 @@ abstract class Driver< Server extends Stream, RequestContextType extends RequestContext, ResponseContextType extends ResponseContext> { - final Angel? app; + final Angel app; final bool useZone; bool _closed = false; - Server? _server; + late Server _server; StreamSubscription? _sub; + final log = Logger('Driver'); /// The function used to bind this instance to a server.. final Future Function(dynamic, int) serverGenerator; @@ -32,39 +34,44 @@ abstract class Driver< Uri get uri; /// The native server running this instance. - Server? get server => _server; + Server get server => _server; Future generateServer(address, int port) => serverGenerator(address, port); /// Starts, and returns the server. - Future startServer([address, int? port]) { + Future startServer([address, int port = 5000]) { var host = address ?? '127.0.0.1'; - return generateServer(host, port ?? 0).then((server) { + return generateServer(host, port).then((server) { _server = server; - return Future.wait(app!.startupHooks.map(app!.configure)).then((_) { - app!.optimizeForProduction(); + return Future.wait(app.startupHooks.map(app.configure)).then((_) { + app.optimizeForProduction(); _sub = server.listen((request) { var stream = createResponseStreamFromRawRequest(request); stream.listen((response) { // TODO: To be revisited handleRawRequest(request, response); - return; }); }); - return _server!; + return Future.value(_server); }); + }).catchError((error) { + log.severe("Failed to create server", error); + throw ArgumentError("[Driver]Failed to create server"); }); } /// Shuts down the underlying server. Future close() { - if (_closed) return Future.value(_server); + if (_closed) { + return Future.value(_server); + } _closed = true; + _sub?.cancel(); - return app!.close().then((_) => - Future.wait(app!.shutdownHooks.map(app!.configure)) - .then((_) => _server!)); + return app.close().then((_) => + Future.wait(app.shutdownHooks.map(app.configure)) + .then((_) => Future.value(_server))); } Future createRequestContext( @@ -102,7 +109,7 @@ abstract class Driver< Tuple4, ParseResult?, MiddlewarePipeline> resolveTuple() { - var r = app!.optimizedRouter; + var r = app.optimizedRouter; var resolved = r.resolveAbsolute(path, method: req.method!, strip: false); var pipeline = MiddlewarePipeline(resolved); @@ -116,8 +123,8 @@ abstract class Driver< } var cacheKey = req.method! + path!; - var tuple = app!.environment.isProduction - ? app!.handlerCache.putIfAbsent(cacheKey, resolveTuple) + var tuple = app.environment.isProduction + ? app.handlerCache.putIfAbsent(cacheKey, resolveTuple) : resolveTuple(); var line = tuple.item4 as MiddlewarePipeline; var it = MiddlewarePipelineIterator(line); @@ -134,7 +141,7 @@ abstract class Driver< ..registerSingleton?>(tuple.item3) ..registerSingleton(tuple.item3); - if (!app!.environment.isProduction && app!.logger != null) { + if (app.environment.isProduction && app.logger != null) { req.container!.registerSingleton(Stopwatch()..start()); } @@ -160,14 +167,14 @@ abstract class Driver< stackTrace: st, statusCode: 500, message: e?.toString() ?? '500 Internal Server Error'); - }, test: (e) => e is! AngelHttpException).catchError( + }, test: (e) => e is AngelHttpException).catchError( (ee, StackTrace st) { var e = ee as AngelHttpException; - if (app!.logger != null) { + if (app.logger != null) { var error = e.error ?? e; var trace = Trace.from(e.stackTrace ?? StackTrace.current).terse; - app!.logger!.severe(e.message ?? e.toString(), error, trace); + app.logger?.severe(e.message ?? e.toString(), error, trace); } return handleAngelHttpException( @@ -176,8 +183,8 @@ abstract class Driver< } else { var zoneSpec = ZoneSpecification( print: (self, parent, zone, line) { - if (app!.logger != null) { - app!.logger!.info(line); + if (app.logger != null) { + app.logger?.info(line); } else { parent.print(zone, line); } @@ -198,8 +205,8 @@ abstract class Driver< stackTrace: stackTrace, message: error.toString()); } - if (app!.logger != null) { - app!.logger!.severe(e.message ?? e.toString(), error, trace); + if (app.logger != null) { + app.logger?.severe(e.message ?? e.toString(), error, trace); } return handleAngelHttpException( @@ -209,8 +216,8 @@ abstract class Driver< closeResponse(response); // Ideally, we won't be in a position where an absolutely fatal error occurs, // but if so, we'll need to log it. - if (app!.logger != null) { - app!.logger!.severe( + if (app.logger != null) { + app.logger?.severe( 'Fatal error occurred when processing $uri.', e, trace); } else { stderr @@ -220,7 +227,6 @@ abstract class Driver< ..writeln(trace); } }); - return; }, ); @@ -243,7 +249,7 @@ abstract class Driver< } /// Handles an [AngelHttpException]. - Future? handleAngelHttpException( + Future handleAngelHttpException( AngelHttpException e, StackTrace st, RequestContext? req, @@ -253,12 +259,12 @@ abstract class Driver< {bool ignoreFinalizers = false}) { if (req == null || res == null) { try { - app!.logger?.severe(null, e, st); + app.logger?.severe(null, e, st); setStatusCode(response, 500); writeStringToResponse(response, '500 Internal Server Error'); closeResponse(response); } finally { - return null; + return Future.value(); } } @@ -269,8 +275,8 @@ abstract class Driver< } else { res.statusCode = e.statusCode; handleError = - Future.sync(() => app!.errorHandler(e, req, res)).then((result) { - return app!.executeHandler(result, req, res).then((_) => res.close()); + Future.sync(() => app.errorHandler(e, req, res)).then((result) { + return app.executeHandler(result, req, res).then((_) => res.close()); }); } @@ -283,11 +289,11 @@ abstract class Driver< ResponseContext res, {bool ignoreFinalizers = false}) { Future _cleanup(_) { - if (!app!.environment.isProduction && - app!.logger != null && + if (app.environment.isProduction && + app.logger != null && req.container!.has()) { var sw = req.container!.make(); - app!.logger!.info( + app.logger?.info( "${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)"); } return req.close(); @@ -297,7 +303,7 @@ abstract class Driver< Future finalizers = ignoreFinalizers == true ? Future.value() - : Future.forEach(app!.responseFinalizers, (dynamic f) => f(req, res)); + : Future.forEach(app.responseFinalizers, (dynamic f) => f(req, res)); return finalizers.then((_) { //if (res.isOpen) res.close(); @@ -358,13 +364,13 @@ abstract class Driver< MiddlewarePipelineIterator it, RequestContextType req, ResponseContextType res, - Angel? app) async { + Angel app) async { var broken = false; while (it.moveNext()) { var current = it.current.handlers.iterator; while (!broken && current.moveNext()) { - var result = await app!.executeHandler(current.current, req, res); + var result = await app.executeHandler(current.current, req, res); if (result != true) { broken = true; break; diff --git a/packages/framework/lib/src/core/routable.dart b/packages/framework/lib/src/core/routable.dart index b63a27a5..a52667ed 100644 --- a/packages/framework/lib/src/core/routable.dart +++ b/packages/framework/lib/src/core/routable.dart @@ -30,7 +30,7 @@ RequestHandler chain(Iterable handlers) { runPipeline = () => Future.sync(() => handler(req, res)); } else { var current = runPipeline; - runPipeline = () => current().then((result) => !res.isOpen + runPipeline = () => current().then((result) => res.isOpen ? Future.value(result) : req.app.executeHandler(handler, req, res)); } @@ -42,7 +42,7 @@ RequestHandler chain(Iterable handlers) { } /// A routable server that can handle dynamic requests. -class Routable extends Router { +class Routable extends Router { final Map _services = {}; final Map _serviceLookups = {}; final Map configuration = {}; @@ -92,10 +92,9 @@ class Routable extends Router { } @override - Route addRoute( - String? method, String? path, RequestHandler? handler, - {Iterable? middleware}) { - middleware ??= []; + Route addRoute( + String method, String path, RequestHandler handler, + {Iterable middleware = const Iterable.empty()}) { final handlers = []; // Merge @Middleware declaration, if any var reflector = _container?.reflector; @@ -108,10 +107,7 @@ class Routable extends Router { } final handlerSequence = []; - handlerSequence.addAll(middleware as Iterable< - FutureOr? Function( - RequestContext, ResponseContext)>? ?? - []); + handlerSequence.addAll(middleware); handlerSequence.addAll(handlers); return super.addRoute(method, path.toString(), handler, diff --git a/packages/framework/lib/src/http/angel_http.dart b/packages/framework/lib/src/http/angel_http.dart index 0276f190..dcb8ef34 100644 --- a/packages/framework/lib/src/http/angel_http.dart +++ b/packages/framework/lib/src/http/angel_http.dart @@ -19,15 +19,19 @@ final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); class AngelHttp extends Driver { @override - Uri get uri => server == null - ? Uri() - : Uri(scheme: 'http', host: server!.address.address, port: server!.port); + Uri get uri { + if (server == null) { + throw ArgumentError("[AngelHttp] Server instance not intialised"); + } + return Uri( + scheme: 'http', host: server!.address.address, port: server!.port); + } - AngelHttp._(Angel? app, + AngelHttp._(Angel app, Future Function(dynamic, int) serverGenerator, bool useZone) : super(app, serverGenerator, useZone: useZone); - factory AngelHttp(Angel? app, {bool useZone = true}) { + factory AngelHttp(Angel app, {bool useZone = true}) { return AngelHttp._(app, HttpServer.bind, useZone); } @@ -59,12 +63,18 @@ class AngelHttp extends Driver server; + HttpServer get httpServer { + if (server == null) { + throw ArgumentError("[AngelHttp] Server instance not initialised"); + } + return server!; + } Future handleRequest(HttpRequest request) => handleRawRequest(request, request.response); @@ -75,7 +85,6 @@ class AngelHttp extends Driver close() async { - await server?.close(); return await super.close(); } @@ -87,7 +96,7 @@ class AngelHttp extends Driver.value(context); // return Future.value( diff --git a/packages/framework/test/controller_test.dart b/packages/framework/test/controller_test.dart index a75b998f..23402a7b 100644 --- a/packages/framework/test/controller_test.dart +++ b/packages/framework/test/controller_test.dart @@ -70,7 +70,7 @@ bool bar(RequestContext req, ResponseContext res) { } main() { - Angel? app; + late Angel app; late TodoController todoController; late NoExposeController noExposeCtrl; late HttpServer server; @@ -79,30 +79,30 @@ main() { setUp(() async { app = Angel(reflector: MirrorsReflector()); - app!.get( + app.get( "/redirect", (req, res) async => res.redirectToAction("TodoController@foo", {"foo": "world"})); // Register as a singleton, just for the purpose of this test - if (!app!.container!.has()) { - app!.container!.registerSingleton(todoController = TodoController()); + if (!app.container!.has()) { + app.container!.registerSingleton(todoController = TodoController()); } // Using mountController(); - await app!.mountController(); + await app.mountController(); - noExposeCtrl = await app!.mountController(); + noExposeCtrl = await app.mountController(); // Place controller in group. The applyRoutes() call, however, is async. // Until https://github.com/angel-dart/route/issues/28 is closed, // this will need to be done by manually mounting the router. var subRouter = Router(); - await todoController.applyRoutes(subRouter, app!.container!.reflector); - app!.mount('/ctrl_group', subRouter); + await todoController.applyRoutes(subRouter, app.container!.reflector); + app.mount('/ctrl_group', subRouter); - print(app!.controllers); - app!.dumpTree(); + print(app.controllers); + app.dumpTree(); server = await AngelHttp(app).startServer(); url = 'http://${server.address.address}:${server.port}'; @@ -110,7 +110,6 @@ main() { tearDown(() async { await server.close(force: true); - app = null; url = null; }); diff --git a/packages/framework/test/di_test.dart b/packages/framework/test/di_test.dart index 51e9999e..c8c12e44 100644 --- a/packages/framework/test/di_test.dart +++ b/packages/framework/test/di_test.dart @@ -15,8 +15,8 @@ final String TEXT = "make your bed"; final String OVER = "never"; main() { - Angel? app; - http.Client? client; + late Angel app; + late http.Client client; late HttpServer server; String? url; @@ -25,31 +25,29 @@ main() { client = http.Client(); // Inject some todos - app!.container!.registerSingleton(Todo(text: TEXT, over: OVER)); - app!.container!.registerFactory>((container) async { + app.container!.registerSingleton(Todo(text: TEXT, over: OVER)); + app.container!.registerFactory>((container) async { var req = container.make()!; var text = await utf8.decoder.bind(req.body!).join(); return Foo(text); }); - app!.get("/errands", ioc((Todo singleton) => singleton)); - app!.get( + app.get("/errands", ioc((Todo singleton) => singleton)); + app.get( "/errands3", ioc(({required Errand singleton, Todo? foo, RequestContext? req}) => singleton.text)); - app!.post('/async', ioc((Foo foo) => {'baz': foo.bar})); - await app!.configure(SingletonController().configureServer); - await app!.configure(ErrandController().configureServer); + app.post('/async', ioc((Foo foo) => {'baz': foo.bar})); + await app.configure(SingletonController().configureServer); + await app.configure(ErrandController().configureServer); server = await AngelHttp(app).startServer(); url = "http://${server.address.host}:${server.port}"; }); tearDown(() async { - app = null; url = null; - client!.close(); - client = null; + client.close(); await server.close(force: true); }); @@ -71,33 +69,33 @@ main() { }); test("singleton in route", () async { - validateTodoSingleton(await client!.get(Uri.parse("$url/errands"))); + validateTodoSingleton(await client.get(Uri.parse("$url/errands"))); }); test("singleton in controller", () async { - validateTodoSingleton(await client!.get(Uri.parse("$url/errands2"))); + validateTodoSingleton(await client.get(Uri.parse("$url/errands2"))); }); test("make in route", () async { - var response = await client!.get(Uri.parse("$url/errands3")); + var response = await client.get(Uri.parse("$url/errands3")); var text = await json.decode(response.body) as String?; expect(text, equals(TEXT)); }); test("make in controller", () async { - var response = await client!.get(Uri.parse("$url/errands4")); + var response = await client.get(Uri.parse("$url/errands4")); var text = await json.decode(response.body) as String?; expect(text, equals(TEXT)); }); test('resolve from future in controller', () async { var response = - await client!.post(Uri.parse('$url/errands4/async'), body: 'hey'); + await client.post(Uri.parse('$url/errands4/async'), body: 'hey'); expect(response.body, json.encode({'bar': 'hey'})); }); test('resolve from future in route', () async { - var response = await client!.post(Uri.parse('$url/async'), body: 'yes'); + var response = await client.post(Uri.parse('$url/async'), body: 'yes'); expect(response.body, json.encode({'baz': 'yes'})); }); } diff --git a/packages/framework/test/general_test.dart b/packages/framework/test/general_test.dart index b279c6c3..b21c02c7 100644 --- a/packages/framework/test/general_test.dart +++ b/packages/framework/test/general_test.dart @@ -7,10 +7,10 @@ import 'package:http/http.dart' as http; import 'package:test/test.dart'; main() { - Angel? app; - http.Client? client; + late Angel app; + late http.Client client; late HttpServer server; - String? url; + late String url; setUp(() async { app = Angel(reflector: MirrorsReflector()) @@ -23,15 +23,12 @@ main() { }); tearDown(() async { - app = null; - url = null; - client!.close(); - client = null; + client.close(); await server.close(force: true); }); test("allow override of method", () async { - var response = await client!.get(Uri.parse('$url/foo'), + var response = await client.get(Uri.parse('$url/foo'), headers: {'X-HTTP-Method-Override': 'POST'}); print('Response: ${response.body}'); expect(json.decode(response.body), equals({'hello': 'world'})); diff --git a/packages/framework/test/hooked_test.dart b/packages/framework/test/hooked_test.dart index 7b01ac05..635fb9cc 100644 --- a/packages/framework/test/hooked_test.dart +++ b/packages/framework/test/hooked_test.dart @@ -13,25 +13,26 @@ main() { 'Content-Type': 'application/json' }; - Angel? app; + late Angel app; late HttpServer server; - String? url; - http.Client? client; - HookedService? todoService; + late String url; + late http.Client client; + late HookedService todoService; setUp(() async { app = Angel(reflector: MirrorsReflector()); client = http.Client(); - app!.use('/todos', MapService()); - app!.use('/books', BookService()); + app.use('/todos', MapService()); + app.use('/books', BookService()); - todoService = app!.findHookedService('todos'); + todoService = app.findHookedService('todos') + as HookedService; - todoService!.beforeAllStream().listen((e) { + todoService.beforeAllStream().listen((e) { print('Fired ${e.eventName}! Data: ${e.data}; Params: ${e.params}'); }); - app!.errorHandler = (e, req, res) { + app.errorHandler = (e, req, res) { throw e.error as Object; }; @@ -41,31 +42,27 @@ main() { tearDown(() async { await server.close(force: true); - app = null; - url = null; - client!.close(); - client = null; - todoService = null; + client.close(); }); test("listen before and after", () async { int count = 0; todoService - ?..beforeIndexed.listen((_) { + ..beforeIndexed.listen((_) { count++; }) ..afterIndexed.listen((_) { count++; }); - var response = await client!.get(Uri.parse("$url/todos")); + var response = await client.get(Uri.parse("$url/todos")); print(response.body); expect(count, equals(2)); }); test("cancel before", () async { - todoService!.beforeCreated + todoService.beforeCreated ..listen((HookedServiceEvent event) { event.cancel({"hello": "hooked world"}); }) @@ -73,7 +70,7 @@ main() { event.cancel({"this_hook": "should never run"}); }); - var response = await client!.post(Uri.parse("$url/todos"), + var response = await client.post(Uri.parse("$url/todos"), body: json.encode({"arbitrary": "data"}), headers: headers as Map); print(response.body); @@ -82,7 +79,7 @@ main() { }); test("cancel after", () async { - todoService!.afterIndexed + todoService.afterIndexed ..listen((HookedServiceEvent event) async { // Hooks can be Futures ;) event.cancel([ @@ -93,20 +90,20 @@ main() { event.cancel({"this_hook": "should never run either"}); }); - var response = await client!.get(Uri.parse("$url/todos")); + var response = await client.get(Uri.parse("$url/todos")); print(response.body); var result = json.decode(response.body) as List; expect(result[0]["angel"], equals("framework")); }); test('asStream() fires', () async { - var stream = todoService!.afterCreated.asStream(); - await todoService!.create({'angel': 'framework'}); + var stream = todoService.afterCreated.asStream(); + await todoService.create({'angel': 'framework'}); expect(await stream.first.then((e) => e.result['angel']), 'framework'); }); test('metadata', () async { - final service = HookedService(IncrementService())..addHooks(app!); + final service = HookedService(IncrementService())..addHooks(app); expect(service.inner, isNot(const IsInstanceOf())); IncrementService.TIMES = 0; await service.index(); @@ -114,15 +111,15 @@ main() { }); test('inject request + response', () async { - HookedService books = app!.findService('books') + HookedService books = app.findService('books') as HookedService>; books.beforeIndexed.listen((e) { expect([e.request, e.response], everyElement(isNotNull)); - print('Indexing books at path: ${e.request!.path}'); + print('Indexing books at path: ${e.request?.path}'); }); - var response = await client!.get(Uri.parse('$url/books')); + var response = await client.get(Uri.parse('$url/books')); print(response.body); var result = json.decode(response.body); @@ -138,8 +135,8 @@ main() { var type = e.isBefore ? 'before' : 'after'; print('Params to $type ${e.eventName}: ${e.params}'); expect(e.params, isMap); - expect(e.params!.keys, contains('provider')); - expect(e.params!['provider'], const IsInstanceOf()); + expect(e.params?.keys, contains('provider')); + expect(e.params?['provider'], const IsInstanceOf()); } svc diff --git a/packages/framework/test/routing_test.dart b/packages/framework/test/routing_test.dart index 97469472..36426968 100644 --- a/packages/framework/test/routing_test.dart +++ b/packages/framework/test/routing_test.dart @@ -35,11 +35,11 @@ bool interceptService(RequestContext req, ResponseContext res) { } main() { - Angel? app; - Angel? nested; - Angel? todos; - String? url; - http.Client? client; + late Angel app; + late Angel nested; + late Angel todos; + late String url; + late http.Client client; setUp(() async { app = Angel(reflector: MirrorsReflector()); @@ -47,7 +47,7 @@ main() { todos = Angel(reflector: MirrorsReflector()); [app, nested, todos].forEach((Angel? app) { - app!.logger = Logger('routing_test') + app?.logger = Logger('routing_test') ..onRecord.listen((rec) { if (rec.error != null) { stdout @@ -58,44 +58,44 @@ main() { }); }); - todos!.get('/action/:action', (req, res) => res.json(req.params)); + todos.get('/action/:action', (req, res) => res.json(req.params)); late Route ted; - ted = nested!.post('/ted/:route', (RequestContext req, res) { + ted = nested.post('/ted/:route', (RequestContext req, res) { print('Params: ${req.params}'); print('Path: ${ted.path}, uri: ${req.path}'); print('matcher: ${ted.parser}'); return req.params; }); - app!.mount('/nes', nested!); - app!.get('/meta', testMiddlewareMetadata); - app!.get('/intercepted', (req, res) => 'This should not be shown', + app.mount('/nes', nested); + app.get('/meta', testMiddlewareMetadata); + app.get('/intercepted', (req, res) => 'This should not be shown', middleware: [interceptor]); - app!.get('/hello', (req, res) => 'world'); - app!.get('/name/:first/last/:last', (req, res) => req.params); - app!.post( + app.get('/hello', (req, res) => 'world'); + app.get('/name/:first/last/:last', (req, res) => req.params); + app.post( '/lambda', (RequestContext req, res) => req.parseBody().then((_) => req.bodyAsMap)); - app!.mount('/todos/:id', todos!); - app! + app.mount('/todos/:id', todos); + app .get('/greet/:name', (RequestContext req, res) async => "Hello ${req.params['name']}") .name = 'Named routes'; - app!.get('/named', (req, ResponseContext res) async { + app.get('/named', (req, ResponseContext res) async { await res.redirectTo('Named routes', {'name': 'tests'}); }); - app!.get('/log', (RequestContext req, res) async { + app.get('/log', (RequestContext req, res) async { print("Query: ${req.queryParameters}"); return "Logged"; }); - app!.get('/method', (req, res) => 'Only GET'); - app!.post('/method', (req, res) => 'Only POST'); + app.get('/method', (req, res) => 'Only GET'); + app.post('/method', (req, res) => 'Only POST'); - app!.use('/query', QueryService()); + app.use('/query', QueryService()); RequestHandler write(String message) { return (req, res) { @@ -104,10 +104,10 @@ main() { }; } - app!.chain([write('a')]).chain([write('b'), write('c')]).get( + app.chain([write('a')]).chain([write('b'), write('c')]).get( '/chained', (req, res) => res.close()); - app!.fallback((req, res) => 'MJ'); + app.fallback((req, res) => 'MJ'); //app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true); @@ -117,22 +117,17 @@ main() { }); tearDown(() async { - await app!.close(); - app = null; - nested = null; - todos = null; - client!.close(); - client = null; - url = null; + await app.close(); + client.close(); }); test('Can match basic url', () async { - var response = await client!.get(Uri.parse("$url/hello")); + var response = await client.get(Uri.parse("$url/hello")); expect(response.body, equals('"world"')); }); test('Can match url with multiple parameters', () async { - var response = await client!.get(Uri.parse('$url/name/HELLO/last/WORLD')); + var response = await client.get(Uri.parse('$url/name/HELLO/last/WORLD')); print('Response: ${response.body}'); var json_ = json.decode(response.body); expect(json_, const IsInstanceOf()); @@ -141,18 +136,18 @@ main() { }); test('Chained routes', () async { - var response = await client!.get(Uri.parse("$url/chained")); + var response = await client.get(Uri.parse("$url/chained")); expect(response.body, equals('abc')); }); test('Can nest another Angel instance', () async { - var response = await client!.post(Uri.parse('$url/nes/ted/foo')); + var response = await client.post(Uri.parse('$url/nes/ted/foo')); var json_ = json.decode(response.body); expect(json_['route'], equals('foo')); }); test('Can parse parameters from a nested Angel instance', () async { - var response = await client!.get(Uri.parse('$url/todos/1337/action/test')); + var response = await client.get(Uri.parse('$url/todos/1337/action/test')); var json_ = json.decode(response.body); print('JSON: $json_'); expect(json_['id'], equals('1337')); @@ -160,32 +155,32 @@ main() { }); test('Can add and use named middleware', () async { - var response = await client!.get(Uri.parse('$url/intercepted')); + var response = await client.get(Uri.parse('$url/intercepted')); expect(response.body, equals('Middleware')); }); test('Middleware via metadata', () async { // Metadata - var response = await client!.get(Uri.parse('$url/meta')); + var response = await client.get(Uri.parse('$url/meta')); expect(response.body, equals('Middleware')); }); test('Can serialize function result as JSON', () async { Map headers = {'Content-Type': 'application/json'}; String postData = json.encode({'it': 'works'}); - var response = await client!.post(Uri.parse("$url/lambda"), + var response = await client.post(Uri.parse("$url/lambda"), headers: headers as Map, body: postData); print('Response: ${response.body}'); expect(json.decode(response.body)['it'], equals('works')); }); test('Fallback routes', () async { - var response = await client!.get(Uri.parse('$url/my_favorite_artist')); + var response = await client.get(Uri.parse('$url/my_favorite_artist')); expect(response.body, equals('"MJ"')); }); test('Can name routes', () { - Route foo = app!.get('/framework/:id', null)..name = 'frm'; + Route foo = app.get('/framework/:id', null)..name = 'frm'; print('Foo: $foo'); String uri = foo.makeUri({'id': 'angel'}); print(uri); @@ -193,32 +188,32 @@ main() { }); test('Redirect to named routes', () async { - var response = await client!.get(Uri.parse('$url/named')); + var response = await client.get(Uri.parse('$url/named')); print(response.body); expect(json.decode(response.body), equals('Hello tests')); }); test('Match routes, even with query params', () async { - var response = await client! + var response = await client .get(Uri.parse("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo")); print(response.body); expect(json.decode(response.body), equals('Logged')); - response = await client!.get(Uri.parse("$url/query/foo?bar=baz")); + response = await client.get(Uri.parse("$url/query/foo?bar=baz")); print(response.body); expect(response.body, equals("Service with Middleware")); }); test('only match route with matching method', () async { - var response = await client!.get(Uri.parse("$url/method")); + var response = await client.get(Uri.parse("$url/method")); print(response.body); expect(response.body, '"Only GET"'); - response = await client!.post(Uri.parse("$url/method")); + response = await client.post(Uri.parse("$url/method")); print(response.body); expect(response.body, '"Only POST"'); - response = await client!.patch(Uri.parse("$url/method")); + response = await client.patch(Uri.parse("$url/method")); print(response.body); expect(response.body, '"MJ"'); }); diff --git a/packages/framework/test/serialize_test.dart b/packages/framework/test/serialize_test.dart index d9d20ce4..d15ea31c 100644 --- a/packages/framework/test/serialize_test.dart +++ b/packages/framework/test/serialize_test.dart @@ -8,10 +8,10 @@ import 'package:http_parser/http_parser.dart'; import 'package:test/test.dart'; main() { - Angel? app; - http.Client? client; + late Angel app; + late http.Client client; late HttpServer server; - String? url; + late String url; setUp(() async { app = Angel(reflector: MirrorsReflector()) @@ -27,19 +27,16 @@ main() { }); tearDown(() async { - app = null; - url = null; - client!.close(); - client = null; + client.close(); await server.close(force: true); }); test("correct content-type", () async { - var response = await client!.get(Uri.parse('$url/foo')); + var response = await client.get(Uri.parse('$url/foo')); print('Response: ${response.body}'); expect(response.headers['content-type'], contains('application/json')); - response = await client!.get(Uri.parse('$url/bar')); + response = await client.get(Uri.parse('$url/bar')); print('Response: ${response.body}'); expect(response.headers['content-type'], contains('text/html')); }); diff --git a/packages/framework/test/services_test.dart b/packages/framework/test/services_test.dart index f5a0717e..05bd8c51 100644 --- a/packages/framework/test/services_test.dart +++ b/packages/framework/test/services_test.dart @@ -18,10 +18,10 @@ main() { 'Accept': 'application/json', 'Content-Type': 'application/json' }; - Angel? app; + late Angel app; late MapService service; - String? url; - http.Client? client; + late String url; + late http.Client client; setUp(() async { app = Angel(reflector: MirrorsReflector()) @@ -37,16 +37,13 @@ main() { }); tearDown(() async { - await app!.close(); - app = null; - url = null; - client!.close(); - client = null; + await app.close(); + client.close(); }); group('memory', () { test('can index an empty service', () async { - var response = await client!.get(Uri.parse("$url/todos/")); + var response = await client.get(Uri.parse("$url/todos/")); print(response.body); expect(response.body, equals('[]')); print(response.body); @@ -55,7 +52,7 @@ main() { test('can create data', () async { String postData = json.encode({'text': 'Hello, world!'}); - var response = await client!.post(Uri.parse("$url/todos"), + var response = await client.post(Uri.parse("$url/todos"), headers: headers as Map, body: postData); expect(response.statusCode, 201); var jsons = json.decode(response.body); @@ -65,9 +62,9 @@ main() { test('can fetch data', () async { String postData = json.encode({'text': 'Hello, world!'}); - await client!.post(Uri.parse("$url/todos"), + await client.post(Uri.parse("$url/todos"), headers: headers as Map, body: postData); - var response = await client!.get(Uri.parse("$url/todos/0")); + var response = await client.get(Uri.parse("$url/todos/0")); expect(response.statusCode, 200); var jsons = json.decode(response.body); print(jsons); @@ -76,12 +73,12 @@ main() { test('can modify data', () async { String postData = json.encode({'text': 'Hello, world!'}); - await client!.post(Uri.parse("$url/todos"), + await client.post(Uri.parse("$url/todos"), headers: headers as Map, body: postData); postData = json.encode({'text': 'modified'}); - var response = await client! - .patch(Uri.parse("$url/todos/0"), headers: headers, body: postData); + var response = await client.patch(Uri.parse("$url/todos/0"), + headers: headers, body: postData); expect(response.statusCode, 200); var jsons = json.decode(response.body); print(jsons); @@ -90,12 +87,12 @@ main() { test('can overwrite data', () async { String postData = json.encode({'text': 'Hello, world!'}); - await client!.post(Uri.parse("$url/todos"), + await client.post(Uri.parse("$url/todos"), headers: headers as Map, body: postData); postData = json.encode({'over': 'write'}); - var response = await client! - .post(Uri.parse("$url/todos/0"), headers: headers, body: postData); + var response = await client.post(Uri.parse("$url/todos/0"), + headers: headers, body: postData); expect(response.statusCode, 200); var jsons = json.decode(response.body); print(jsons); @@ -116,12 +113,12 @@ main() { test('can delete data', () async { String postData = json.encode({'text': 'Hello, world!'}); - var created = await client! + var created = await client .post(Uri.parse("$url/todos"), headers: headers as Map, body: postData) .then((r) => json.decode(r.body)); var response = - await client!.delete(Uri.parse("$url/todos/${created['id']}")); + await client.delete(Uri.parse("$url/todos/${created['id']}")); expect(response.statusCode, 200); var json_ = json.decode(response.body); print(json_); @@ -129,7 +126,7 @@ main() { }); test('cannot remove all unless explicitly set', () async { - var response = await client!.delete(Uri.parse('$url/todos/null')); + var response = await client.delete(Uri.parse('$url/todos/null')); expect(response.statusCode, 403); }); }); diff --git a/packages/route/example/main.dart b/packages/route/example/main.dart index b0352ac2..891dd750 100644 --- a/packages/route/example/main.dart +++ b/packages/route/example/main.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:angel_route/angel_route.dart'; -main() { +void main() { final router = Router(); router.get('/whois/~:user', () {}); @@ -12,7 +12,7 @@ main() { router.get('/ordinal/int:n([0-9]+)st', () {}); print(router.resolveAbsolute('/whois/~thosakwe').first.allParams); - print(router.resolveAbsolute('/wild_thornberrys').first.route!.path); + print(router.resolveAbsolute('/wild_thornberrys').first.route.path); print(router.resolveAbsolute('/ordinal/1st').first.allParams); router.get('/users', () {}); diff --git a/packages/route/lib/browser.dart b/packages/route/lib/browser.dart index eb54fe45..dae09d2f 100644 --- a/packages/route/lib/browser.dart +++ b/packages/route/lib/browser.dart @@ -11,10 +11,10 @@ final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); /// A variation of the [Router] support both hash routing and push state. abstract class BrowserRouter extends Router { /// Fires whenever the active route changes. Fires `null` if none is selected (404). - Stream?> get onResolve; + Stream> get onResolve; /// Fires whenever the active route changes. Fires `null` if none is selected (404). - Stream?> get onRoute; + Stream> get onRoute; /// Set `hash` to true to use hash routing instead of push state. /// `listen` as `true` will call `listen` after initialization. @@ -26,7 +26,7 @@ abstract class BrowserRouter extends Router { BrowserRouter._() : super(); - void _goTo(String? path); + void _goTo(String path); /// Navigates to the path generated by calling /// [navigate] with the given [linkParams]. @@ -41,26 +41,26 @@ abstract class BrowserRouter extends Router { void listen(); /// Identical to [all]. - Route on(String path, T handler, {Iterable? middleware}); + Route on(String path, T handler, {Iterable middleware}); } abstract class _BrowserRouterImpl extends Router implements BrowserRouter { bool _listening = false; Route? _current; - StreamController?> _onResolve = - StreamController?>(); - StreamController?> _onRoute = StreamController?>(); + final StreamController> _onResolve = + StreamController>(); + final StreamController> _onRoute = StreamController>(); Route? get currentRoute => _current; @override - Stream?> get onResolve => _onResolve.stream; + Stream> get onResolve => _onResolve.stream; @override - Stream?> get onRoute => _onRoute.stream; + Stream> get onRoute => _onRoute.stream; - _BrowserRouterImpl({bool? listen}) : super() { + _BrowserRouterImpl({bool listen = false}) : super() { if (listen != false) this.listen(); prepareAnchors(); } @@ -68,7 +68,9 @@ abstract class _BrowserRouterImpl extends Router @override void go(Iterable linkParams) => _goTo(navigate(linkParams)); - Route on(String path, T handler, {Iterable? middleware}) => + @override + Route on(String path, T handler, + {Iterable middleware = const Iterable.empty()}) => all(path, handler, middleware: middleware); void prepareAnchors() { @@ -76,14 +78,14 @@ abstract class _BrowserRouterImpl extends Router .querySelectorAll('a') .cast(); //:not([dynamic])'); - for (final AnchorElement $a in anchors) { + for (final $a in anchors) { if ($a.attributes.containsKey('href') && - !$a.attributes.containsKey('download') && - !$a.attributes.containsKey('target') && + $a.attributes.containsKey('download') && + $a.attributes.containsKey('target') && $a.attributes['rel'] != 'external') { $a.onClick.listen((e) { e.preventDefault(); - _goTo($a.attributes['href']); + _goTo($a.attributes['href']!); //go($a.attributes['href'].split('/').where((str) => str.isNotEmpty)); }); } @@ -110,32 +112,36 @@ class _HashRouter extends _BrowserRouterImpl { } @override - void _goTo(String? uri) { + void _goTo(String uri) { window.location.hash = '#$uri'; } void handleHash([_]) { final path = window.location.hash.replaceAll(_hash, ''); - Iterable> allResolved = resolveAbsolute(path); + var allResolved = resolveAbsolute(path); - final resolved = allResolved.isEmpty ? null : allResolved.first; - - if (resolved == null) { - _onResolve.add(null); - _onRoute.add(_current = null); - } else if (resolved != null && resolved.route != _current) { - _onResolve.add(resolved); - _onRoute.add(_current = resolved.route); + if (allResolved.isEmpty) { + // TODO: Need fixing + //_onResolve.add(null); + //_onRoute.add(_current = null); + _current = null; + } else { + var resolved = allResolved.first; + if (resolved.route != _current) { + _onResolve.add(resolved); + _onRoute.add(_current = resolved.route); + } } } void handlePath(String path) { - final RoutingResult resolved = resolveAbsolute(path).first; + final resolved = resolveAbsolute(path).first; - if (resolved == null) { - _onResolve.add(null); - _onRoute.add(_current = null); - } else if (resolved != null && resolved.route != _current) { + //if (resolved == null) { + // _onResolve.add(null); + // _onRoute.add(_current = null); + //} else + if (resolved.route != _current) { _onResolve.add(resolved); _onRoute.add(_current = resolved.route); } @@ -149,12 +155,12 @@ class _HashRouter extends _BrowserRouterImpl { } class _PushStateRouter extends _BrowserRouterImpl { - String? _basePath; + late String _basePath; - _PushStateRouter({required bool listen, Route? root}) : super(listen: listen) { + _PushStateRouter({required bool listen}) : super(listen: listen) { var $base = window.document.querySelector('base[href]') as BaseElement; - if ($base?.href?.isNotEmpty != true) { + if ($base.href.isNotEmpty != true) { throw StateError( 'You must have a element present in your document to run the push state router.'); } @@ -163,42 +169,48 @@ class _PushStateRouter extends _BrowserRouterImpl { } @override - void _goTo(String? uri) { - final RoutingResult resolved = resolveAbsolute(uri).first; + void _goTo(String uri) { + final resolved = resolveAbsolute(uri).first; var relativeUri = uri; - if (_basePath?.isNotEmpty == true) { - relativeUri = p.join(_basePath!, uri!.replaceAll(_straySlashes, '')); + if (_basePath.isNotEmpty) { + relativeUri = p.join(_basePath, uri.replaceAll(_straySlashes, '')); } - if (resolved == null) { - _onResolve.add(null); - _onRoute.add(_current = null); - } else { - final route = resolved.route!; - window.history.pushState({'path': route.path, 'params': {}}, - route.name ?? route.path, relativeUri); - _onResolve.add(resolved); - _onRoute.add(_current = route); + //if (resolved == null) { + // _onResolve.add(null); + // _onRoute.add(_current = null); + //} else { + final route = resolved.route; + var thisPath = route.name; + if (thisPath.isEmpty) { + thisPath = route.path; } + window.history + .pushState({'path': route.path, 'params': {}}, thisPath, relativeUri); + _onResolve.add(resolved); + _onRoute.add(_current = route); + //} } void handleState(state) { if (state is Map && state.containsKey('path')) { var path = state['path'].toString(); - final RoutingResult resolved = resolveAbsolute(path).first; + final resolved = resolveAbsolute(path).first; - if (resolved != null && resolved.route != _current) { + if (resolved.route != _current) { //properties.addAll(state['properties'] ?? {}); _onResolve.add(resolved); _onRoute.add(_current = resolved.route); } else { - _onResolve.add(null); - _onRoute.add(_current = null); + //_onResolve.add(null); + //_onRoute.add(_current = null); + _current = null; } } else { - _onResolve.add(null); - _onRoute.add(_current = null); + //_onResolve.add(null); + //_onRoute.add(_current = null); + _current = null; } } diff --git a/packages/route/lib/src/grammar.dart b/packages/route/lib/src/grammar.dart index 3f970ba7..9d7df15b 100644 --- a/packages/route/lib/src/grammar.dart +++ b/packages/route/lib/src/grammar.dart @@ -40,7 +40,9 @@ class RouteGrammar { } } - var s = ParameterSegment(match[2], rgx); + // TODO: relook at this later + var m2 = match[2] ?? ''; + var s = ParameterSegment(m2, rgx); return r.value![1] == true ? OptionalSegment(s) : s; }); @@ -95,12 +97,12 @@ class RouteDefinition { RouteDefinition(this.segments); - Parser? compile() { - Parser? out; + Parser? compile() { + Parser? out; - for (int i = 0; i < segments.length; i++) { + for (var i = 0; i < segments.length; i++) { var s = segments[i]; - bool isLast = i == segments.length - 1; + var isLast = i == segments.length - 1; if (out == null) { out = s.compile(isLast); } else { @@ -116,7 +118,7 @@ class RouteDefinition { abstract class RouteSegment { Parser compile(bool isLast); - Parser compileNext(Parser p, bool isLast); + Parser compileNext(Parser p, bool isLast); } class SlashSegment implements RouteSegment { @@ -182,8 +184,15 @@ class WildcardSegment extends RouteSegment { @override Parser compile(bool isLast) { - return match(_compile(isLast)) - .map((r) => RouteResult({}, tail: r.scanner.lastMatch![1])); + return match(_compile(isLast)).map((r) { + var result = r.scanner.lastMatch; + if (result != null) { + //return RouteResult({}, tail: r.scanner.lastMatch![1]) + return RouteResult({}, tail: result[1]); + } else { + return RouteResult({}); + } + }); } @override @@ -214,31 +223,53 @@ class OptionalSegment extends ParameterSegment { } @override - Parser compileNext(Parser p, bool isLast) { + Parser compileNext(Parser p, bool isLast) { return p.then(_compile().opt()).map((r) { - if (r.value![1] == null) return r.value![0] as RouteResult?; - return (r.value![0] as RouteResult) - ..addAll({name: Uri.decodeComponent(r.value![1] as String)}); + // Return an empty RouteResult if null + if (r.value == null) { + return RouteResult({}); + } + + var v = r.value!; + + if (v[1] == null) { + return v[0] as RouteResult; + } + return (v[0] as RouteResult) + ..addAll({name: Uri.decodeComponent(v as String)}); }); } } class ParameterSegment extends RouteSegment { - final String? name; + final String name; final RegExp? regExp; ParameterSegment(this.name, this.regExp); @override String toString() { - if (regExp != null) return 'Param: $name (${regExp!.pattern})'; + if (regExp != null) { + return 'Param: $name (${regExp?.pattern})'; + } return 'Param: $name'; } - Parser _compile() { - return regExp != null - ? match(regExp!).value((r) => r.scanner.lastMatch![1]) - : RouteGrammar.notSlash; + Parser _compile() { + if (regExp != null) { + return match(regExp!).value((r) { + var result = r.scanner.lastMatch; + if (result != null) { + // TODO: Invalid method + //return r.scanner.lastMatch![1]; + return result.toString(); + } else { + return ''; + } + }); + } else { + return RouteGrammar.notSlash; + } } @override @@ -248,7 +279,7 @@ class ParameterSegment extends RouteSegment { } @override - Parser compileNext(Parser p, bool isLast) { + Parser compileNext(Parser p, bool isLast) { return p.then(_compile()).map((r) { return (r.value![0] as RouteResult) ..addAll({name: Uri.decodeComponent(r.value![1] as String)}); diff --git a/packages/route/lib/src/route.dart b/packages/route/lib/src/route.dart index dd943015..aa5fb17e 100644 --- a/packages/route/lib/src/route.dart +++ b/packages/route/lib/src/route.dart @@ -2,20 +2,27 @@ part of angel_route.src.router; /// Represents a virtual location within an application. class Route { - final String? method; - final String path; - final List? handlers; + String method; + String path; final Map> _cache = {}; - final RouteDefinition? _routeDefinition; - String? name; - Parser? _parser; + String name = ''; + Parser? _parser; + late RouteDefinition _routeDefinition; + late List handlers; - Route(this.path, {required this.method, required this.handlers}) - : _routeDefinition = RouteGrammar.routeDefinition - .parse(SpanScanner(path.replaceAll(_straySlashes, '')))! - .value { - if (_routeDefinition?.segments.isNotEmpty != true) { - _parser = match('').map((r) => RouteResult({})); + Route(this.path, {required this.method, required this.handlers}) { + var result = RouteGrammar.routeDefinition + .parse(SpanScanner(path.replaceAll(_straySlashes, ''))); + + if (result.value != null) { + //throw ArgumentError('[Route] Failed to create route for $path'); + _routeDefinition = result.value!; + + if (_routeDefinition.segments.isEmpty) { + _parser = match('').map((r) => RouteResult({})); + } + } else { + //print('[Route] Failed to create route for $path'); } } @@ -26,7 +33,9 @@ class Route { method: b.method, handlers: b.handlers); } - Parser? get parser => _parser ??= _routeDefinition!.compile(); + //List get handlers => _handlers; + + Parser? get parser => _parser ??= _routeDefinition.compile(); @override String toString() { @@ -40,9 +49,9 @@ class Route { String makeUri(Map params) { var b = StringBuffer(); - int i = 0; + var i = 0; - for (var seg in _routeDefinition!.segments) { + for (var seg in _routeDefinition.segments) { if (i++ > 0) b.write('/'); if (seg is ConstantSegment) { b.write(seg.text); @@ -50,7 +59,7 @@ class Route { if (!params.containsKey(seg.name)) { throw ArgumentError('Missing parameter "${seg.name}".'); } - b.write(params[seg.name!]); + b.write(params[seg.name]); } } @@ -61,7 +70,7 @@ class Route { /// The result of matching an individual route. class RouteResult { /// The parsed route parameters. - final Map params; + final Map params; /// Optional. An explicit "tail" value to set. String? get tail => _tail; @@ -73,7 +82,7 @@ class RouteResult { void _setTail(String? v) => _tail ??= v; /// Adds parameters. - void addAll(Map map) { + void addAll(Map map) { params.addAll(map); } } diff --git a/packages/route/lib/src/router.dart b/packages/route/lib/src/router.dart index 986844d4..24a8404c 100644 --- a/packages/route/lib/src/router.dart +++ b/packages/route/lib/src/router.dart @@ -2,7 +2,6 @@ library angel_route.src.router; import 'dart:async'; import 'package:combinator/combinator.dart'; -import 'package:meta/meta.dart'; import 'package:string_scanner/string_scanner.dart'; import '../string_util.dart'; @@ -65,9 +64,8 @@ class Router { /// Adds a route that responds to the given path /// for requests with the given method (case-insensitive). /// Provide '*' as the method to respond to all methods. - Route addRoute(String? method, String path, T handler, + Route addRoute(String method, String path, T handler, {Iterable? middleware}) { - middleware ??= []; if (_useCache == true) { throw StateError('Cannot add routes after caching is enabled.'); } @@ -75,7 +73,10 @@ class Router { // Check if any mounted routers can match this final handlers = [handler]; - if (middleware != null) handlers.insertAll(0, middleware); + //middleware = []; + middleware ??= []; + + handlers.insertAll(0, middleware); final route = Route(path, method: method, handlers: handlers); _routes.add(route); @@ -115,29 +116,29 @@ class Router { /// Creates a visual representation of the route hierarchy and /// passes it to a callback. If none is provided, `print` is called. void dumpTree( - {callback(String tree)?, + {Function(String tree)? callback, String header = 'Dumping route tree:', String tab = ' '}) { final buf = StringBuffer(); - int tabs = 0; + var tabs = 0; - if (header != null && header.isNotEmpty) { + if (header.isNotEmpty) { buf.writeln(header); } buf.writeln(''); - indent() { - for (int i = 0; i < tabs; i++) { + void indent() { + for (var i = 0; i < tabs; i++) { buf.write(tab); } } - dumpRouter(Router router) { + void dumpRouter(Router router) { indent(); tabs++; - for (Route route in router.routes) { + for (var route in router.routes) { indent(); buf.write('- '); if (route is! SymlinkRoute) buf.write('${route.method} '); @@ -147,7 +148,7 @@ class Router { buf.writeln(); dumpRouter(route.router); } else { - buf.writeln(' => ${route.handlers!.length} handler(s)'); + buf.writeln(' => ${route.handlers.length} handler(s)'); } } @@ -164,9 +165,8 @@ class Router { /// /// Returns the created route. /// You can also register middleware within the router. - SymlinkRoute group(String path, void callback(Router router), - {Iterable? middleware, String? name}) { - middleware ??= []; + SymlinkRoute group(String path, void Function(Router router) callback, + {Iterable middleware = const Iterable.empty(), String name = ''}) { final router = Router().._middleware.addAll(middleware); callback(router); return mount(path, router)..name = name; @@ -174,9 +174,9 @@ class Router { /// Asynchronous equivalent of [group]. Future> groupAsync( - String path, FutureOr callback(Router router), - {Iterable? middleware, String? name}) async { - middleware ??= []; + String path, FutureOr Function(Router router) callback, + {Iterable middleware = const Iterable.empty(), + String name = ''}) async { final router = Router().._middleware.addAll(middleware); await callback(router); return mount(path, router)..name = name; @@ -210,16 +210,16 @@ class Router { /// router.navigate(['users/:id', {'id': '1337'}, 'profile']); /// ``` String navigate(Iterable linkParams, {bool absolute = true}) { - final List segments = []; + final segments = []; Router search = this; Route? lastRoute; for (final param in linkParams) { - bool resolved = false; + var resolved = false; if (param is String) { // Search by name - for (Route route in search.routes) { + for (var route in search.routes) { if (route.name == param) { segments.add(route.path.replaceAll(_straySlashes, '')); lastRoute = route; @@ -236,18 +236,23 @@ class Router { // Search by path if (!resolved) { var scanner = SpanScanner(param.replaceAll(_straySlashes, '')); - for (Route route in search.routes) { - int pos = scanner.position; - if (route.parser!.parse(scanner)!.successful && scanner.isDone) { - segments.add(route.path.replaceAll(_straySlashes, '')); - lastRoute = route; + for (var route in search.routes) { + var pos = scanner.position; + var parseResult = route.parser?.parse(scanner); + if (parseResult != null) { + if (parseResult.successful && scanner.isDone) { + segments.add(route.path.replaceAll(_straySlashes, '')); + lastRoute = route; - if (route is SymlinkRoute) { - search = route.router; + if (route is SymlinkRoute) { + search = route.router; + } + + resolved = true; + break; + } else { + scanner.position = pos; } - - resolved = true; - break; } else { scanner.position = pos; } @@ -281,39 +286,44 @@ class Router { /// Finds the first [Route] that matches the given path, /// with the given method. - bool resolve(String? absolute, String? relative, List> out, + bool resolve(String absolute, String relative, List> out, {String method = 'GET', bool strip = true}) { final cleanRelative = - strip == false ? relative! : stripStraySlashes(relative!); + strip == false ? relative : stripStraySlashes(relative); var scanner = SpanScanner(cleanRelative); bool crawl(Router r) { - bool success = false; + var success = false; for (var route in r.routes) { - int pos = scanner.position; + var pos = scanner.position; if (route is SymlinkRoute) { - if (route.parser!.parse(scanner)!.successful) { - var s = crawl(route.router); - if (s) success = true; + if (route.parser != null) { + var pp = route.parser!; + if (pp.parse(scanner).successful) { + var s = crawl(route.router); + if (s) success = true; + } } scanner.position = pos; } else if (route.method == '*' || route.method == method) { - var parseResult = route.parser!.parse(scanner)!; - - if (parseResult.successful && scanner.isDone) { - var result = RoutingResult( - parseResult: parseResult, - params: parseResult.value!.params, - shallowRoute: route, - shallowRouter: this, - tail: (parseResult.value!.tail ?? '') + scanner.rest); - out.add(result); - success = true; + var parseResult = route.parser?.parse(scanner); + if (parseResult != null) { + if (parseResult.successful && scanner.isDone) { + var tailResult = parseResult.value?.tail ?? ''; + print(tailResult); + var result = RoutingResult( + parseResult: parseResult, + params: parseResult.value!.params, + shallowRoute: route, + shallowRouter: this, + tail: tailResult + scanner.rest); + out.add(result); + success = true; + } } - scanner.position = pos; } } @@ -326,13 +336,13 @@ class Router { /// Returns the result of [resolve] with [path] passed as /// both `absolute` and `relative`. - Iterable> resolveAbsolute(String? path, + Iterable> resolveAbsolute(String path, {String method = 'GET', bool strip = true}) => resolveAll(path, path, method: method, strip: strip); /// Finds every possible [Route] that matches the given path, /// with the given method. - Iterable> resolveAll(String? absolute, String? relative, + Iterable> resolveAll(String absolute, String relative, {String method = 'GET', bool strip = true}) { if (_useCache == true) { return _cache.putIfAbsent('$method$absolute', @@ -342,7 +352,7 @@ class Router { return _resolveAll(absolute, relative, method: method, strip: strip); } - Iterable> _resolveAll(String? absolute, String? relative, + Iterable> _resolveAll(String absolute, String relative, {String method = 'GET', bool strip = true}) { var results = >[]; resolve(absolute, relative, results, method: method, strip: strip); @@ -363,88 +373,95 @@ class Router { } /// Adds a route that responds to any request matching the given path. - Route all(String path, T handler, {Iterable? middleware}) { + Route all(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('*', path, handler, middleware: middleware); } /// Adds a route that responds to a DELETE request. - Route delete(String path, T handler, {Iterable? middleware}) { + Route delete(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('DELETE', path, handler, middleware: middleware); } /// Adds a route that responds to a GET request. - Route get(String path, T handler, {Iterable? middleware}) { + Route get(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('GET', path, handler, middleware: middleware); } /// Adds a route that responds to a HEAD request. - Route head(String path, T handler, {Iterable? middleware}) { + Route head(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('HEAD', path, handler, middleware: middleware); } /// Adds a route that responds to a OPTIONS request. - Route options(String path, T handler, {Iterable? middleware}) { + Route options(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('OPTIONS', path, handler, middleware: middleware); } /// Adds a route that responds to a POST request. - Route post(String path, T handler, {Iterable? middleware}) { + Route post(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('POST', path, handler, middleware: middleware); } /// Adds a route that responds to a PATCH request. - Route patch(String path, T handler, {Iterable? middleware}) { + Route patch(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('PATCH', path, handler, middleware: middleware); } /// Adds a route that responds to a PUT request. - Route put(String path, T handler, {Iterable? middleware}) { + Route put(String path, T handler, + {Iterable middleware = const Iterable.empty()}) { return addRoute('PUT', path, handler, middleware: middleware); } } class _ChainedRouter extends Router { final List _handlers = []; - Router? _root; + Router _root; - _ChainedRouter.empty(); + _ChainedRouter.empty() : _root = Router(); - _ChainedRouter(Router? root, Iterable middleware) { - this._root = root; + _ChainedRouter(this._root, Iterable middleware) { _handlers.addAll(middleware); } @override - Route addRoute(String? method, String path, handler, + Route addRoute(String method, String path, handler, {Iterable? middleware}) { - Route route = super.addRoute(method, path, handler, - middleware: []..addAll(_handlers)..addAll(middleware ?? [])); + middleware ??= []; + var route = super.addRoute(method, path, handler, + middleware: [..._handlers, ...middleware]); //_root._routes.add(route); return route; } @override - SymlinkRoute group(String path, void callback(Router router), - {Iterable? middleware, String? name}) { - final router = _ChainedRouter( - _root, []..addAll(_handlers)..addAll(middleware ?? [])); + SymlinkRoute group(String path, void Function(Router router) callback, + {Iterable middleware = const Iterable.empty(), String name = ''}) { + final router = _ChainedRouter(_root, [..._handlers, ...middleware]); callback(router); return mount(path, router)..name = name; } @override Future> groupAsync( - String path, FutureOr callback(Router router), - {Iterable? middleware, String? name}) async { - final router = _ChainedRouter( - _root, []..addAll(_handlers)..addAll(middleware ?? [])); + String path, FutureOr Function(Router router) callback, + {Iterable middleware = const Iterable.empty(), + String name = ''}) async { + final router = _ChainedRouter(_root, [..._handlers, ...middleware]); await callback(router); return mount(path, router)..name = name; } @override SymlinkRoute mount(String path, Router router) { - final SymlinkRoute route = super.mount(path, router); + final route = super.mount(path, router); route.router._middleware.insertAll(0, _handlers); //_root._routes.add(route); return route; @@ -453,7 +470,7 @@ class _ChainedRouter extends Router { @override _ChainedRouter chain(Iterable middleware) { final piped = _ChainedRouter.empty().._root = _root; - piped._handlers.addAll([]..addAll(_handlers)..addAll(middleware)); + piped._handlers.addAll([..._handlers, ...middleware]); var route = SymlinkRoute('/', piped); _routes.add(route); return piped; @@ -473,13 +490,13 @@ Router flatten(Router router) { var path = route.path.replaceAll(_straySlashes, ''); var joined = '$base/$path'.replaceAll(_straySlashes, ''); flattened.addRoute(route.method, joined.replaceAll(_straySlashes, ''), - route.handlers!.last, + route.handlers.last, middleware: - route.handlers!.take(route.handlers!.length - 1).toList()); + route.handlers.take(route.handlers.length - 1).toList()); } } else { - flattened.addRoute(route.method, route.path, route.handlers!.last, - middleware: route.handlers!.take(route.handlers!.length - 1).toList()); + flattened.addRoute(route.method, route.path, route.handlers.last, + middleware: route.handlers.take(route.handlers.length - 1).toList()); } } diff --git a/packages/route/lib/src/routing_result.dart b/packages/route/lib/src/routing_result.dart index 03ec4f73..351d235b 100644 --- a/packages/route/lib/src/routing_result.dart +++ b/packages/route/lib/src/routing_result.dart @@ -3,23 +3,23 @@ part of angel_route.src.router; /// Represents a complex result of navigating to a path. class RoutingResult { /// The parse result that matched the given sub-path. - final ParseResult? parseResult; + final ParseResult parseResult; /// A nested instance, if a sub-path was matched. - final Iterable>? nested; + final Iterable> nested; /// All route params matching this route on the current sub-path. - final Map params = {}; + final Map params = {}; /// The [Route] that answered this sub-path. /// /// This is mostly for internal use, and useless in production. - final Route? shallowRoute; + final Route shallowRoute; /// The [Router] that answered this sub-path. /// /// Only really for internal use. - final Router? shallowRouter; + final Router shallowRouter; /// The remainder of the full path that was not matched, and was passed to [nested] routes. final String tail; @@ -28,22 +28,22 @@ class RoutingResult { RoutingResult get deepest { var search = this; - while (search.nested?.isNotEmpty == true) { - search = search.nested!.first; + while (search.nested.isNotEmpty == true) { + search = search.nested.first; } return search; } /// The most specific route. - Route? get route => deepest.shallowRoute; + Route get route => deepest.shallowRoute; /// The most specific router. - Router? get router => deepest.shallowRouter; + Router get router => deepest.shallowRouter; /// The handlers at this sub-path. List get handlers { - return [...shallowRouter!.middleware, ...shallowRoute!.handlers!]; + return [...shallowRouter.middleware, ...shallowRoute.handlers]; } /// All handlers on this sub-path and its children. @@ -53,8 +53,8 @@ class RoutingResult { void crawl(RoutingResult result) { handlers.addAll(result.handlers); - if (result.nested?.isNotEmpty == true) { - for (var r in result.nested!) { + if (result.nested.isNotEmpty == true) { + for (var r in result.nested) { crawl(r); } } @@ -66,14 +66,14 @@ class RoutingResult { } /// All parameters on this sub-path and its children. - Map get allParams { - final params = {}; + Map get allParams { + final params = {}; void crawl(RoutingResult result) { params.addAll(result.params); - if (result.nested?.isNotEmpty == true) { - for (var r in result.nested!) { + if (result.nested.isNotEmpty == true) { + for (var r in result.nested) { crawl(r); } } @@ -84,11 +84,11 @@ class RoutingResult { } RoutingResult( - {this.parseResult, - Map params = const {}, - this.nested, - this.shallowRoute, - this.shallowRouter, + {required this.parseResult, + Map params = const {}, + this.nested = const Iterable.empty(), + required this.shallowRoute, + required this.shallowRouter, required this.tail}) { this.params.addAll(params); } diff --git a/packages/route/lib/src/symlink_route.dart b/packages/route/lib/src/symlink_route.dart index 9f49b369..a52d12de 100644 --- a/packages/route/lib/src/symlink_route.dart +++ b/packages/route/lib/src/symlink_route.dart @@ -5,5 +5,5 @@ part of angel_route.src.router; class SymlinkRoute extends Route { final Router router; SymlinkRoute(String path, this.router) - : super(path, method: null, handlers: null); + : super(path, method: 'GET', handlers: []); } diff --git a/packages/route/lib/string_util.dart b/packages/route/lib/string_util.dart index df2c8e2b..d09266ad 100644 --- a/packages/route/lib/string_util.dart +++ b/packages/route/lib/string_util.dart @@ -17,7 +17,7 @@ String stripStray(String haystack, String needle) { } // Find last leading index of slash - for (int i = firstSlash + 1; i < haystack.length; i++) { + for (var i = firstSlash + 1; i < haystack.length; i++) { if (haystack[i] != needle) { var sub = haystack.substring(i); @@ -25,7 +25,7 @@ String stripStray(String haystack, String needle) { var lastSlash = sub.lastIndexOf(needle); - for (int j = lastSlash - 1; j >= 0; j--) { + for (var j = lastSlash - 1; j >= 0; j--) { if (sub[j] != needle) { return sub.substring(0, j + 1); } diff --git a/packages/route/test/chain_nest_test.dart b/packages/route/test/chain_nest_test.dart index 64c5b75f..b4a62ee7 100644 --- a/packages/route/test/chain_nest_test.dart +++ b/packages/route/test/chain_nest_test.dart @@ -11,7 +11,7 @@ void main() { ..dumpTree(); test('nested route groups with chain', () { - var r = router.resolveAbsolute('/b/e/f').first.route!; + var r = router.resolveAbsolute('/b/e/f').first.route; expect(r, isNotNull); expect(r.handlers, hasLength(4)); expect(r.handlers, equals(['a', 'c', 'd', 'g'])); diff --git a/packages/route/test/navigate_test.dart b/packages/route/test/navigate_test.dart index b68273fe..37e29e74 100644 --- a/packages/route/test/navigate_test.dart +++ b/packages/route/test/navigate_test.dart @@ -8,7 +8,7 @@ void main() { router.get('/user/:id', 'GET'); router.get('/first/:first/last/:last', 'GET').name = 'full_name'; - navigate(params) { + String navigate(params) { final uri = router.navigate(params as Iterable); print('Uri: $uri'); return uri; diff --git a/packages/route/test/wildcard_test.dart b/packages/route/test/wildcard_test.dart index e974d51b..a49cfcf1 100644 --- a/packages/route/test/wildcard_test.dart +++ b/packages/route/test/wildcard_test.dart @@ -26,21 +26,21 @@ void main() { test('tail explicitly set intermediate', () { var results = router.resolveAbsolute('/songs/in_the/key'); var result = results.first; - print(results.map((r) => {r.route!.path: r.tail})); + print(results.map((r) => {r.route.path: r.tail})); expect(result.tail, 'in_the'); }); test('tail explicitly set at end', () { var results = router.resolveAbsolute('/isnt/she/epic'); var result = results.first; - print(results.map((r) => {r.route!.path: r.tail})); + print(results.map((r) => {r.route.path: r.tail})); expect(result.tail, 'epic'); }); test('tail with trailing', () { var results = router.resolveAbsolute('/isnt/she/epic/fail'); var result = results.first; - print(results.map((r) => {r.route!.path: r.tail})); + print(results.map((r) => {r.route.path: r.tail})); expect(result.tail, 'epic/fail'); }); } diff --git a/packages/route/web/shared/basic.dart b/packages/route/web/shared/basic.dart index 3f00c120..ff3bbebb 100644 --- a/packages/route/web/shared/basic.dart +++ b/packages/route/web/shared/basic.dart @@ -1,25 +1,30 @@ import 'dart:html'; import 'package:angel_route/browser.dart'; -basic(BrowserRouter router) { +void basic(BrowserRouter router) { final $h1 = window.document.querySelector('h1'); final $ul = window.document.getElementById('handlers'); router.onResolve.listen((result) { - final route = result?.route; + final route = result.route; - if (route == null) { - $h1!.text = 'No Active Route'; - $ul!.children + // TODO: Relook at this logic + //if (route == null) { + // $h1!.text = 'No Active Route'; + // $ul!.children + // ..clear() + // ..add(LIElement()..text = '(empty)'); + //} else { + if ($h1 != null && $ul != null) { + $h1.text = 'Active Route: ${route.name}'; + $ul.children ..clear() - ..add(LIElement()..text = '(empty)'); - } else { - $h1!.text = 'Active Route: ${route.name ?? route.path}'; - $ul!.children - ..clear() - ..addAll(result!.allHandlers + ..addAll(result.allHandlers .map((handler) => LIElement()..text = handler.toString())); + } else { + print('No active Route'); } + //} }); router.get('a', 'a handler');