No-expose controller tests
This commit is contained in:
parent
32d0396ebd
commit
94c37e103c
4 changed files with 98 additions and 24 deletions
|
@ -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.
|
||||
|
|
|
@ -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, '_')));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)`.
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue