No-expose controller tests

This commit is contained in:
Tobe O 2019-10-12 10:56:24 -04:00
parent 32d0396ebd
commit 94c37e103c
4 changed files with 98 additions and 24 deletions

View file

@ -5,6 +5,7 @@
* Add `HostnameRouter`, which allows for routing based on hostname. https://github.com/angel-dart/angel/issues/110
* Default to using `ThrowingReflector`, instead of `EmptyReflector`. This will give a more descriptive
error when trying to use controllers, etc. without reflection enabled.
* `mountController` returns the mounted controller.
# 2.0.4+1
* Run `Controller.configureRoutes` before mounting `@Expose` routes.

View file

@ -23,6 +23,11 @@ class Controller {
/// A mapping of route paths to routes, produced from the [Expose] annotations on this class.
Map<String, Route> routeMappings = {};
SymlinkRoute<RequestHandler> _mountPoint;
/// The route at which this controller is mounted on the server.
SymlinkRoute<RequestHandler> get mountPoint => _mountPoint;
Controller({this.injectSingleton = true});
/// Applies routes, DI, and other configuration to an [app].
@ -42,7 +47,7 @@ class Controller {
}
/// Applies the routes from this [Controller] to some [router].
Future<String> applyRoutes(Router router, Reflector reflector) async {
Future<String> applyRoutes(Router<RequestHandler> router, Reflector reflector) async {
// Load global expose decl
var classMirror = reflector.reflectClass(this.runtimeType);
Expose exposeDecl = findExpose(reflector);
@ -52,7 +57,7 @@ class Controller {
}
var routable = Routable();
router.mount(exposeDecl.path, routable);
_mountPoint = router.mount(exposeDecl.path, routable);
var typeMirror = reflector.reflectType(this.runtimeType);
// Pre-reflect methods
@ -77,6 +82,7 @@ class Controller {
return (ReflectedDeclaration decl) {
var methodName = decl.name;
// Ignore built-in methods.
if (methodName != 'toString' &&
methodName != 'noSuchMethod' &&
methodName != 'call' &&
@ -129,14 +135,18 @@ class Controller {
var path = exposeDecl.path;
var httpMethod = exposeDecl.method ?? 'GET';
if (path == null) {
// Try to build a route path by finding all potential
// path segments, and then joining them.
var parts = <String>[];
var methodMatch = _methods.firstMatch(method.name);
// If the name starts with get/post/patch, etc., then that
// should be the path.
var methodMatch = _methods.firstMatch(method.name);
if (methodMatch != null) {
var rest = method.name.replaceAll(_methods, '');
var restPath = ReCase(rest.isEmpty ? 'index' : rest)
.snakeCase
.replaceAll(_multiScore, '_');
.replaceAll(_rgxMultipleUnderscores, '_');
httpMethod = methodMatch[1].toUpperCase();
if (['index', 'by_id'].contains(restPath)) {
@ -144,16 +154,23 @@ class Controller {
} else {
parts.add(restPath);
}
} else {
}
// If the name does NOT start with get/post/patch, etc. then
// snake_case-ify the name, and add it to the list of segments.
// If the name is index, though, add "/".
else {
if (method.name == 'index') {
parts.add('/');
} else {
parts.add(
ReCase(method.name).snakeCase.replaceAll(_multiScore, '_'));
parts.add(ReCase(method.name)
.snakeCase
.replaceAll(_rgxMultipleUnderscores, '_'));
}
}
// Try to infer String, int, or double.
// Try to infer String, int, or double. We called
// preInject() earlier, so we can figure out the types
// of required parameters, and add those to the path.
for (var p in injection.required) {
if (p is List && p.length == 2 && p[0] is String && p[1] is Type) {
var name = p[0] as String;
@ -169,6 +186,7 @@ class Controller {
}
path = parts.join('/');
if (!path.startsWith('/')) path = '/$path';
}
routeMappings[name] = routable.addRoute(
@ -190,9 +208,12 @@ class Controller {
FutureOr<void> configureRoutes(Routable routable) {}
static final RegExp _methods = RegExp(r'^(get|post|patch|delete)');
static final RegExp _multiScore = RegExp(r'__+');
static final RegExp _rgxMultipleUnderscores = RegExp(r'__+');
/// Finds the [Expose] declaration for this class.
///
/// If [concreteOnly] is `false`, then if there is no actual
/// [Expose], one will be automatically created.
Expose findExpose(Reflector reflector, {bool concreteOnly = false}) {
var existing = reflector
.reflectClass(runtimeType)
@ -206,6 +227,6 @@ class Controller {
.snakeCase
.replaceAll('_controller', '')
.replaceAll('_ctrl', '')
.replaceAll(_multiScore, '_')));
.replaceAll(_rgxMultipleUnderscores, '_')));
}
}

View file

@ -334,13 +334,15 @@ class Angel extends Routable {
}
/// Shorthand for using the [container] to instantiate, and then mount a [Controller].
/// Returns the created controller.
///
/// Just like [Container].make, in contexts without properly-reified generics (dev releases of Dart 2),
/// provide a [type] argument as well.
///
/// If you are on `Dart >=2.0.0`, simply call `mountController<T>()`..
Future mountController<T extends Controller>([Type type]) {
return configure(container.make<T>(type).configureServer);
/// If you are on `Dart >=2.0.0`, simply call `mountController<T>()`.
Future<T> mountController<T extends Controller>([Type type]) {
var controller = container.make<T>(type);
return configure(controller.configureServer).then((_) => controller);
}
/// Shorthand for calling `all('*', handler)`.

View file

@ -30,7 +30,28 @@ class TodoController extends Controller {
}
}
class NoExposeController extends Controller {}
class NoExposeController extends Controller {
String getIndex() => 'Hey!';
int timesTwo(int n) => n * 2;
String repeatName(String name, int times) {
var b = StringBuffer();
for (int i = 0; i < times; i++) {
b.writeln(name);
}
return b.toString();
}
@Expose('/yellow', method: 'POST')
String someColor() => 'yellow';
@Expose.patch
int three() => 333;
@noExpose
String hideThis() => 'Should not be exposed';
}
@Expose('/named', as: 'foo')
class NamedController extends Controller {
@ -51,6 +72,7 @@ bool bar(RequestContext req, ResponseContext res) {
main() {
Angel app;
TodoController ctrl;
NoExposeController noExposeCtrl;
HttpServer server;
http.Client client = http.Client();
String url;
@ -70,6 +92,8 @@ main() {
// Using mountController<T>();
await app.mountController<TodoController>();
noExposeCtrl = await app.mountController<NoExposeController>();
// Place controller in group...
app.group('/ctrl_group', (router) {
app.container
@ -94,16 +118,6 @@ main() {
expect(ctrl.app, app);
});
test('require expose', () async {
try {
var app = Angel(reflector: MirrorsReflector());
await app.configure(NoExposeController().configureServer);
throw 'Should require @Expose';
} on Exception {
// :)
}
});
test('create dynamic handler', () async {
var app = Angel(reflector: MirrorsReflector());
app.get(
@ -154,4 +168,40 @@ main() {
print('Response: ${response.body}');
expect(response.body, equals("Hello, \"world!\""));
});
group('optional expose', () {
test('removes suffixes from controller names', () {
expect(noExposeCtrl.mountPoint.path, 'no_expose');
});
test('mounts correct routes', () {
print(noExposeCtrl.routeMappings.keys);
expect(noExposeCtrl.routeMappings.keys.toList(),
['getIndex', 'timesTwo', 'repeatName', 'someColor', 'three']);
});
test('mounts correct methods', () {
void expectMethod(String name, String method) {
expect(noExposeCtrl.routeMappings[name].method, method);
}
expectMethod('getIndex', 'GET');
expectMethod('timesTwo', 'GET');
expectMethod('repeatName', 'GET');
expectMethod('someColor', 'POST');
expectMethod('three', 'PATCH');
});
test('mounts correct paths', () {
void expectPath(String name, String path) {
expect(noExposeCtrl.routeMappings[name].path, path);
}
expectPath('getIndex', '/');
expectPath('timesTwo', '/times_two/int:n');
expectPath('repeatName', '/repeat_name/:name/int:times');
expectPath('someColor', '/yellow');
expectPath('three', '/three');
});
});
}