Fixed migration bugs

This commit is contained in:
thomashii 2021-04-03 13:50:52 +08:00
parent 01171d8954
commit ddbf7f33b6
22 changed files with 477 additions and 412 deletions

View file

@ -4,6 +4,7 @@ import 'dart:io' show stderr, Cookie;
import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart'; import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart'; import 'package:combinator/combinator.dart';
import 'package:logging/logging.dart';
import 'package:stack_trace/stack_trace.dart'; import 'package:stack_trace/stack_trace.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'core.dart'; import 'core.dart';
@ -17,11 +18,12 @@ abstract class Driver<
Server extends Stream<Request>, Server extends Stream<Request>,
RequestContextType extends RequestContext, RequestContextType extends RequestContext,
ResponseContextType extends ResponseContext> { ResponseContextType extends ResponseContext> {
final Angel? app; final Angel app;
final bool useZone; final bool useZone;
bool _closed = false; bool _closed = false;
Server? _server; late Server _server;
StreamSubscription<Request>? _sub; StreamSubscription<Request>? _sub;
final log = Logger('Driver');
/// The function used to bind this instance to a server.. /// The function used to bind this instance to a server..
final Future<Server> Function(dynamic, int) serverGenerator; final Future<Server> Function(dynamic, int) serverGenerator;
@ -32,39 +34,44 @@ abstract class Driver<
Uri get uri; Uri get uri;
/// The native server running this instance. /// The native server running this instance.
Server? get server => _server; Server get server => _server;
Future<Server> generateServer(address, int port) => Future<Server> generateServer(address, int port) =>
serverGenerator(address, port); serverGenerator(address, port);
/// Starts, and returns the server. /// Starts, and returns the server.
Future<Server> startServer([address, int? port]) { Future<Server> startServer([address, int port = 5000]) {
var host = address ?? '127.0.0.1'; var host = address ?? '127.0.0.1';
return generateServer(host, port ?? 0).then((server) { return generateServer(host, port).then((server) {
_server = server; _server = server;
return Future.wait(app!.startupHooks.map(app!.configure)).then((_) { return Future.wait(app.startupHooks.map(app.configure)).then((_) {
app!.optimizeForProduction(); app.optimizeForProduction();
_sub = server.listen((request) { _sub = server.listen((request) {
var stream = createResponseStreamFromRawRequest(request); var stream = createResponseStreamFromRawRequest(request);
stream.listen((response) { stream.listen((response) {
// TODO: To be revisited // TODO: To be revisited
handleRawRequest(request, response); 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. /// Shuts down the underlying server.
Future<Server> close() { Future<Server> close() {
if (_closed) return Future.value(_server); if (_closed) {
return Future.value(_server);
}
_closed = true; _closed = true;
_sub?.cancel(); _sub?.cancel();
return app!.close().then((_) => return app.close().then((_) =>
Future.wait(app!.shutdownHooks.map(app!.configure)) Future.wait(app.shutdownHooks.map(app.configure))
.then((_) => _server!)); .then((_) => Future.value(_server)));
} }
Future<RequestContextType> createRequestContext( Future<RequestContextType> createRequestContext(
@ -102,7 +109,7 @@ abstract class Driver<
Tuple4<List?, Map<String?, dynamic>, ParseResult<RouteResult?>?, Tuple4<List?, Map<String?, dynamic>, ParseResult<RouteResult?>?,
MiddlewarePipeline> resolveTuple() { MiddlewarePipeline> resolveTuple() {
var r = app!.optimizedRouter; var r = app.optimizedRouter;
var resolved = var resolved =
r.resolveAbsolute(path, method: req.method!, strip: false); r.resolveAbsolute(path, method: req.method!, strip: false);
var pipeline = MiddlewarePipeline<RequestHandler?>(resolved); var pipeline = MiddlewarePipeline<RequestHandler?>(resolved);
@ -116,8 +123,8 @@ abstract class Driver<
} }
var cacheKey = req.method! + path!; var cacheKey = req.method! + path!;
var tuple = app!.environment.isProduction var tuple = app.environment.isProduction
? app!.handlerCache.putIfAbsent(cacheKey, resolveTuple) ? app.handlerCache.putIfAbsent(cacheKey, resolveTuple)
: resolveTuple(); : resolveTuple();
var line = tuple.item4 as MiddlewarePipeline<RequestHandler?>; var line = tuple.item4 as MiddlewarePipeline<RequestHandler?>;
var it = MiddlewarePipelineIterator<RequestHandler?>(line); var it = MiddlewarePipelineIterator<RequestHandler?>(line);
@ -134,7 +141,7 @@ abstract class Driver<
..registerSingleton<ParseResult<RouteResult?>?>(tuple.item3) ..registerSingleton<ParseResult<RouteResult?>?>(tuple.item3)
..registerSingleton<ParseResult?>(tuple.item3); ..registerSingleton<ParseResult?>(tuple.item3);
if (!app!.environment.isProduction && app!.logger != null) { if (app.environment.isProduction && app.logger != null) {
req.container!.registerSingleton<Stopwatch>(Stopwatch()..start()); req.container!.registerSingleton<Stopwatch>(Stopwatch()..start());
} }
@ -160,14 +167,14 @@ abstract class Driver<
stackTrace: st, stackTrace: st,
statusCode: 500, statusCode: 500,
message: e?.toString() ?? '500 Internal Server Error'); message: e?.toString() ?? '500 Internal Server Error');
}, test: (e) => e is! AngelHttpException).catchError( }, test: (e) => e is AngelHttpException).catchError(
(ee, StackTrace st) { (ee, StackTrace st) {
var e = ee as AngelHttpException; var e = ee as AngelHttpException;
if (app!.logger != null) { if (app.logger != null) {
var error = e.error ?? e; var error = e.error ?? e;
var trace = Trace.from(e.stackTrace ?? StackTrace.current).terse; 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( return handleAngelHttpException(
@ -176,8 +183,8 @@ abstract class Driver<
} else { } else {
var zoneSpec = ZoneSpecification( var zoneSpec = ZoneSpecification(
print: (self, parent, zone, line) { print: (self, parent, zone, line) {
if (app!.logger != null) { if (app.logger != null) {
app!.logger!.info(line); app.logger?.info(line);
} else { } else {
parent.print(zone, line); parent.print(zone, line);
} }
@ -198,8 +205,8 @@ abstract class Driver<
stackTrace: stackTrace, message: error.toString()); stackTrace: stackTrace, message: error.toString());
} }
if (app!.logger != null) { if (app.logger != null) {
app!.logger!.severe(e.message ?? e.toString(), error, trace); app.logger?.severe(e.message ?? e.toString(), error, trace);
} }
return handleAngelHttpException( return handleAngelHttpException(
@ -209,8 +216,8 @@ abstract class Driver<
closeResponse(response); closeResponse(response);
// Ideally, we won't be in a position where an absolutely fatal error occurs, // Ideally, we won't be in a position where an absolutely fatal error occurs,
// but if so, we'll need to log it. // but if so, we'll need to log it.
if (app!.logger != null) { if (app.logger != null) {
app!.logger!.severe( app.logger?.severe(
'Fatal error occurred when processing $uri.', e, trace); 'Fatal error occurred when processing $uri.', e, trace);
} else { } else {
stderr stderr
@ -220,7 +227,6 @@ abstract class Driver<
..writeln(trace); ..writeln(trace);
} }
}); });
return;
}, },
); );
@ -243,7 +249,7 @@ abstract class Driver<
} }
/// Handles an [AngelHttpException]. /// Handles an [AngelHttpException].
Future? handleAngelHttpException( Future handleAngelHttpException(
AngelHttpException e, AngelHttpException e,
StackTrace st, StackTrace st,
RequestContext? req, RequestContext? req,
@ -253,12 +259,12 @@ abstract class Driver<
{bool ignoreFinalizers = false}) { {bool ignoreFinalizers = false}) {
if (req == null || res == null) { if (req == null || res == null) {
try { try {
app!.logger?.severe(null, e, st); app.logger?.severe(null, e, st);
setStatusCode(response, 500); setStatusCode(response, 500);
writeStringToResponse(response, '500 Internal Server Error'); writeStringToResponse(response, '500 Internal Server Error');
closeResponse(response); closeResponse(response);
} finally { } finally {
return null; return Future.value();
} }
} }
@ -269,8 +275,8 @@ abstract class Driver<
} else { } else {
res.statusCode = e.statusCode; res.statusCode = e.statusCode;
handleError = handleError =
Future.sync(() => app!.errorHandler(e, req, res)).then((result) { Future.sync(() => app.errorHandler(e, req, res)).then((result) {
return app!.executeHandler(result, req, res).then((_) => res.close()); return app.executeHandler(result, req, res).then((_) => res.close());
}); });
} }
@ -283,11 +289,11 @@ abstract class Driver<
ResponseContext res, ResponseContext res,
{bool ignoreFinalizers = false}) { {bool ignoreFinalizers = false}) {
Future<void> _cleanup(_) { Future<void> _cleanup(_) {
if (!app!.environment.isProduction && if (app.environment.isProduction &&
app!.logger != null && app.logger != null &&
req.container!.has<Stopwatch>()) { req.container!.has<Stopwatch>()) {
var sw = req.container!.make<Stopwatch>(); var sw = req.container!.make<Stopwatch>();
app!.logger!.info( app.logger?.info(
"${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)"); "${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)");
} }
return req.close(); return req.close();
@ -297,7 +303,7 @@ abstract class Driver<
Future finalizers = ignoreFinalizers == true Future finalizers = ignoreFinalizers == true
? Future.value() ? Future.value()
: Future.forEach(app!.responseFinalizers, (dynamic f) => f(req, res)); : Future.forEach(app.responseFinalizers, (dynamic f) => f(req, res));
return finalizers.then((_) { return finalizers.then((_) {
//if (res.isOpen) res.close(); //if (res.isOpen) res.close();
@ -358,13 +364,13 @@ abstract class Driver<
MiddlewarePipelineIterator<RequestHandler?> it, MiddlewarePipelineIterator<RequestHandler?> it,
RequestContextType req, RequestContextType req,
ResponseContextType res, ResponseContextType res,
Angel? app) async { Angel app) async {
var broken = false; var broken = false;
while (it.moveNext()) { while (it.moveNext()) {
var current = it.current.handlers.iterator; var current = it.current.handlers.iterator;
while (!broken && current.moveNext()) { 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) { if (result != true) {
broken = true; broken = true;
break; break;

View file

@ -30,7 +30,7 @@ RequestHandler chain(Iterable<RequestHandler> handlers) {
runPipeline = () => Future.sync(() => handler(req, res)); runPipeline = () => Future.sync(() => handler(req, res));
} else { } else {
var current = runPipeline; var current = runPipeline;
runPipeline = () => current().then((result) => !res.isOpen runPipeline = () => current().then((result) => res.isOpen
? Future.value(result) ? Future.value(result)
: req.app.executeHandler(handler, req, res)); : req.app.executeHandler(handler, req, res));
} }
@ -42,7 +42,7 @@ RequestHandler chain(Iterable<RequestHandler> handlers) {
} }
/// A routable server that can handle dynamic requests. /// A routable server that can handle dynamic requests.
class Routable extends Router<RequestHandler?> { class Routable extends Router<RequestHandler> {
final Map<Pattern, Service> _services = {}; final Map<Pattern, Service> _services = {};
final Map<Pattern, Service?> _serviceLookups = {}; final Map<Pattern, Service?> _serviceLookups = {};
final Map configuration = {}; final Map configuration = {};
@ -92,10 +92,9 @@ class Routable extends Router<RequestHandler?> {
} }
@override @override
Route<RequestHandler?> addRoute( Route<RequestHandler> addRoute(
String? method, String? path, RequestHandler? handler, String method, String path, RequestHandler handler,
{Iterable<RequestHandler?>? middleware}) { {Iterable<RequestHandler> middleware = const Iterable.empty()}) {
middleware ??= [];
final handlers = <RequestHandler>[]; final handlers = <RequestHandler>[];
// Merge @Middleware declaration, if any // Merge @Middleware declaration, if any
var reflector = _container?.reflector; var reflector = _container?.reflector;
@ -108,10 +107,7 @@ class Routable extends Router<RequestHandler?> {
} }
final handlerSequence = <RequestHandler>[]; final handlerSequence = <RequestHandler>[];
handlerSequence.addAll(middleware as Iterable< handlerSequence.addAll(middleware);
FutureOr<dynamic>? Function(
RequestContext<dynamic>, ResponseContext<dynamic>)>? ??
[]);
handlerSequence.addAll(handlers); handlerSequence.addAll(handlers);
return super.addRoute(method, path.toString(), handler, return super.addRoute(method, path.toString(), handler,

View file

@ -19,15 +19,19 @@ final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer, class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpRequestContext, HttpResponseContext> { HttpRequestContext, HttpResponseContext> {
@override @override
Uri get uri => server == null Uri get uri {
? Uri() if (server == null) {
: Uri(scheme: 'http', host: server!.address.address, port: server!.port); 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<HttpServer> Function(dynamic, int) serverGenerator, bool useZone) Future<HttpServer> Function(dynamic, int) serverGenerator, bool useZone)
: super(app, serverGenerator, useZone: 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); return AngelHttp._(app, HttpServer.bind, useZone);
} }
@ -59,12 +63,18 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
var serverContext = SecurityContext(); var serverContext = SecurityContext();
serverContext.useCertificateChain(certificateChain, password: password); serverContext.useCertificateChain(certificateChain, password: password);
serverContext.usePrivateKey(serverKey, password: password); serverContext.usePrivateKey(serverKey, password: password);
return AngelHttp.fromSecurityContext(app, serverContext, useZone: useZone); return AngelHttp.fromSecurityContext(app, serverContext, useZone: useZone);
} }
/// Use [server] instead. /// Use [server] instead.
@deprecated @deprecated
HttpServer? get httpServer => server; HttpServer get httpServer {
if (server == null) {
throw ArgumentError("[AngelHttp] Server instance not initialised");
}
return server!;
}
Future handleRequest(HttpRequest request) => Future handleRequest(HttpRequest request) =>
handleRawRequest(request, request.response); handleRawRequest(request, request.response);
@ -75,7 +85,6 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
@override @override
Future<HttpServer> close() async { Future<HttpServer> close() async {
await server?.close();
return await super.close(); return await super.close();
} }
@ -87,7 +96,7 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpRequest request, HttpResponse response) { HttpRequest request, HttpResponse response) {
var path = request.uri.path.replaceAll(_straySlashes, ''); var path = request.uri.path.replaceAll(_straySlashes, '');
if (path.isEmpty) path = '/'; if (path.isEmpty) path = '/';
return HttpRequestContext.from(request, app!, path); return HttpRequestContext.from(request, app, path);
} }
@override @override
@ -98,10 +107,10 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpResponseContext context = HttpResponseContext context =
HttpResponseContext(response, app, correspondingRequest); HttpResponseContext(response, app, correspondingRequest);
if (app!.serializer == null) { if (app.serializer == null) {
context.serializer = json.encode; context.serializer = json.encode;
} else { } else {
context.serializer = app!.serializer; context.serializer = app.serializer;
} }
return Future<HttpResponseContext>.value(context); return Future<HttpResponseContext>.value(context);
// return Future<HttpResponseContext>.value( // return Future<HttpResponseContext>.value(

View file

@ -70,7 +70,7 @@ bool bar(RequestContext req, ResponseContext res) {
} }
main() { main() {
Angel? app; late Angel app;
late TodoController todoController; late TodoController todoController;
late NoExposeController noExposeCtrl; late NoExposeController noExposeCtrl;
late HttpServer server; late HttpServer server;
@ -79,30 +79,30 @@ main() {
setUp(() async { setUp(() async {
app = Angel(reflector: MirrorsReflector()); app = Angel(reflector: MirrorsReflector());
app!.get( app.get(
"/redirect", "/redirect",
(req, res) async => (req, res) async =>
res.redirectToAction("TodoController@foo", {"foo": "world"})); res.redirectToAction("TodoController@foo", {"foo": "world"}));
// Register as a singleton, just for the purpose of this test // Register as a singleton, just for the purpose of this test
if (!app!.container!.has<TodoController>()) { if (!app.container!.has<TodoController>()) {
app!.container!.registerSingleton(todoController = TodoController()); app.container!.registerSingleton(todoController = TodoController());
} }
// Using mountController<T>(); // Using mountController<T>();
await app!.mountController<TodoController>(); await app.mountController<TodoController>();
noExposeCtrl = await app!.mountController<NoExposeController>(); noExposeCtrl = await app.mountController<NoExposeController>();
// Place controller in group. The applyRoutes() call, however, is async. // Place controller in group. The applyRoutes() call, however, is async.
// Until https://github.com/angel-dart/route/issues/28 is closed, // Until https://github.com/angel-dart/route/issues/28 is closed,
// this will need to be done by manually mounting the router. // this will need to be done by manually mounting the router.
var subRouter = Router<RequestHandler>(); var subRouter = Router<RequestHandler>();
await todoController.applyRoutes(subRouter, app!.container!.reflector); await todoController.applyRoutes(subRouter, app.container!.reflector);
app!.mount('/ctrl_group', subRouter); app.mount('/ctrl_group', subRouter);
print(app!.controllers); print(app.controllers);
app!.dumpTree(); app.dumpTree();
server = await AngelHttp(app).startServer(); server = await AngelHttp(app).startServer();
url = 'http://${server.address.address}:${server.port}'; url = 'http://${server.address.address}:${server.port}';
@ -110,7 +110,6 @@ main() {
tearDown(() async { tearDown(() async {
await server.close(force: true); await server.close(force: true);
app = null;
url = null; url = null;
}); });

View file

@ -15,8 +15,8 @@ final String TEXT = "make your bed";
final String OVER = "never"; final String OVER = "never";
main() { main() {
Angel? app; late Angel app;
http.Client? client; late http.Client client;
late HttpServer server; late HttpServer server;
String? url; String? url;
@ -25,31 +25,29 @@ main() {
client = http.Client(); client = http.Client();
// Inject some todos // Inject some todos
app!.container!.registerSingleton(Todo(text: TEXT, over: OVER)); app.container!.registerSingleton(Todo(text: TEXT, over: OVER));
app!.container!.registerFactory<Future<Foo>>((container) async { app.container!.registerFactory<Future<Foo>>((container) async {
var req = container.make<RequestContext>()!; var req = container.make<RequestContext>()!;
var text = await utf8.decoder.bind(req.body!).join(); var text = await utf8.decoder.bind(req.body!).join();
return Foo(text); return Foo(text);
}); });
app!.get("/errands", ioc((Todo singleton) => singleton)); app.get("/errands", ioc((Todo singleton) => singleton));
app!.get( app.get(
"/errands3", "/errands3",
ioc(({required Errand singleton, Todo? foo, RequestContext? req}) => ioc(({required Errand singleton, Todo? foo, RequestContext? req}) =>
singleton.text)); singleton.text));
app!.post('/async', ioc((Foo foo) => {'baz': foo.bar})); app.post('/async', ioc((Foo foo) => {'baz': foo.bar}));
await app!.configure(SingletonController().configureServer); await app.configure(SingletonController().configureServer);
await app!.configure(ErrandController().configureServer); await app.configure(ErrandController().configureServer);
server = await AngelHttp(app).startServer(); server = await AngelHttp(app).startServer();
url = "http://${server.address.host}:${server.port}"; url = "http://${server.address.host}:${server.port}";
}); });
tearDown(() async { tearDown(() async {
app = null;
url = null; url = null;
client!.close(); client.close();
client = null;
await server.close(force: true); await server.close(force: true);
}); });
@ -71,33 +69,33 @@ main() {
}); });
test("singleton in route", () async { 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 { 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 { 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?; var text = await json.decode(response.body) as String?;
expect(text, equals(TEXT)); expect(text, equals(TEXT));
}); });
test("make in controller", () async { 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?; var text = await json.decode(response.body) as String?;
expect(text, equals(TEXT)); expect(text, equals(TEXT));
}); });
test('resolve from future in controller', () async { test('resolve from future in controller', () async {
var response = 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'})); expect(response.body, json.encode({'bar': 'hey'}));
}); });
test('resolve from future in route', () async { 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'})); expect(response.body, json.encode({'baz': 'yes'}));
}); });
} }

View file

@ -7,10 +7,10 @@ import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { main() {
Angel? app; late Angel app;
http.Client? client; late http.Client client;
late HttpServer server; late HttpServer server;
String? url; late String url;
setUp(() async { setUp(() async {
app = Angel(reflector: MirrorsReflector()) app = Angel(reflector: MirrorsReflector())
@ -23,15 +23,12 @@ main() {
}); });
tearDown(() async { tearDown(() async {
app = null; client.close();
url = null;
client!.close();
client = null;
await server.close(force: true); await server.close(force: true);
}); });
test("allow override of method", () async { 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'}); headers: {'X-HTTP-Method-Override': 'POST'});
print('Response: ${response.body}'); print('Response: ${response.body}');
expect(json.decode(response.body), equals({'hello': 'world'})); expect(json.decode(response.body), equals({'hello': 'world'}));

View file

@ -13,25 +13,26 @@ main() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}; };
Angel? app; late Angel app;
late HttpServer server; late HttpServer server;
String? url; late String url;
http.Client? client; late http.Client client;
HookedService? todoService; late HookedService todoService;
setUp(() async { setUp(() async {
app = Angel(reflector: MirrorsReflector()); app = Angel(reflector: MirrorsReflector());
client = http.Client(); client = http.Client();
app!.use('/todos', MapService()); app.use('/todos', MapService());
app!.use('/books', BookService()); app.use('/books', BookService());
todoService = app!.findHookedService<MapService>('todos'); todoService = app.findHookedService<MapService>('todos')
as HookedService<dynamic, dynamic, Service>;
todoService!.beforeAllStream().listen((e) { todoService.beforeAllStream().listen((e) {
print('Fired ${e.eventName}! Data: ${e.data}; Params: ${e.params}'); 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; throw e.error as Object;
}; };
@ -41,31 +42,27 @@ main() {
tearDown(() async { tearDown(() async {
await server.close(force: true); await server.close(force: true);
app = null; client.close();
url = null;
client!.close();
client = null;
todoService = null;
}); });
test("listen before and after", () async { test("listen before and after", () async {
int count = 0; int count = 0;
todoService todoService
?..beforeIndexed.listen((_) { ..beforeIndexed.listen((_) {
count++; count++;
}) })
..afterIndexed.listen((_) { ..afterIndexed.listen((_) {
count++; count++;
}); });
var response = await client!.get(Uri.parse("$url/todos")); var response = await client.get(Uri.parse("$url/todos"));
print(response.body); print(response.body);
expect(count, equals(2)); expect(count, equals(2));
}); });
test("cancel before", () async { test("cancel before", () async {
todoService!.beforeCreated todoService.beforeCreated
..listen((HookedServiceEvent event) { ..listen((HookedServiceEvent event) {
event.cancel({"hello": "hooked world"}); event.cancel({"hello": "hooked world"});
}) })
@ -73,7 +70,7 @@ main() {
event.cancel({"this_hook": "should never run"}); 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"}), body: json.encode({"arbitrary": "data"}),
headers: headers as Map<String, String>); headers: headers as Map<String, String>);
print(response.body); print(response.body);
@ -82,7 +79,7 @@ main() {
}); });
test("cancel after", () async { test("cancel after", () async {
todoService!.afterIndexed todoService.afterIndexed
..listen((HookedServiceEvent event) async { ..listen((HookedServiceEvent event) async {
// Hooks can be Futures ;) // Hooks can be Futures ;)
event.cancel([ event.cancel([
@ -93,20 +90,20 @@ main() {
event.cancel({"this_hook": "should never run either"}); 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); print(response.body);
var result = json.decode(response.body) as List; var result = json.decode(response.body) as List;
expect(result[0]["angel"], equals("framework")); expect(result[0]["angel"], equals("framework"));
}); });
test('asStream() fires', () async { test('asStream() fires', () async {
var stream = todoService!.afterCreated.asStream(); var stream = todoService.afterCreated.asStream();
await todoService!.create({'angel': 'framework'}); await todoService.create({'angel': 'framework'});
expect(await stream.first.then((e) => e.result['angel']), 'framework'); expect(await stream.first.then((e) => e.result['angel']), 'framework');
}); });
test('metadata', () async { test('metadata', () async {
final service = HookedService(IncrementService())..addHooks(app!); final service = HookedService(IncrementService())..addHooks(app);
expect(service.inner, isNot(const IsInstanceOf<MapService>())); expect(service.inner, isNot(const IsInstanceOf<MapService>()));
IncrementService.TIMES = 0; IncrementService.TIMES = 0;
await service.index(); await service.index();
@ -114,15 +111,15 @@ main() {
}); });
test('inject request + response', () async { test('inject request + response', () async {
HookedService books = app!.findService('books') HookedService books = app.findService('books')
as HookedService<dynamic, dynamic, Service<dynamic, dynamic>>; as HookedService<dynamic, dynamic, Service<dynamic, dynamic>>;
books.beforeIndexed.listen((e) { books.beforeIndexed.listen((e) {
expect([e.request, e.response], everyElement(isNotNull)); 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); print(response.body);
var result = json.decode(response.body); var result = json.decode(response.body);
@ -138,8 +135,8 @@ main() {
var type = e.isBefore ? 'before' : 'after'; var type = e.isBefore ? 'before' : 'after';
print('Params to $type ${e.eventName}: ${e.params}'); print('Params to $type ${e.eventName}: ${e.params}');
expect(e.params, isMap); expect(e.params, isMap);
expect(e.params!.keys, contains('provider')); expect(e.params?.keys, contains('provider'));
expect(e.params!['provider'], const IsInstanceOf<Providers>()); expect(e.params?['provider'], const IsInstanceOf<Providers>());
} }
svc svc

View file

@ -35,11 +35,11 @@ bool interceptService(RequestContext req, ResponseContext res) {
} }
main() { main() {
Angel? app; late Angel app;
Angel? nested; late Angel nested;
Angel? todos; late Angel todos;
String? url; late String url;
http.Client? client; late http.Client client;
setUp(() async { setUp(() async {
app = Angel(reflector: MirrorsReflector()); app = Angel(reflector: MirrorsReflector());
@ -47,7 +47,7 @@ main() {
todos = Angel(reflector: MirrorsReflector()); todos = Angel(reflector: MirrorsReflector());
[app, nested, todos].forEach((Angel? app) { [app, nested, todos].forEach((Angel? app) {
app!.logger = Logger('routing_test') app?.logger = Logger('routing_test')
..onRecord.listen((rec) { ..onRecord.listen((rec) {
if (rec.error != null) { if (rec.error != null) {
stdout 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; late Route ted;
ted = nested!.post('/ted/:route', (RequestContext req, res) { ted = nested.post('/ted/:route', (RequestContext req, res) {
print('Params: ${req.params}'); print('Params: ${req.params}');
print('Path: ${ted.path}, uri: ${req.path}'); print('Path: ${ted.path}, uri: ${req.path}');
print('matcher: ${ted.parser}'); print('matcher: ${ted.parser}');
return req.params; return req.params;
}); });
app!.mount('/nes', nested!); app.mount('/nes', nested);
app!.get('/meta', testMiddlewareMetadata); app.get('/meta', testMiddlewareMetadata);
app!.get('/intercepted', (req, res) => 'This should not be shown', app.get('/intercepted', (req, res) => 'This should not be shown',
middleware: [interceptor]); middleware: [interceptor]);
app!.get('/hello', (req, res) => 'world'); app.get('/hello', (req, res) => 'world');
app!.get('/name/:first/last/:last', (req, res) => req.params); app.get('/name/:first/last/:last', (req, res) => req.params);
app!.post( app.post(
'/lambda', '/lambda',
(RequestContext req, res) => (RequestContext req, res) =>
req.parseBody().then((_) => req.bodyAsMap)); req.parseBody().then((_) => req.bodyAsMap));
app!.mount('/todos/:id', todos!); app.mount('/todos/:id', todos);
app! app
.get('/greet/:name', .get('/greet/:name',
(RequestContext req, res) async => "Hello ${req.params['name']}") (RequestContext req, res) async => "Hello ${req.params['name']}")
.name = 'Named routes'; .name = 'Named routes';
app!.get('/named', (req, ResponseContext res) async { app.get('/named', (req, ResponseContext res) async {
await res.redirectTo('Named routes', {'name': 'tests'}); 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}"); print("Query: ${req.queryParameters}");
return "Logged"; return "Logged";
}); });
app!.get('/method', (req, res) => 'Only GET'); app.get('/method', (req, res) => 'Only GET');
app!.post('/method', (req, res) => 'Only POST'); app.post('/method', (req, res) => 'Only POST');
app!.use('/query', QueryService()); app.use('/query', QueryService());
RequestHandler write(String message) { RequestHandler write(String message) {
return (req, res) { 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()); '/chained', (req, res) => res.close());
app!.fallback((req, res) => 'MJ'); app.fallback((req, res) => 'MJ');
//app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true); //app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true);
@ -117,22 +117,17 @@ main() {
}); });
tearDown(() async { tearDown(() async {
await app!.close(); await app.close();
app = null; client.close();
nested = null;
todos = null;
client!.close();
client = null;
url = null;
}); });
test('Can match basic url', () async { 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"')); expect(response.body, equals('"world"'));
}); });
test('Can match url with multiple parameters', () async { 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}'); print('Response: ${response.body}');
var json_ = json.decode(response.body); var json_ = json.decode(response.body);
expect(json_, const IsInstanceOf<Map>()); expect(json_, const IsInstanceOf<Map>());
@ -141,18 +136,18 @@ main() {
}); });
test('Chained routes', () async { 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')); expect(response.body, equals('abc'));
}); });
test('Can nest another Angel instance', () async { 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); var json_ = json.decode(response.body);
expect(json_['route'], equals('foo')); expect(json_['route'], equals('foo'));
}); });
test('Can parse parameters from a nested Angel instance', () async { 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); var json_ = json.decode(response.body);
print('JSON: $json_'); print('JSON: $json_');
expect(json_['id'], equals('1337')); expect(json_['id'], equals('1337'));
@ -160,32 +155,32 @@ main() {
}); });
test('Can add and use named middleware', () async { 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')); expect(response.body, equals('Middleware'));
}); });
test('Middleware via metadata', () async { test('Middleware via metadata', () async {
// Metadata // Metadata
var response = await client!.get(Uri.parse('$url/meta')); var response = await client.get(Uri.parse('$url/meta'));
expect(response.body, equals('Middleware')); expect(response.body, equals('Middleware'));
}); });
test('Can serialize function result as JSON', () async { test('Can serialize function result as JSON', () async {
Map headers = <String, String>{'Content-Type': 'application/json'}; Map headers = <String, String>{'Content-Type': 'application/json'};
String postData = json.encode({'it': 'works'}); 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<String, String>, body: postData); headers: headers as Map<String, String>, body: postData);
print('Response: ${response.body}'); print('Response: ${response.body}');
expect(json.decode(response.body)['it'], equals('works')); expect(json.decode(response.body)['it'], equals('works'));
}); });
test('Fallback routes', () async { 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"')); expect(response.body, equals('"MJ"'));
}); });
test('Can name routes', () { 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'); print('Foo: $foo');
String uri = foo.makeUri({'id': 'angel'}); String uri = foo.makeUri({'id': 'angel'});
print(uri); print(uri);
@ -193,32 +188,32 @@ main() {
}); });
test('Redirect to named routes', () async { 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); print(response.body);
expect(json.decode(response.body), equals('Hello tests')); expect(json.decode(response.body), equals('Hello tests'));
}); });
test('Match routes, even with query params', () async { 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")); .get(Uri.parse("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo"));
print(response.body); print(response.body);
expect(json.decode(response.body), equals('Logged')); 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); print(response.body);
expect(response.body, equals("Service with Middleware")); expect(response.body, equals("Service with Middleware"));
}); });
test('only match route with matching method', () async { 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); print(response.body);
expect(response.body, '"Only GET"'); expect(response.body, '"Only GET"');
response = await client!.post(Uri.parse("$url/method")); response = await client.post(Uri.parse("$url/method"));
print(response.body); print(response.body);
expect(response.body, '"Only POST"'); expect(response.body, '"Only POST"');
response = await client!.patch(Uri.parse("$url/method")); response = await client.patch(Uri.parse("$url/method"));
print(response.body); print(response.body);
expect(response.body, '"MJ"'); expect(response.body, '"MJ"');
}); });

View file

@ -8,10 +8,10 @@ import 'package:http_parser/http_parser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { main() {
Angel? app; late Angel app;
http.Client? client; late http.Client client;
late HttpServer server; late HttpServer server;
String? url; late String url;
setUp(() async { setUp(() async {
app = Angel(reflector: MirrorsReflector()) app = Angel(reflector: MirrorsReflector())
@ -27,19 +27,16 @@ main() {
}); });
tearDown(() async { tearDown(() async {
app = null; client.close();
url = null;
client!.close();
client = null;
await server.close(force: true); await server.close(force: true);
}); });
test("correct content-type", () async { 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}'); print('Response: ${response.body}');
expect(response.headers['content-type'], contains('application/json')); 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}'); print('Response: ${response.body}');
expect(response.headers['content-type'], contains('text/html')); expect(response.headers['content-type'], contains('text/html'));
}); });

View file

@ -18,10 +18,10 @@ main() {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}; };
Angel? app; late Angel app;
late MapService service; late MapService service;
String? url; late String url;
http.Client? client; late http.Client client;
setUp(() async { setUp(() async {
app = Angel(reflector: MirrorsReflector()) app = Angel(reflector: MirrorsReflector())
@ -37,16 +37,13 @@ main() {
}); });
tearDown(() async { tearDown(() async {
await app!.close(); await app.close();
app = null; client.close();
url = null;
client!.close();
client = null;
}); });
group('memory', () { group('memory', () {
test('can index an empty service', () async { 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); print(response.body);
expect(response.body, equals('[]')); expect(response.body, equals('[]'));
print(response.body); print(response.body);
@ -55,7 +52,7 @@ main() {
test('can create data', () async { test('can create data', () async {
String postData = json.encode({'text': 'Hello, world!'}); 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<String, String>, body: postData); headers: headers as Map<String, String>, body: postData);
expect(response.statusCode, 201); expect(response.statusCode, 201);
var jsons = json.decode(response.body); var jsons = json.decode(response.body);
@ -65,9 +62,9 @@ main() {
test('can fetch data', () async { test('can fetch data', () async {
String postData = json.encode({'text': 'Hello, world!'}); 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<String, String>, body: postData); headers: headers as Map<String, String>, 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); expect(response.statusCode, 200);
var jsons = json.decode(response.body); var jsons = json.decode(response.body);
print(jsons); print(jsons);
@ -76,12 +73,12 @@ main() {
test('can modify data', () async { test('can modify data', () async {
String postData = json.encode({'text': 'Hello, world!'}); 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<String, String>, body: postData); headers: headers as Map<String, String>, body: postData);
postData = json.encode({'text': 'modified'}); postData = json.encode({'text': 'modified'});
var response = await client! var response = await client.patch(Uri.parse("$url/todos/0"),
.patch(Uri.parse("$url/todos/0"), headers: headers, body: postData); headers: headers, body: postData);
expect(response.statusCode, 200); expect(response.statusCode, 200);
var jsons = json.decode(response.body); var jsons = json.decode(response.body);
print(jsons); print(jsons);
@ -90,12 +87,12 @@ main() {
test('can overwrite data', () async { test('can overwrite data', () async {
String postData = json.encode({'text': 'Hello, world!'}); 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<String, String>, body: postData); headers: headers as Map<String, String>, body: postData);
postData = json.encode({'over': 'write'}); postData = json.encode({'over': 'write'});
var response = await client! var response = await client.post(Uri.parse("$url/todos/0"),
.post(Uri.parse("$url/todos/0"), headers: headers, body: postData); headers: headers, body: postData);
expect(response.statusCode, 200); expect(response.statusCode, 200);
var jsons = json.decode(response.body); var jsons = json.decode(response.body);
print(jsons); print(jsons);
@ -116,12 +113,12 @@ main() {
test('can delete data', () async { test('can delete data', () async {
String postData = json.encode({'text': 'Hello, world!'}); String postData = json.encode({'text': 'Hello, world!'});
var created = await client! var created = await client
.post(Uri.parse("$url/todos"), .post(Uri.parse("$url/todos"),
headers: headers as Map<String, String>, body: postData) headers: headers as Map<String, String>, body: postData)
.then((r) => json.decode(r.body)); .then((r) => json.decode(r.body));
var response = var response =
await client!.delete(Uri.parse("$url/todos/${created['id']}")); await client.delete(Uri.parse("$url/todos/${created['id']}"));
expect(response.statusCode, 200); expect(response.statusCode, 200);
var json_ = json.decode(response.body); var json_ = json.decode(response.body);
print(json_); print(json_);
@ -129,7 +126,7 @@ main() {
}); });
test('cannot remove all unless explicitly set', () async { 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); expect(response.statusCode, 403);
}); });
}); });

View file

@ -2,7 +2,7 @@ import 'dart:math';
import 'package:angel_route/angel_route.dart'; import 'package:angel_route/angel_route.dart';
main() { void main() {
final router = Router(); final router = Router();
router.get('/whois/~:user', () {}); router.get('/whois/~:user', () {});
@ -12,7 +12,7 @@ main() {
router.get('/ordinal/int:n([0-9]+)st', () {}); router.get('/ordinal/int:n([0-9]+)st', () {});
print(router.resolveAbsolute('/whois/~thosakwe').first.allParams); 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); print(router.resolveAbsolute('/ordinal/1st').first.allParams);
router.get('/users', () {}); router.get('/users', () {});

View file

@ -11,10 +11,10 @@ final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
/// A variation of the [Router] support both hash routing and push state. /// A variation of the [Router] support both hash routing and push state.
abstract class BrowserRouter<T> extends Router<T> { abstract class BrowserRouter<T> extends Router<T> {
/// Fires whenever the active route changes. Fires `null` if none is selected (404). /// Fires whenever the active route changes. Fires `null` if none is selected (404).
Stream<RoutingResult<T>?> get onResolve; Stream<RoutingResult<T>> get onResolve;
/// Fires whenever the active route changes. Fires `null` if none is selected (404). /// Fires whenever the active route changes. Fires `null` if none is selected (404).
Stream<Route<T>?> get onRoute; Stream<Route<T>> get onRoute;
/// Set `hash` to true to use hash routing instead of push state. /// Set `hash` to true to use hash routing instead of push state.
/// `listen` as `true` will call `listen` after initialization. /// `listen` as `true` will call `listen` after initialization.
@ -26,7 +26,7 @@ abstract class BrowserRouter<T> extends Router<T> {
BrowserRouter._() : super(); BrowserRouter._() : super();
void _goTo(String? path); void _goTo(String path);
/// Navigates to the path generated by calling /// Navigates to the path generated by calling
/// [navigate] with the given [linkParams]. /// [navigate] with the given [linkParams].
@ -41,26 +41,26 @@ abstract class BrowserRouter<T> extends Router<T> {
void listen(); void listen();
/// Identical to [all]. /// Identical to [all].
Route on(String path, T handler, {Iterable<T>? middleware}); Route on(String path, T handler, {Iterable<T> middleware});
} }
abstract class _BrowserRouterImpl<T> extends Router<T> abstract class _BrowserRouterImpl<T> extends Router<T>
implements BrowserRouter<T> { implements BrowserRouter<T> {
bool _listening = false; bool _listening = false;
Route? _current; Route? _current;
StreamController<RoutingResult<T>?> _onResolve = final StreamController<RoutingResult<T>> _onResolve =
StreamController<RoutingResult<T>?>(); StreamController<RoutingResult<T>>();
StreamController<Route<T>?> _onRoute = StreamController<Route<T>?>(); final StreamController<Route<T>> _onRoute = StreamController<Route<T>>();
Route? get currentRoute => _current; Route? get currentRoute => _current;
@override @override
Stream<RoutingResult<T>?> get onResolve => _onResolve.stream; Stream<RoutingResult<T>> get onResolve => _onResolve.stream;
@override @override
Stream<Route<T>?> get onRoute => _onRoute.stream; Stream<Route<T>> get onRoute => _onRoute.stream;
_BrowserRouterImpl({bool? listen}) : super() { _BrowserRouterImpl({bool listen = false}) : super() {
if (listen != false) this.listen(); if (listen != false) this.listen();
prepareAnchors(); prepareAnchors();
} }
@ -68,7 +68,9 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
@override @override
void go(Iterable linkParams) => _goTo(navigate(linkParams)); void go(Iterable linkParams) => _goTo(navigate(linkParams));
Route on(String path, T handler, {Iterable<T>? middleware}) => @override
Route on(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) =>
all(path, handler, middleware: middleware); all(path, handler, middleware: middleware);
void prepareAnchors() { void prepareAnchors() {
@ -76,14 +78,14 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
.querySelectorAll('a') .querySelectorAll('a')
.cast<AnchorElement>(); //:not([dynamic])'); .cast<AnchorElement>(); //:not([dynamic])');
for (final AnchorElement $a in anchors) { for (final $a in anchors) {
if ($a.attributes.containsKey('href') && if ($a.attributes.containsKey('href') &&
!$a.attributes.containsKey('download') && $a.attributes.containsKey('download') &&
!$a.attributes.containsKey('target') && $a.attributes.containsKey('target') &&
$a.attributes['rel'] != 'external') { $a.attributes['rel'] != 'external') {
$a.onClick.listen((e) { $a.onClick.listen((e) {
e.preventDefault(); e.preventDefault();
_goTo($a.attributes['href']); _goTo($a.attributes['href']!);
//go($a.attributes['href'].split('/').where((str) => str.isNotEmpty)); //go($a.attributes['href'].split('/').where((str) => str.isNotEmpty));
}); });
} }
@ -110,32 +112,36 @@ class _HashRouter<T> extends _BrowserRouterImpl<T> {
} }
@override @override
void _goTo(String? uri) { void _goTo(String uri) {
window.location.hash = '#$uri'; window.location.hash = '#$uri';
} }
void handleHash([_]) { void handleHash([_]) {
final path = window.location.hash.replaceAll(_hash, ''); final path = window.location.hash.replaceAll(_hash, '');
Iterable<RoutingResult<T>> allResolved = resolveAbsolute(path); var allResolved = resolveAbsolute(path);
final resolved = allResolved.isEmpty ? null : allResolved.first; if (allResolved.isEmpty) {
// TODO: Need fixing
if (resolved == null) { //_onResolve.add(null);
_onResolve.add(null); //_onRoute.add(_current = null);
_onRoute.add(_current = null); _current = null;
} else if (resolved != null && resolved.route != _current) { } else {
var resolved = allResolved.first;
if (resolved.route != _current) {
_onResolve.add(resolved); _onResolve.add(resolved);
_onRoute.add(_current = resolved.route); _onRoute.add(_current = resolved.route);
} }
} }
}
void handlePath(String path) { void handlePath(String path) {
final RoutingResult<T> resolved = resolveAbsolute(path).first; final resolved = resolveAbsolute(path).first;
if (resolved == null) { //if (resolved == null) {
_onResolve.add(null); // _onResolve.add(null);
_onRoute.add(_current = null); // _onRoute.add(_current = null);
} else if (resolved != null && resolved.route != _current) { //} else
if (resolved.route != _current) {
_onResolve.add(resolved); _onResolve.add(resolved);
_onRoute.add(_current = resolved.route); _onRoute.add(_current = resolved.route);
} }
@ -149,12 +155,12 @@ class _HashRouter<T> extends _BrowserRouterImpl<T> {
} }
class _PushStateRouter<T> extends _BrowserRouterImpl<T> { class _PushStateRouter<T> extends _BrowserRouterImpl<T> {
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; var $base = window.document.querySelector('base[href]') as BaseElement;
if ($base?.href?.isNotEmpty != true) { if ($base.href.isNotEmpty != true) {
throw StateError( throw StateError(
'You must have a <base href="<base-url-here>"> element present in your document to run the push state router.'); 'You must have a <base href="<base-url-here>"> element present in your document to run the push state router.');
} }
@ -163,42 +169,48 @@ class _PushStateRouter<T> extends _BrowserRouterImpl<T> {
} }
@override @override
void _goTo(String? uri) { void _goTo(String uri) {
final RoutingResult<T> resolved = resolveAbsolute(uri).first; final resolved = resolveAbsolute(uri).first;
var relativeUri = uri; var relativeUri = uri;
if (_basePath?.isNotEmpty == true) { if (_basePath.isNotEmpty) {
relativeUri = p.join(_basePath!, uri!.replaceAll(_straySlashes, '')); relativeUri = p.join(_basePath, uri.replaceAll(_straySlashes, ''));
} }
if (resolved == null) { //if (resolved == null) {
_onResolve.add(null); // _onResolve.add(null);
_onRoute.add(_current = null); // _onRoute.add(_current = null);
} else { //} else {
final route = resolved.route!; final route = resolved.route;
window.history.pushState({'path': route.path, 'params': {}}, var thisPath = route.name;
route.name ?? route.path, relativeUri); if (thisPath.isEmpty) {
thisPath = route.path;
}
window.history
.pushState({'path': route.path, 'params': {}}, thisPath, relativeUri);
_onResolve.add(resolved); _onResolve.add(resolved);
_onRoute.add(_current = route); _onRoute.add(_current = route);
} //}
} }
void handleState(state) { void handleState(state) {
if (state is Map && state.containsKey('path')) { if (state is Map && state.containsKey('path')) {
var path = state['path'].toString(); var path = state['path'].toString();
final RoutingResult<T> resolved = resolveAbsolute(path).first; final resolved = resolveAbsolute(path).first;
if (resolved != null && resolved.route != _current) { if (resolved.route != _current) {
//properties.addAll(state['properties'] ?? {}); //properties.addAll(state['properties'] ?? {});
_onResolve.add(resolved); _onResolve.add(resolved);
_onRoute.add(_current = resolved.route); _onRoute.add(_current = resolved.route);
} else { } else {
_onResolve.add(null); //_onResolve.add(null);
_onRoute.add(_current = null); //_onRoute.add(_current = null);
_current = null;
} }
} else { } else {
_onResolve.add(null); //_onResolve.add(null);
_onRoute.add(_current = null); //_onRoute.add(_current = null);
_current = null;
} }
} }

View file

@ -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; return r.value![1] == true ? OptionalSegment(s) : s;
}); });
@ -95,12 +97,12 @@ class RouteDefinition {
RouteDefinition(this.segments); RouteDefinition(this.segments);
Parser<RouteResult?>? compile() { Parser<RouteResult>? compile() {
Parser<RouteResult?>? out; Parser<RouteResult>? out;
for (int i = 0; i < segments.length; i++) { for (var i = 0; i < segments.length; i++) {
var s = segments[i]; var s = segments[i];
bool isLast = i == segments.length - 1; var isLast = i == segments.length - 1;
if (out == null) { if (out == null) {
out = s.compile(isLast); out = s.compile(isLast);
} else { } else {
@ -116,7 +118,7 @@ class RouteDefinition {
abstract class RouteSegment { abstract class RouteSegment {
Parser<RouteResult> compile(bool isLast); Parser<RouteResult> compile(bool isLast);
Parser<RouteResult?> compileNext(Parser<RouteResult> p, bool isLast); Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast);
} }
class SlashSegment implements RouteSegment { class SlashSegment implements RouteSegment {
@ -182,8 +184,15 @@ class WildcardSegment extends RouteSegment {
@override @override
Parser<RouteResult> compile(bool isLast) { Parser<RouteResult> compile(bool isLast) {
return match(_compile(isLast)) return match(_compile(isLast)).map((r) {
.map((r) => RouteResult({}, tail: r.scanner.lastMatch![1])); var result = r.scanner.lastMatch;
if (result != null) {
//return RouteResult({}, tail: r.scanner.lastMatch![1])
return RouteResult({}, tail: result[1]);
} else {
return RouteResult({});
}
});
} }
@override @override
@ -214,31 +223,53 @@ class OptionalSegment extends ParameterSegment {
} }
@override @override
Parser<RouteResult?> compileNext(Parser<RouteResult> p, bool isLast) { Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
return p.then(_compile().opt()).map((r) { return p.then(_compile().opt()).map((r) {
if (r.value![1] == null) return r.value![0] as RouteResult?; // Return an empty RouteResult if null
return (r.value![0] as RouteResult) if (r.value == null) {
..addAll({name: Uri.decodeComponent(r.value![1] as String)}); 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 { class ParameterSegment extends RouteSegment {
final String? name; final String name;
final RegExp? regExp; final RegExp? regExp;
ParameterSegment(this.name, this.regExp); ParameterSegment(this.name, this.regExp);
@override @override
String toString() { String toString() {
if (regExp != null) return 'Param: $name (${regExp!.pattern})'; if (regExp != null) {
return 'Param: $name (${regExp?.pattern})';
}
return 'Param: $name'; return 'Param: $name';
} }
Parser<String?> _compile() { Parser<String> _compile() {
return regExp != null if (regExp != null) {
? match<String?>(regExp!).value((r) => r.scanner.lastMatch![1]) return match<String>(regExp!).value((r) {
: RouteGrammar.notSlash; 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 @override
@ -248,7 +279,7 @@ class ParameterSegment extends RouteSegment {
} }
@override @override
Parser<RouteResult?> compileNext(Parser<RouteResult> p, bool isLast) { Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
return p.then(_compile()).map((r) { return p.then(_compile()).map((r) {
return (r.value![0] as RouteResult) return (r.value![0] as RouteResult)
..addAll({name: Uri.decodeComponent(r.value![1] as String)}); ..addAll({name: Uri.decodeComponent(r.value![1] as String)});

View file

@ -2,21 +2,28 @@ part of angel_route.src.router;
/// Represents a virtual location within an application. /// Represents a virtual location within an application.
class Route<T> { class Route<T> {
final String? method; String method;
final String path; String path;
final List<T>? handlers;
final Map<String, Map<String, dynamic>> _cache = {}; final Map<String, Map<String, dynamic>> _cache = {};
final RouteDefinition? _routeDefinition; String name = '';
String? name; Parser<RouteResult>? _parser;
Parser<RouteResult?>? _parser; late RouteDefinition _routeDefinition;
late List<T> handlers;
Route(this.path, {required this.method, required this.handlers}) Route(this.path, {required this.method, required this.handlers}) {
: _routeDefinition = RouteGrammar.routeDefinition var result = RouteGrammar.routeDefinition
.parse(SpanScanner(path.replaceAll(_straySlashes, '')))! .parse(SpanScanner(path.replaceAll(_straySlashes, '')));
.value {
if (_routeDefinition?.segments.isNotEmpty != true) { 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({})); _parser = match('').map((r) => RouteResult({}));
} }
} else {
//print('[Route] Failed to create route for $path');
}
} }
factory Route.join(Route<T> a, Route<T> b) { factory Route.join(Route<T> a, Route<T> b) {
@ -26,7 +33,9 @@ class Route<T> {
method: b.method, handlers: b.handlers); method: b.method, handlers: b.handlers);
} }
Parser<RouteResult?>? get parser => _parser ??= _routeDefinition!.compile(); //List<T> get handlers => _handlers;
Parser<RouteResult>? get parser => _parser ??= _routeDefinition.compile();
@override @override
String toString() { String toString() {
@ -40,9 +49,9 @@ class Route<T> {
String makeUri(Map<String, dynamic> params) { String makeUri(Map<String, dynamic> params) {
var b = StringBuffer(); 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 (i++ > 0) b.write('/');
if (seg is ConstantSegment) { if (seg is ConstantSegment) {
b.write(seg.text); b.write(seg.text);
@ -50,7 +59,7 @@ class Route<T> {
if (!params.containsKey(seg.name)) { if (!params.containsKey(seg.name)) {
throw ArgumentError('Missing parameter "${seg.name}".'); throw ArgumentError('Missing parameter "${seg.name}".');
} }
b.write(params[seg.name!]); b.write(params[seg.name]);
} }
} }
@ -61,7 +70,7 @@ class Route<T> {
/// The result of matching an individual route. /// The result of matching an individual route.
class RouteResult { class RouteResult {
/// The parsed route parameters. /// The parsed route parameters.
final Map<String?, dynamic> params; final Map<String, dynamic> params;
/// Optional. An explicit "tail" value to set. /// Optional. An explicit "tail" value to set.
String? get tail => _tail; String? get tail => _tail;
@ -73,7 +82,7 @@ class RouteResult {
void _setTail(String? v) => _tail ??= v; void _setTail(String? v) => _tail ??= v;
/// Adds parameters. /// Adds parameters.
void addAll(Map<String?, dynamic> map) { void addAll(Map<String, dynamic> map) {
params.addAll(map); params.addAll(map);
} }
} }

View file

@ -2,7 +2,6 @@ library angel_route.src.router;
import 'dart:async'; import 'dart:async';
import 'package:combinator/combinator.dart'; import 'package:combinator/combinator.dart';
import 'package:meta/meta.dart';
import 'package:string_scanner/string_scanner.dart'; import 'package:string_scanner/string_scanner.dart';
import '../string_util.dart'; import '../string_util.dart';
@ -65,9 +64,8 @@ class Router<T> {
/// Adds a route that responds to the given path /// Adds a route that responds to the given path
/// for requests with the given method (case-insensitive). /// for requests with the given method (case-insensitive).
/// Provide '*' as the method to respond to all methods. /// Provide '*' as the method to respond to all methods.
Route<T> addRoute(String? method, String path, T handler, Route<T> addRoute(String method, String path, T handler,
{Iterable<T>? middleware}) { {Iterable<T>? middleware}) {
middleware ??= <T>[];
if (_useCache == true) { if (_useCache == true) {
throw StateError('Cannot add routes after caching is enabled.'); throw StateError('Cannot add routes after caching is enabled.');
} }
@ -75,7 +73,10 @@ class Router<T> {
// Check if any mounted routers can match this // Check if any mounted routers can match this
final handlers = <T>[handler]; final handlers = <T>[handler];
if (middleware != null) handlers.insertAll(0, middleware); //middleware = <T>[];
middleware ??= <T>[];
handlers.insertAll(0, middleware);
final route = Route<T>(path, method: method, handlers: handlers); final route = Route<T>(path, method: method, handlers: handlers);
_routes.add(route); _routes.add(route);
@ -115,29 +116,29 @@ class Router<T> {
/// Creates a visual representation of the route hierarchy and /// Creates a visual representation of the route hierarchy and
/// passes it to a callback. If none is provided, `print` is called. /// passes it to a callback. If none is provided, `print` is called.
void dumpTree( void dumpTree(
{callback(String tree)?, {Function(String tree)? callback,
String header = 'Dumping route tree:', String header = 'Dumping route tree:',
String tab = ' '}) { String tab = ' '}) {
final buf = StringBuffer(); final buf = StringBuffer();
int tabs = 0; var tabs = 0;
if (header != null && header.isNotEmpty) { if (header.isNotEmpty) {
buf.writeln(header); buf.writeln(header);
} }
buf.writeln('<root>'); buf.writeln('<root>');
indent() { void indent() {
for (int i = 0; i < tabs; i++) { for (var i = 0; i < tabs; i++) {
buf.write(tab); buf.write(tab);
} }
} }
dumpRouter(Router router) { void dumpRouter(Router router) {
indent(); indent();
tabs++; tabs++;
for (Route route in router.routes) { for (var route in router.routes) {
indent(); indent();
buf.write('- '); buf.write('- ');
if (route is! SymlinkRoute) buf.write('${route.method} '); if (route is! SymlinkRoute) buf.write('${route.method} ');
@ -147,7 +148,7 @@ class Router<T> {
buf.writeln(); buf.writeln();
dumpRouter(route.router); dumpRouter(route.router);
} else { } else {
buf.writeln(' => ${route.handlers!.length} handler(s)'); buf.writeln(' => ${route.handlers.length} handler(s)');
} }
} }
@ -164,9 +165,8 @@ class Router<T> {
/// ///
/// Returns the created route. /// Returns the created route.
/// You can also register middleware within the router. /// You can also register middleware within the router.
SymlinkRoute<T> group(String path, void callback(Router<T> router), SymlinkRoute<T> group(String path, void Function(Router<T> router) callback,
{Iterable<T>? middleware, String? name}) { {Iterable<T> middleware = const Iterable.empty(), String name = ''}) {
middleware ??= <T>[];
final router = Router<T>().._middleware.addAll(middleware); final router = Router<T>().._middleware.addAll(middleware);
callback(router); callback(router);
return mount(path, router)..name = name; return mount(path, router)..name = name;
@ -174,9 +174,9 @@ class Router<T> {
/// Asynchronous equivalent of [group]. /// Asynchronous equivalent of [group].
Future<SymlinkRoute<T>> groupAsync( Future<SymlinkRoute<T>> groupAsync(
String path, FutureOr<void> callback(Router<T> router), String path, FutureOr<void> Function(Router<T> router) callback,
{Iterable<T>? middleware, String? name}) async { {Iterable<T> middleware = const Iterable.empty(),
middleware ??= <T>[]; String name = ''}) async {
final router = Router<T>().._middleware.addAll(middleware); final router = Router<T>().._middleware.addAll(middleware);
await callback(router); await callback(router);
return mount(path, router)..name = name; return mount(path, router)..name = name;
@ -210,16 +210,16 @@ class Router<T> {
/// router.navigate(['users/:id', {'id': '1337'}, 'profile']); /// router.navigate(['users/:id', {'id': '1337'}, 'profile']);
/// ``` /// ```
String navigate(Iterable linkParams, {bool absolute = true}) { String navigate(Iterable linkParams, {bool absolute = true}) {
final List<String> segments = []; final segments = <String>[];
Router search = this; Router search = this;
Route? lastRoute; Route? lastRoute;
for (final param in linkParams) { for (final param in linkParams) {
bool resolved = false; var resolved = false;
if (param is String) { if (param is String) {
// Search by name // Search by name
for (Route route in search.routes) { for (var route in search.routes) {
if (route.name == param) { if (route.name == param) {
segments.add(route.path.replaceAll(_straySlashes, '')); segments.add(route.path.replaceAll(_straySlashes, ''));
lastRoute = route; lastRoute = route;
@ -236,9 +236,11 @@ class Router<T> {
// Search by path // Search by path
if (!resolved) { if (!resolved) {
var scanner = SpanScanner(param.replaceAll(_straySlashes, '')); var scanner = SpanScanner(param.replaceAll(_straySlashes, ''));
for (Route route in search.routes) { for (var route in search.routes) {
int pos = scanner.position; var pos = scanner.position;
if (route.parser!.parse(scanner)!.successful && scanner.isDone) { var parseResult = route.parser?.parse(scanner);
if (parseResult != null) {
if (parseResult.successful && scanner.isDone) {
segments.add(route.path.replaceAll(_straySlashes, '')); segments.add(route.path.replaceAll(_straySlashes, ''));
lastRoute = route; lastRoute = route;
@ -251,6 +253,9 @@ class Router<T> {
} else { } else {
scanner.position = pos; scanner.position = pos;
} }
} else {
scanner.position = pos;
}
} }
} }
@ -281,39 +286,44 @@ class Router<T> {
/// Finds the first [Route] that matches the given path, /// Finds the first [Route] that matches the given path,
/// with the given method. /// with the given method.
bool resolve(String? absolute, String? relative, List<RoutingResult<T>> out, bool resolve(String absolute, String relative, List<RoutingResult<T>> out,
{String method = 'GET', bool strip = true}) { {String method = 'GET', bool strip = true}) {
final cleanRelative = final cleanRelative =
strip == false ? relative! : stripStraySlashes(relative!); strip == false ? relative : stripStraySlashes(relative);
var scanner = SpanScanner(cleanRelative); var scanner = SpanScanner(cleanRelative);
bool crawl(Router<T> r) { bool crawl(Router<T> r) {
bool success = false; var success = false;
for (var route in r.routes) { for (var route in r.routes) {
int pos = scanner.position; var pos = scanner.position;
if (route is SymlinkRoute<T>) { if (route is SymlinkRoute<T>) {
if (route.parser!.parse(scanner)!.successful) { if (route.parser != null) {
var pp = route.parser!;
if (pp.parse(scanner).successful) {
var s = crawl(route.router); var s = crawl(route.router);
if (s) success = true; if (s) success = true;
} }
}
scanner.position = pos; scanner.position = pos;
} else if (route.method == '*' || route.method == method) { } else if (route.method == '*' || route.method == method) {
var parseResult = route.parser!.parse(scanner)!; var parseResult = route.parser?.parse(scanner);
if (parseResult != null) {
if (parseResult.successful && scanner.isDone) { if (parseResult.successful && scanner.isDone) {
var tailResult = parseResult.value?.tail ?? '';
print(tailResult);
var result = RoutingResult<T>( var result = RoutingResult<T>(
parseResult: parseResult, parseResult: parseResult,
params: parseResult.value!.params, params: parseResult.value!.params,
shallowRoute: route, shallowRoute: route,
shallowRouter: this, shallowRouter: this,
tail: (parseResult.value!.tail ?? '') + scanner.rest); tail: tailResult + scanner.rest);
out.add(result); out.add(result);
success = true; success = true;
} }
}
scanner.position = pos; scanner.position = pos;
} }
} }
@ -326,13 +336,13 @@ class Router<T> {
/// Returns the result of [resolve] with [path] passed as /// Returns the result of [resolve] with [path] passed as
/// both `absolute` and `relative`. /// both `absolute` and `relative`.
Iterable<RoutingResult<T>> resolveAbsolute(String? path, Iterable<RoutingResult<T>> resolveAbsolute(String path,
{String method = 'GET', bool strip = true}) => {String method = 'GET', bool strip = true}) =>
resolveAll(path, path, method: method, strip: strip); resolveAll(path, path, method: method, strip: strip);
/// Finds every possible [Route] that matches the given path, /// Finds every possible [Route] that matches the given path,
/// with the given method. /// with the given method.
Iterable<RoutingResult<T>> resolveAll(String? absolute, String? relative, Iterable<RoutingResult<T>> resolveAll(String absolute, String relative,
{String method = 'GET', bool strip = true}) { {String method = 'GET', bool strip = true}) {
if (_useCache == true) { if (_useCache == true) {
return _cache.putIfAbsent('$method$absolute', return _cache.putIfAbsent('$method$absolute',
@ -342,7 +352,7 @@ class Router<T> {
return _resolveAll(absolute, relative, method: method, strip: strip); return _resolveAll(absolute, relative, method: method, strip: strip);
} }
Iterable<RoutingResult<T>> _resolveAll(String? absolute, String? relative, Iterable<RoutingResult<T>> _resolveAll(String absolute, String relative,
{String method = 'GET', bool strip = true}) { {String method = 'GET', bool strip = true}) {
var results = <RoutingResult<T>>[]; var results = <RoutingResult<T>>[];
resolve(absolute, relative, results, method: method, strip: strip); resolve(absolute, relative, results, method: method, strip: strip);
@ -363,88 +373,95 @@ class Router<T> {
} }
/// Adds a route that responds to any request matching the given path. /// Adds a route that responds to any request matching the given path.
Route<T> all(String path, T handler, {Iterable<T>? middleware}) { Route<T> all(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('*', path, handler, middleware: middleware); return addRoute('*', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a DELETE request. /// Adds a route that responds to a DELETE request.
Route<T> delete(String path, T handler, {Iterable<T>? middleware}) { Route<T> delete(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('DELETE', path, handler, middleware: middleware); return addRoute('DELETE', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a GET request. /// Adds a route that responds to a GET request.
Route<T> get(String path, T handler, {Iterable<T>? middleware}) { Route<T> get(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('GET', path, handler, middleware: middleware); return addRoute('GET', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a HEAD request. /// Adds a route that responds to a HEAD request.
Route<T> head(String path, T handler, {Iterable<T>? middleware}) { Route<T> head(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('HEAD', path, handler, middleware: middleware); return addRoute('HEAD', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a OPTIONS request. /// Adds a route that responds to a OPTIONS request.
Route<T> options(String path, T handler, {Iterable<T>? middleware}) { Route<T> options(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('OPTIONS', path, handler, middleware: middleware); return addRoute('OPTIONS', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a POST request. /// Adds a route that responds to a POST request.
Route<T> post(String path, T handler, {Iterable<T>? middleware}) { Route<T> post(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('POST', path, handler, middleware: middleware); return addRoute('POST', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a PATCH request. /// Adds a route that responds to a PATCH request.
Route<T> patch(String path, T handler, {Iterable<T>? middleware}) { Route<T> patch(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('PATCH', path, handler, middleware: middleware); return addRoute('PATCH', path, handler, middleware: middleware);
} }
/// Adds a route that responds to a PUT request. /// Adds a route that responds to a PUT request.
Route put(String path, T handler, {Iterable<T>? middleware}) { Route put(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) {
return addRoute('PUT', path, handler, middleware: middleware); return addRoute('PUT', path, handler, middleware: middleware);
} }
} }
class _ChainedRouter<T> extends Router<T> { class _ChainedRouter<T> extends Router<T> {
final List<T> _handlers = <T>[]; final List<T> _handlers = <T>[];
Router? _root; Router _root;
_ChainedRouter.empty(); _ChainedRouter.empty() : _root = Router();
_ChainedRouter(Router? root, Iterable<T> middleware) { _ChainedRouter(this._root, Iterable<T> middleware) {
this._root = root;
_handlers.addAll(middleware); _handlers.addAll(middleware);
} }
@override @override
Route<T> addRoute(String? method, String path, handler, Route<T> addRoute(String method, String path, handler,
{Iterable<T>? middleware}) { {Iterable<T>? middleware}) {
Route<T> route = super.addRoute(method, path, handler, middleware ??= <T>[];
middleware: []..addAll(_handlers)..addAll(middleware ?? [])); var route = super.addRoute(method, path, handler,
middleware: [..._handlers, ...middleware]);
//_root._routes.add(route); //_root._routes.add(route);
return route; return route;
} }
@override @override
SymlinkRoute<T> group(String path, void callback(Router<T> router), SymlinkRoute<T> group(String path, void Function(Router<T> router) callback,
{Iterable<T>? middleware, String? name}) { {Iterable<T> middleware = const Iterable.empty(), String name = ''}) {
final router = _ChainedRouter<T>( final router = _ChainedRouter<T>(_root, [..._handlers, ...middleware]);
_root, []..addAll(_handlers)..addAll(middleware ?? []));
callback(router); callback(router);
return mount(path, router)..name = name; return mount(path, router)..name = name;
} }
@override @override
Future<SymlinkRoute<T>> groupAsync( Future<SymlinkRoute<T>> groupAsync(
String path, FutureOr<void> callback(Router<T> router), String path, FutureOr<void> Function(Router<T> router) callback,
{Iterable<T>? middleware, String? name}) async { {Iterable<T> middleware = const Iterable.empty(),
final router = _ChainedRouter<T>( String name = ''}) async {
_root, []..addAll(_handlers)..addAll(middleware ?? [])); final router = _ChainedRouter<T>(_root, [..._handlers, ...middleware]);
await callback(router); await callback(router);
return mount(path, router)..name = name; return mount(path, router)..name = name;
} }
@override @override
SymlinkRoute<T> mount(String path, Router<T> router) { SymlinkRoute<T> mount(String path, Router<T> router) {
final SymlinkRoute<T> route = super.mount(path, router); final route = super.mount(path, router);
route.router._middleware.insertAll(0, _handlers); route.router._middleware.insertAll(0, _handlers);
//_root._routes.add(route); //_root._routes.add(route);
return route; return route;
@ -453,7 +470,7 @@ class _ChainedRouter<T> extends Router<T> {
@override @override
_ChainedRouter<T> chain(Iterable<T> middleware) { _ChainedRouter<T> chain(Iterable<T> middleware) {
final piped = _ChainedRouter<T>.empty().._root = _root; final piped = _ChainedRouter<T>.empty().._root = _root;
piped._handlers.addAll([]..addAll(_handlers)..addAll(middleware)); piped._handlers.addAll([..._handlers, ...middleware]);
var route = SymlinkRoute<T>('/', piped); var route = SymlinkRoute<T>('/', piped);
_routes.add(route); _routes.add(route);
return piped; return piped;
@ -473,13 +490,13 @@ Router<T> flatten<T>(Router<T> router) {
var path = route.path.replaceAll(_straySlashes, ''); var path = route.path.replaceAll(_straySlashes, '');
var joined = '$base/$path'.replaceAll(_straySlashes, ''); var joined = '$base/$path'.replaceAll(_straySlashes, '');
flattened.addRoute(route.method, joined.replaceAll(_straySlashes, ''), flattened.addRoute(route.method, joined.replaceAll(_straySlashes, ''),
route.handlers!.last, route.handlers.last,
middleware: middleware:
route.handlers!.take(route.handlers!.length - 1).toList()); route.handlers.take(route.handlers.length - 1).toList());
} }
} else { } else {
flattened.addRoute(route.method, route.path, route.handlers!.last, flattened.addRoute(route.method, route.path, route.handlers.last,
middleware: route.handlers!.take(route.handlers!.length - 1).toList()); middleware: route.handlers.take(route.handlers.length - 1).toList());
} }
} }

View file

@ -3,23 +3,23 @@ part of angel_route.src.router;
/// Represents a complex result of navigating to a path. /// Represents a complex result of navigating to a path.
class RoutingResult<T> { class RoutingResult<T> {
/// The parse result that matched the given sub-path. /// The parse result that matched the given sub-path.
final ParseResult<RouteResult?>? parseResult; final ParseResult<RouteResult> parseResult;
/// A nested instance, if a sub-path was matched. /// A nested instance, if a sub-path was matched.
final Iterable<RoutingResult<T>>? nested; final Iterable<RoutingResult<T>> nested;
/// All route params matching this route on the current sub-path. /// All route params matching this route on the current sub-path.
final Map<String?, dynamic> params = {}; final Map<String, dynamic> params = {};
/// The [Route] that answered this sub-path. /// The [Route] that answered this sub-path.
/// ///
/// This is mostly for internal use, and useless in production. /// This is mostly for internal use, and useless in production.
final Route<T>? shallowRoute; final Route<T> shallowRoute;
/// The [Router] that answered this sub-path. /// The [Router] that answered this sub-path.
/// ///
/// Only really for internal use. /// Only really for internal use.
final Router<T>? shallowRouter; final Router<T> shallowRouter;
/// The remainder of the full path that was not matched, and was passed to [nested] routes. /// The remainder of the full path that was not matched, and was passed to [nested] routes.
final String tail; final String tail;
@ -28,22 +28,22 @@ class RoutingResult<T> {
RoutingResult<T> get deepest { RoutingResult<T> get deepest {
var search = this; var search = this;
while (search.nested?.isNotEmpty == true) { while (search.nested.isNotEmpty == true) {
search = search.nested!.first; search = search.nested.first;
} }
return search; return search;
} }
/// The most specific route. /// The most specific route.
Route<T>? get route => deepest.shallowRoute; Route<T> get route => deepest.shallowRoute;
/// The most specific router. /// The most specific router.
Router<T>? get router => deepest.shallowRouter; Router<T> get router => deepest.shallowRouter;
/// The handlers at this sub-path. /// The handlers at this sub-path.
List<T> get handlers { List<T> get handlers {
return <T>[...shallowRouter!.middleware, ...shallowRoute!.handlers!]; return <T>[...shallowRouter.middleware, ...shallowRoute.handlers];
} }
/// All handlers on this sub-path and its children. /// All handlers on this sub-path and its children.
@ -53,8 +53,8 @@ class RoutingResult<T> {
void crawl(RoutingResult<T> result) { void crawl(RoutingResult<T> result) {
handlers.addAll(result.handlers); handlers.addAll(result.handlers);
if (result.nested?.isNotEmpty == true) { if (result.nested.isNotEmpty == true) {
for (var r in result.nested!) { for (var r in result.nested) {
crawl(r); crawl(r);
} }
} }
@ -66,14 +66,14 @@ class RoutingResult<T> {
} }
/// All parameters on this sub-path and its children. /// All parameters on this sub-path and its children.
Map<String?, dynamic> get allParams { Map<String, dynamic> get allParams {
final params = <String?, dynamic>{}; final params = <String, dynamic>{};
void crawl(RoutingResult result) { void crawl(RoutingResult result) {
params.addAll(result.params); params.addAll(result.params);
if (result.nested?.isNotEmpty == true) { if (result.nested.isNotEmpty == true) {
for (var r in result.nested!) { for (var r in result.nested) {
crawl(r); crawl(r);
} }
} }
@ -84,11 +84,11 @@ class RoutingResult<T> {
} }
RoutingResult( RoutingResult(
{this.parseResult, {required this.parseResult,
Map<String?, dynamic> params = const {}, Map<String, dynamic> params = const {},
this.nested, this.nested = const Iterable.empty(),
this.shallowRoute, required this.shallowRoute,
this.shallowRouter, required this.shallowRouter,
required this.tail}) { required this.tail}) {
this.params.addAll(params); this.params.addAll(params);
} }

View file

@ -5,5 +5,5 @@ part of angel_route.src.router;
class SymlinkRoute<T> extends Route<T> { class SymlinkRoute<T> extends Route<T> {
final Router<T> router; final Router<T> router;
SymlinkRoute(String path, this.router) SymlinkRoute(String path, this.router)
: super(path, method: null, handlers: null); : super(path, method: 'GET', handlers: <T>[]);
} }

View file

@ -17,7 +17,7 @@ String stripStray(String haystack, String needle) {
} }
// Find last leading index of slash // 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) { if (haystack[i] != needle) {
var sub = haystack.substring(i); var sub = haystack.substring(i);
@ -25,7 +25,7 @@ String stripStray(String haystack, String needle) {
var lastSlash = sub.lastIndexOf(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) { if (sub[j] != needle) {
return sub.substring(0, j + 1); return sub.substring(0, j + 1);
} }

View file

@ -11,7 +11,7 @@ void main() {
..dumpTree(); ..dumpTree();
test('nested route groups with chain', () { 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, isNotNull);
expect(r.handlers, hasLength(4)); expect(r.handlers, hasLength(4));
expect(r.handlers, equals(['a', 'c', 'd', 'g'])); expect(r.handlers, equals(['a', 'c', 'd', 'g']));

View file

@ -8,7 +8,7 @@ void main() {
router.get('/user/:id', 'GET'); router.get('/user/:id', 'GET');
router.get('/first/:first/last/:last', 'GET').name = 'full_name'; router.get('/first/:first/last/:last', 'GET').name = 'full_name';
navigate(params) { String navigate(params) {
final uri = router.navigate(params as Iterable); final uri = router.navigate(params as Iterable);
print('Uri: $uri'); print('Uri: $uri');
return uri; return uri;

View file

@ -26,21 +26,21 @@ void main() {
test('tail explicitly set intermediate', () { test('tail explicitly set intermediate', () {
var results = router.resolveAbsolute('/songs/in_the/key'); var results = router.resolveAbsolute('/songs/in_the/key');
var result = results.first; 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'); expect(result.tail, 'in_the');
}); });
test('tail explicitly set at end', () { test('tail explicitly set at end', () {
var results = router.resolveAbsolute('/isnt/she/epic'); var results = router.resolveAbsolute('/isnt/she/epic');
var result = results.first; 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'); expect(result.tail, 'epic');
}); });
test('tail with trailing', () { test('tail with trailing', () {
var results = router.resolveAbsolute('/isnt/she/epic/fail'); var results = router.resolveAbsolute('/isnt/she/epic/fail');
var result = results.first; 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'); expect(result.tail, 'epic/fail');
}); });
} }

View file

@ -1,25 +1,30 @@
import 'dart:html'; import 'dart:html';
import 'package:angel_route/browser.dart'; import 'package:angel_route/browser.dart';
basic(BrowserRouter router) { void basic(BrowserRouter router) {
final $h1 = window.document.querySelector('h1'); final $h1 = window.document.querySelector('h1');
final $ul = window.document.getElementById('handlers'); final $ul = window.document.getElementById('handlers');
router.onResolve.listen((result) { router.onResolve.listen((result) {
final route = result?.route; final route = result.route;
if (route == null) { // TODO: Relook at this logic
$h1!.text = 'No Active Route'; //if (route == null) {
$ul!.children // $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() ..clear()
..add(LIElement()..text = '(empty)'); ..addAll(result.allHandlers
} else {
$h1!.text = 'Active Route: ${route.name ?? route.path}';
$ul!.children
..clear()
..addAll(result!.allHandlers
.map((handler) => LIElement()..text = handler.toString())); .map((handler) => LIElement()..text = handler.toString()));
} else {
print('No active Route');
} }
//}
}); });
router.get('a', 'a handler'); router.get('a', 'a handler');