diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f23074..9d596d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # 2.0.5 +* Make `@Expose()` in `Controller` optional. https://github.com/angel-dart/angel/issues/107 * Add `allowHttp1` to `AngelHttp2` constructors. https://github.com/angel-dart/angel/issues/108 * Add `deserializeBody` and `decodeBody` to `RequestContext`. https://github.com/angel-dart/angel/issues/109 * Add `HostnameRouter`, which allows for routing based on hostname. https://github.com/angel-dart/angel/issues/110 diff --git a/lib/src/core/controller.dart b/lib/src/core/controller.dart index 6befde29..9dd11cd5 100644 --- a/lib/src/core/controller.dart +++ b/lib/src/core/controller.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'package:angel_container/angel_container.dart'; import 'package:angel_route/angel_route.dart'; import 'package:meta/meta.dart'; - +import 'package:recase/recase.dart'; import '../core/core.dart'; /// Supports grouping routes with shared functionality. @@ -86,7 +86,15 @@ class Controller { .map((m) => m.reflectee) .firstWhere((r) => r is Expose, orElse: () => null) as Expose; - if (exposeDecl == null) return; + if (exposeDecl == null) { + // If this has a @noExpose, return null. + if (decl.function.annotations.any((m) => m.reflectee is NoExpose)) { + return; + } else { + // Otherwise, create an @Expose. + exposeDecl = Expose(null); + } + } var reflectedMethod = instanceMirror.getField(methodName).reflectee as Function; @@ -117,6 +125,30 @@ class Controller { injection.optional?.addAll(exposeDecl.allowNull); } + // If there is no path, reverse-engineer one. + var path = exposeDecl.path; + if (path == null) { + var parts = []; + parts.add(ReCase(method.name).snakeCase.replaceAll(_multiScore, '_')); + + // Try to infer String, int, or double. + 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; + var type = p[1] as Type; + if (type == String) { + parts.add(':$name'); + } else if (type == int) { + parts.add('int:$name'); + } else if (type == double) { + parts.add('double:$name'); + } + } + } + + path = parts.join('/'); + } + routeMappings[name] = routable.addRoute(exposeDecl.method, exposeDecl.path, handleContained(reflectedMethod, injection), middleware: middleware); @@ -127,10 +159,22 @@ class Controller { /// Used to add additional routes to the router from within a [Controller]. void configureRoutes(Routable routable) {} + static final RegExp _multiScore = RegExp(r'__+'); + /// Finds the [Expose] declaration for this class. - Expose findExpose(Reflector reflector) => reflector - .reflectClass(runtimeType) - .annotations - .map((m) => m.reflectee) - .firstWhere((r) => r is Expose, orElse: () => null) as Expose; + Expose findExpose(Reflector reflector, {bool concreteOnly = false}) { + var existing = reflector + .reflectClass(runtimeType) + .annotations + .map((m) => m.reflectee) + .firstWhere((r) => r is Expose, orElse: () => null) as Expose; + return existing ?? + (concreteOnly + ? null + : Expose(ReCase(runtimeType.toString()) + .snakeCase + .replaceAll('_controller', '') + .replaceAll('_ctrl', '') + .replaceAll(_multiScore, '_'))); + } } diff --git a/lib/src/core/metadata.dart b/lib/src/core/metadata.dart index 877073ff..91e419b2 100644 --- a/lib/src/core/metadata.dart +++ b/lib/src/core/metadata.dart @@ -21,7 +21,14 @@ class Hooks { const Hooks({this.before = const [], this.after = const []}); } -/// Exposes a [Controller] to the Internet. +/// Specifies to NOT expose a method to the Internet. +class NoExpose { + const NoExpose(); +} + +const NoExpose noExpose = NoExpose(); + +/// Exposes a [Controller] or method to the Internet. class Expose { final String method; final String path; @@ -29,11 +36,22 @@ class Expose { final String as; final List allowNull; + static const Expose get = Expose(null, method: 'GET'), + post = Expose(null, method: 'POST'), + patch = Expose(null, method: 'PATCH'), + put = Expose(null, method: 'PUT'), + delete = Expose(null, method: 'DELETE'), + head = Expose(null, method: 'HEAD'); + const Expose(this.path, {this.method = "GET", this.middleware = const [], this.as, this.allowNull = const []}); + + const Expose.method(this.method, + {this.middleware, this.as, this.allowNull = const []}) + : path = null; } /// Used to apply special dependency injections or functionality to a function parameter. diff --git a/pubspec.yaml b/pubspec.yaml index e4207fad..152229ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: path: ^1.0.0 pedantic: ^1.0.0 quiver_hashcode: ^2.0.0 + recase: ^2.0.0 stack_trace: ^1.0.0 tuple: ^1.0.0 uuid: ^2.0.0-rc.1