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_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<Request>,
RequestContextType extends RequestContext,
ResponseContextType extends ResponseContext> {
final Angel? app;
final Angel app;
final bool useZone;
bool _closed = false;
Server? _server;
late Server _server;
StreamSubscription<Request>? _sub;
final log = Logger('Driver');
/// The function used to bind this instance to a server..
final Future<Server> 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<Server> generateServer(address, int port) =>
serverGenerator(address, port);
/// 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';
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<Server> 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<RequestContextType> createRequestContext(
@ -102,7 +109,7 @@ abstract class Driver<
Tuple4<List?, Map<String?, dynamic>, ParseResult<RouteResult?>?,
MiddlewarePipeline> resolveTuple() {
var r = app!.optimizedRouter;
var r = app.optimizedRouter;
var resolved =
r.resolveAbsolute(path, method: req.method!, strip: false);
var pipeline = MiddlewarePipeline<RequestHandler?>(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<RequestHandler?>;
var it = MiddlewarePipelineIterator<RequestHandler?>(line);
@ -134,7 +141,7 @@ abstract class Driver<
..registerSingleton<ParseResult<RouteResult?>?>(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());
}
@ -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<void> _cleanup(_) {
if (!app!.environment.isProduction &&
app!.logger != null &&
if (app.environment.isProduction &&
app.logger != null &&
req.container!.has<Stopwatch>()) {
var sw = req.container!.make<Stopwatch>();
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<RequestHandler?> 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;

View file

@ -30,7 +30,7 @@ RequestHandler chain(Iterable<RequestHandler> 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<RequestHandler> handlers) {
}
/// 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?> _serviceLookups = {};
final Map configuration = {};
@ -92,10 +92,9 @@ class Routable extends Router<RequestHandler?> {
}
@override
Route<RequestHandler?> addRoute(
String? method, String? path, RequestHandler? handler,
{Iterable<RequestHandler?>? middleware}) {
middleware ??= [];
Route<RequestHandler> addRoute(
String method, String path, RequestHandler handler,
{Iterable<RequestHandler> middleware = const Iterable.empty()}) {
final handlers = <RequestHandler>[];
// Merge @Middleware declaration, if any
var reflector = _container?.reflector;
@ -108,10 +107,7 @@ class Routable extends Router<RequestHandler?> {
}
final handlerSequence = <RequestHandler>[];
handlerSequence.addAll(middleware as Iterable<
FutureOr<dynamic>? Function(
RequestContext<dynamic>, ResponseContext<dynamic>)>? ??
[]);
handlerSequence.addAll(middleware);
handlerSequence.addAll(handlers);
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,
HttpRequestContext, HttpResponseContext> {
@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<HttpServer> 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<HttpRequest, HttpResponse, HttpServer,
var serverContext = SecurityContext();
serverContext.useCertificateChain(certificateChain, password: password);
serverContext.usePrivateKey(serverKey, password: password);
return AngelHttp.fromSecurityContext(app, serverContext, useZone: useZone);
}
/// Use [server] instead.
@deprecated
HttpServer? get httpServer => 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<HttpRequest, HttpResponse, HttpServer,
@override
Future<HttpServer> close() async {
await server?.close();
return await super.close();
}
@ -87,7 +96,7 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpRequest request, HttpResponse response) {
var path = request.uri.path.replaceAll(_straySlashes, '');
if (path.isEmpty) path = '/';
return HttpRequestContext.from(request, app!, path);
return HttpRequestContext.from(request, app, path);
}
@override
@ -98,10 +107,10 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpResponseContext context =
HttpResponseContext(response, app, correspondingRequest);
if (app!.serializer == null) {
if (app.serializer == null) {
context.serializer = json.encode;
} else {
context.serializer = app!.serializer;
context.serializer = app.serializer;
}
return Future<HttpResponseContext>.value(context);
// return Future<HttpResponseContext>.value(

View file

@ -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<TodoController>()) {
app!.container!.registerSingleton(todoController = TodoController());
if (!app.container!.has<TodoController>()) {
app.container!.registerSingleton(todoController = TodoController());
}
// 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.
// 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<RequestHandler>();
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;
});

View file

@ -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<Future<Foo>>((container) async {
app.container!.registerSingleton(Todo(text: TEXT, over: OVER));
app.container!.registerFactory<Future<Foo>>((container) async {
var req = container.make<RequestContext>()!;
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'}));
});
}

View file

@ -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'}));

View file

@ -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<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}');
});
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<String, String>);
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<MapService>()));
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<dynamic, dynamic, Service<dynamic, dynamic>>;
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<Providers>());
expect(e.params?.keys, contains('provider'));
expect(e.params?['provider'], const IsInstanceOf<Providers>());
}
svc

View file

@ -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<Map>());
@ -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 = <String, String>{'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<String, String>, 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"');
});

View file

@ -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'));
});

View file

@ -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<String, String>, 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<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);
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<String, String>, 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<String, String>, 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<String, String>, 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);
});
});

View file

@ -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', () {});

View file

@ -11,10 +11,10 @@ final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
/// A variation of the [Router] support both hash routing and push state.
abstract class BrowserRouter<T> extends Router<T> {
/// 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).
Stream<Route<T>?> get onRoute;
Stream<Route<T>> 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<T> extends Router<T> {
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<T> extends Router<T> {
void listen();
/// 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>
implements BrowserRouter<T> {
bool _listening = false;
Route? _current;
StreamController<RoutingResult<T>?> _onResolve =
StreamController<RoutingResult<T>?>();
StreamController<Route<T>?> _onRoute = StreamController<Route<T>?>();
final StreamController<RoutingResult<T>> _onResolve =
StreamController<RoutingResult<T>>();
final StreamController<Route<T>> _onRoute = StreamController<Route<T>>();
Route? get currentRoute => _current;
@override
Stream<RoutingResult<T>?> get onResolve => _onResolve.stream;
Stream<RoutingResult<T>> get onResolve => _onResolve.stream;
@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();
prepareAnchors();
}
@ -68,7 +68,9 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
@override
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);
void prepareAnchors() {
@ -76,14 +78,14 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
.querySelectorAll('a')
.cast<AnchorElement>(); //: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<T> extends _BrowserRouterImpl<T> {
}
@override
void _goTo(String? uri) {
void _goTo(String uri) {
window.location.hash = '#$uri';
}
void handleHash([_]) {
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 (resolved == null) {
_onResolve.add(null);
_onRoute.add(_current = null);
} else if (resolved != null && resolved.route != _current) {
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<T> 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<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;
if ($base?.href?.isNotEmpty != true) {
if ($base.href.isNotEmpty != true) {
throw StateError(
'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
void _goTo(String? uri) {
final RoutingResult<T> 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);
//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<T> 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;
}
}

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;
});
@ -95,12 +97,12 @@ class RouteDefinition {
RouteDefinition(this.segments);
Parser<RouteResult?>? compile() {
Parser<RouteResult?>? out;
Parser<RouteResult>? compile() {
Parser<RouteResult>? 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<RouteResult> compile(bool isLast);
Parser<RouteResult?> compileNext(Parser<RouteResult> p, bool isLast);
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast);
}
class SlashSegment implements RouteSegment {
@ -182,8 +184,15 @@ class WildcardSegment extends RouteSegment {
@override
Parser<RouteResult> 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<RouteResult?> compileNext(Parser<RouteResult> p, bool isLast) {
Parser<RouteResult> compileNext(Parser<RouteResult> 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<String?> _compile() {
return regExp != null
? match<String?>(regExp!).value((r) => r.scanner.lastMatch![1])
: RouteGrammar.notSlash;
Parser<String> _compile() {
if (regExp != null) {
return match<String>(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<RouteResult?> compileNext(Parser<RouteResult> p, bool isLast) {
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
return p.then(_compile()).map((r) {
return (r.value![0] as RouteResult)
..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.
class Route<T> {
final String? method;
final String path;
final List<T>? handlers;
String method;
String path;
final Map<String, Map<String, dynamic>> _cache = {};
final RouteDefinition? _routeDefinition;
String? name;
Parser<RouteResult?>? _parser;
String name = '';
Parser<RouteResult>? _parser;
late RouteDefinition _routeDefinition;
late List<T> handlers;
Route(this.path, {required this.method, required this.handlers})
: _routeDefinition = RouteGrammar.routeDefinition
.parse(SpanScanner(path.replaceAll(_straySlashes, '')))!
.value {
if (_routeDefinition?.segments.isNotEmpty != true) {
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');
}
}
factory Route.join(Route<T> a, Route<T> b) {
@ -26,7 +33,9 @@ class Route<T> {
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
String toString() {
@ -40,9 +49,9 @@ class Route<T> {
String makeUri(Map<String, dynamic> 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<T> {
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<T> {
/// The result of matching an individual route.
class RouteResult {
/// The parsed route parameters.
final Map<String?, dynamic> params;
final Map<String, dynamic> 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<String?, dynamic> map) {
void addAll(Map<String, dynamic> map) {
params.addAll(map);
}
}

View file

@ -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<T> {
/// 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<T> addRoute(String? method, String path, T handler,
Route<T> addRoute(String method, String path, T handler,
{Iterable<T>? middleware}) {
middleware ??= <T>[];
if (_useCache == true) {
throw StateError('Cannot add routes after caching is enabled.');
}
@ -75,7 +73,10 @@ class Router<T> {
// Check if any mounted routers can match this
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);
_routes.add(route);
@ -115,29 +116,29 @@ class Router<T> {
/// 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('<root>');
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<T> {
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<T> {
///
/// Returns the created route.
/// You can also register middleware within the router.
SymlinkRoute<T> group(String path, void callback(Router<T> router),
{Iterable<T>? middleware, String? name}) {
middleware ??= <T>[];
SymlinkRoute<T> group(String path, void Function(Router<T> router) callback,
{Iterable<T> middleware = const Iterable.empty(), String name = ''}) {
final router = Router<T>().._middleware.addAll(middleware);
callback(router);
return mount(path, router)..name = name;
@ -174,9 +174,9 @@ class Router<T> {
/// Asynchronous equivalent of [group].
Future<SymlinkRoute<T>> groupAsync(
String path, FutureOr<void> callback(Router<T> router),
{Iterable<T>? middleware, String? name}) async {
middleware ??= <T>[];
String path, FutureOr<void> Function(Router<T> router) callback,
{Iterable<T> middleware = const Iterable.empty(),
String name = ''}) async {
final router = Router<T>().._middleware.addAll(middleware);
await callback(router);
return mount(path, router)..name = name;
@ -210,16 +210,16 @@ class Router<T> {
/// router.navigate(['users/:id', {'id': '1337'}, 'profile']);
/// ```
String navigate(Iterable linkParams, {bool absolute = true}) {
final List<String> segments = [];
final segments = <String>[];
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,9 +236,11 @@ class Router<T> {
// 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) {
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;
@ -251,6 +253,9 @@ class Router<T> {
} else {
scanner.position = pos;
}
} else {
scanner.position = pos;
}
}
}
@ -281,39 +286,44 @@ class Router<T> {
/// Finds the first [Route] that matches the given path,
/// 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}) {
final cleanRelative =
strip == false ? relative! : stripStraySlashes(relative!);
strip == false ? relative : stripStraySlashes(relative);
var scanner = SpanScanner(cleanRelative);
bool crawl(Router<T> 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<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);
if (s) success = true;
}
}
scanner.position = pos;
} 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) {
var tailResult = parseResult.value?.tail ?? '';
print(tailResult);
var result = RoutingResult<T>(
parseResult: parseResult,
params: parseResult.value!.params,
shallowRoute: route,
shallowRouter: this,
tail: (parseResult.value!.tail ?? '') + scanner.rest);
tail: tailResult + scanner.rest);
out.add(result);
success = true;
}
}
scanner.position = pos;
}
}
@ -326,13 +336,13 @@ class Router<T> {
/// Returns the result of [resolve] with [path] passed as
/// both `absolute` and `relative`.
Iterable<RoutingResult<T>> resolveAbsolute(String? path,
Iterable<RoutingResult<T>> 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<RoutingResult<T>> resolveAll(String? absolute, String? relative,
Iterable<RoutingResult<T>> 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<T> {
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}) {
var results = <RoutingResult<T>>[];
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.
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);
}
/// 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);
}
/// 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);
}
/// 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);
}
/// 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);
}
/// 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);
}
/// 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);
}
/// 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);
}
}
class _ChainedRouter<T> extends Router<T> {
final List<T> _handlers = <T>[];
Router? _root;
Router _root;
_ChainedRouter.empty();
_ChainedRouter.empty() : _root = Router();
_ChainedRouter(Router? root, Iterable<T> middleware) {
this._root = root;
_ChainedRouter(this._root, Iterable<T> middleware) {
_handlers.addAll(middleware);
}
@override
Route<T> addRoute(String? method, String path, handler,
Route<T> addRoute(String method, String path, handler,
{Iterable<T>? middleware}) {
Route<T> route = super.addRoute(method, path, handler,
middleware: []..addAll(_handlers)..addAll(middleware ?? []));
middleware ??= <T>[];
var route = super.addRoute(method, path, handler,
middleware: [..._handlers, ...middleware]);
//_root._routes.add(route);
return route;
}
@override
SymlinkRoute<T> group(String path, void callback(Router<T> router),
{Iterable<T>? middleware, String? name}) {
final router = _ChainedRouter<T>(
_root, []..addAll(_handlers)..addAll(middleware ?? []));
SymlinkRoute<T> group(String path, void Function(Router<T> router) callback,
{Iterable<T> middleware = const Iterable.empty(), String name = ''}) {
final router = _ChainedRouter<T>(_root, [..._handlers, ...middleware]);
callback(router);
return mount(path, router)..name = name;
}
@override
Future<SymlinkRoute<T>> groupAsync(
String path, FutureOr<void> callback(Router<T> router),
{Iterable<T>? middleware, String? name}) async {
final router = _ChainedRouter<T>(
_root, []..addAll(_handlers)..addAll(middleware ?? []));
String path, FutureOr<void> Function(Router<T> router) callback,
{Iterable<T> middleware = const Iterable.empty(),
String name = ''}) async {
final router = _ChainedRouter<T>(_root, [..._handlers, ...middleware]);
await callback(router);
return mount(path, router)..name = name;
}
@override
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);
//_root._routes.add(route);
return route;
@ -453,7 +470,7 @@ class _ChainedRouter<T> extends Router<T> {
@override
_ChainedRouter<T> chain(Iterable<T> middleware) {
final piped = _ChainedRouter<T>.empty().._root = _root;
piped._handlers.addAll([]..addAll(_handlers)..addAll(middleware));
piped._handlers.addAll([..._handlers, ...middleware]);
var route = SymlinkRoute<T>('/', piped);
_routes.add(route);
return piped;
@ -473,13 +490,13 @@ Router<T> flatten<T>(Router<T> 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());
}
}

View file

@ -3,23 +3,23 @@ part of angel_route.src.router;
/// Represents a complex result of navigating to a path.
class RoutingResult<T> {
/// 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.
final Iterable<RoutingResult<T>>? nested;
final Iterable<RoutingResult<T>> nested;
/// 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.
///
/// 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.
///
/// 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.
final String tail;
@ -28,22 +28,22 @@ class RoutingResult<T> {
RoutingResult<T> 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<T>? get route => deepest.shallowRoute;
Route<T> get route => deepest.shallowRoute;
/// The most specific router.
Router<T>? get router => deepest.shallowRouter;
Router<T> get router => deepest.shallowRouter;
/// The handlers at this sub-path.
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.
@ -53,8 +53,8 @@ class RoutingResult<T> {
void crawl(RoutingResult<T> 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<T> {
}
/// All parameters on this sub-path and its children.
Map<String?, dynamic> get allParams {
final params = <String?, dynamic>{};
Map<String, dynamic> get allParams {
final params = <String, dynamic>{};
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<T> {
}
RoutingResult(
{this.parseResult,
Map<String?, dynamic> params = const {},
this.nested,
this.shallowRoute,
this.shallowRouter,
{required this.parseResult,
Map<String, dynamic> params = const {},
this.nested = const Iterable.empty(),
required this.shallowRoute,
required this.shallowRouter,
required this.tail}) {
this.params.addAll(params);
}

View file

@ -5,5 +5,5 @@ part of angel_route.src.router;
class SymlinkRoute<T> extends Route<T> {
final Router<T> 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
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);
}

View file

@ -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']));

View file

@ -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;

View file

@ -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');
});
}

View file

@ -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');