Added Controllers, omg
This commit is contained in:
parent
2459c82bf9
commit
a7c8a95af7
11 changed files with 238 additions and 13 deletions
6
lib/defs.dart
Normal file
6
lib/defs.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
library angel_framework.defs;
|
||||
|
||||
/// Represents data that can be serialized into a MemoryService;
|
||||
class MemoryModel {
|
||||
int id;
|
||||
}
|
99
lib/src/http/controller.dart
Normal file
99
lib/src/http/controller.dart
Normal file
|
@ -0,0 +1,99 @@
|
|||
part of angel_framework.http;
|
||||
|
||||
class Controller {
|
||||
List middleware = [];
|
||||
List<Route> routes = [];
|
||||
Map<String, Route> _mappings = {};
|
||||
Expose exposeDecl;
|
||||
|
||||
Future call(Angel app) async {
|
||||
Routable routable = new Routable()
|
||||
..routes.addAll(routes);
|
||||
app.use(exposeDecl.path, routable);
|
||||
|
||||
TypeMirror typeMirror = reflectType(this.runtimeType);
|
||||
String name = exposeDecl.as;
|
||||
|
||||
if (name == null || name.isEmpty)
|
||||
name = MirrorSystem.getName(typeMirror.simpleName);
|
||||
|
||||
app.controllers[name] = this;
|
||||
}
|
||||
|
||||
Controller() {
|
||||
// Load global expose decl
|
||||
ClassMirror classMirror = reflectClass(this.runtimeType);
|
||||
|
||||
for (InstanceMirror metadatum in classMirror.metadata) {
|
||||
if (metadatum.reflectee is Expose) {
|
||||
exposeDecl = metadatum.reflectee;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exposeDecl == null)
|
||||
throw new Exception(
|
||||
"All controllers must carry an @Expose() declaration.");
|
||||
else routes.add(
|
||||
new Route(
|
||||
"*", "*", []..addAll(exposeDecl.middleware)..addAll(middleware)));
|
||||
|
||||
InstanceMirror instanceMirror = reflect(this);
|
||||
classMirror.instanceMembers.forEach((Symbol key,
|
||||
MethodMirror methodMirror) {
|
||||
if (methodMirror.isRegularMethod && key != #toString &&
|
||||
key != #noSuchMethod && key != #call && key != #equals &&
|
||||
key != #==) {
|
||||
InstanceMirror exposeMirror = methodMirror.metadata.firstWhere((
|
||||
mirror) => mirror.reflectee is Expose, orElse: () => null);
|
||||
|
||||
if (exposeMirror != null) {
|
||||
RequestHandler handler = (RequestContext req,
|
||||
ResponseContext res) async {
|
||||
List args = [];
|
||||
|
||||
try {
|
||||
// Load parameters, and execute
|
||||
for (int i = 0; i < methodMirror.parameters.length; i++) {
|
||||
ParameterMirror parameter = methodMirror.parameters[i];
|
||||
if (parameter.type.reflectedType == RequestContext)
|
||||
args.add(req);
|
||||
else if (parameter.type.reflectedType == ResponseContext)
|
||||
args.add(res);
|
||||
else {
|
||||
String name = MirrorSystem.getName(parameter.simpleName);
|
||||
var arg = req.params[name];
|
||||
|
||||
if (arg == null &&
|
||||
!exposeMirror.reflectee.allowNull.contain(name)) {
|
||||
throw new AngelHttpException.BadRequest();
|
||||
}
|
||||
|
||||
args.add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return await instanceMirror
|
||||
.invoke(key, args)
|
||||
.reflectee;
|
||||
} catch (e) {
|
||||
throw new AngelHttpException(e);
|
||||
}
|
||||
};
|
||||
Route route = new Route(
|
||||
exposeMirror.reflectee.method,
|
||||
exposeMirror.reflectee.path,
|
||||
[handler]..addAll(exposeMirror.reflectee.middleware));
|
||||
routes.add(route);
|
||||
|
||||
String name = exposeMirror.reflectee.as;
|
||||
|
||||
if (name == null || name.isEmpty)
|
||||
name = MirrorSystem.getName(key);
|
||||
|
||||
_mappings[name] = route;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -10,7 +10,9 @@ import 'package:body_parser/body_parser.dart';
|
|||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:merge_map/merge_map.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import '../../defs.dart';
|
||||
|
||||
part 'controller.dart';
|
||||
part 'extensible.dart';
|
||||
part 'errors.dart';
|
||||
part 'metadata/metadata.dart';
|
||||
|
@ -22,3 +24,4 @@ part 'server.dart';
|
|||
part 'service.dart';
|
||||
part 'service_hooked.dart';
|
||||
part 'services/memory.dart';
|
||||
|
||||
|
|
|
@ -11,3 +11,17 @@ class Middleware {
|
|||
class Hooked {
|
||||
const Hooked();
|
||||
}
|
||||
|
||||
class Expose {
|
||||
final String method;
|
||||
final Pattern path;
|
||||
final List middleware;
|
||||
final String as;
|
||||
final List<String> allowNull;
|
||||
|
||||
const Expose(Pattern this.path,
|
||||
{String this.method: "GET",
|
||||
List this.middleware: const [],
|
||||
String this.as: null,
|
||||
List<String> this.allowNull: const[]});
|
||||
}
|
|
@ -103,6 +103,27 @@ class ResponseContext extends Extensible {
|
|||
throw new ArgumentError.notNull('Route to redirect to ($name)');
|
||||
}
|
||||
|
||||
/// Redirects to the given [Controller] action.
|
||||
redirectToAction(String action, [Map params, int code]) {
|
||||
// UserController@show
|
||||
List<String> split = action.split("@");
|
||||
|
||||
if (split.length < 2)
|
||||
throw new Exception("Controller redirects must take the form of 'Controller@action'. You gave: $action");
|
||||
|
||||
Controller controller = app.controller(split[0]);
|
||||
|
||||
if (controller == null)
|
||||
throw new Exception("Could not find a controller named '${split[0]}'");
|
||||
|
||||
Route matched = controller._mappings[split[1]];
|
||||
|
||||
if (matched == null)
|
||||
throw new Exception("Controller '${split[0]}' does not contain any action named '${split[1]}'");
|
||||
|
||||
return redirect(matched.makeUri(params), code: code);
|
||||
}
|
||||
|
||||
/// Streams a file to this response as chunked data.
|
||||
///
|
||||
/// Useful for video sites.
|
||||
|
|
|
@ -37,6 +37,9 @@ class Routable extends Extensible {
|
|||
/// A set of [Service] objects that have been mapped into routes.
|
||||
Map <Pattern, Service> services = {};
|
||||
|
||||
/// A set of [Controller] objects that have been loaded into the application.
|
||||
Map<String, Controller> controllers = {};
|
||||
|
||||
/// Assigns a middleware to a name for convenience.
|
||||
registerMiddleware(String name, RequestMiddleware middleware) {
|
||||
this.requestMiddleware[name] = middleware;
|
||||
|
@ -45,6 +48,9 @@ class Routable extends Extensible {
|
|||
/// Retrieves the service assigned to the given path.
|
||||
Service service(Pattern path) => services[path];
|
||||
|
||||
/// Retrieves the controller with the given name.
|
||||
Controller controller(String name) => controllers[name];
|
||||
|
||||
/// Incorporates another [Routable]'s routes into this one's.
|
||||
///
|
||||
/// If `hooked` is set to `true` and a [Service] is provided,
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
part of angel_framework.http;
|
||||
|
||||
/// Represents data that can be serialized into a MemoryService;
|
||||
class MemoryModel {
|
||||
int id;
|
||||
}
|
||||
|
||||
/// An in-memory [Service].
|
||||
class MemoryService<T> extends Service {
|
||||
Map <int, MemoryModel> items = {};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: angel_framework
|
||||
version: 1.0.0-dev+5
|
||||
version: 1.0.0-dev+6
|
||||
description: Core libraries for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_framework
|
||||
|
|
8
test/common.dart
Normal file
8
test/common.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
library angel_framework.test.common;
|
||||
|
||||
class Todo {
|
||||
String text;
|
||||
String over;
|
||||
|
||||
Todo({String this.text, String this.over});
|
||||
}
|
79
test/controller.dart
Normal file
79
test/controller.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
@Expose("/todos", middleware: const ["foo"])
|
||||
class TodoController extends Controller {
|
||||
List<Todo> todos = [new Todo(text: "Hello", over: "world")];
|
||||
|
||||
@Expose("/:id", middleware: const["bar"])
|
||||
Future<Todo> fetchTodo(int id, RequestContext req,
|
||||
ResponseContext res) async {
|
||||
expect(req, isNotNull);
|
||||
expect(res, isNotNull);
|
||||
return todos[id];
|
||||
}
|
||||
|
||||
@Expose("/namedRoute/:foo", as: "foo")
|
||||
Future<String> someRandomRoute(RequestContext req, ResponseContext res) async {
|
||||
return "${req.params['foo']}!";
|
||||
}
|
||||
}
|
||||
|
||||
main() {
|
||||
group("controller", () {
|
||||
Angel app = new Angel();
|
||||
HttpServer server;
|
||||
InternetAddress host = InternetAddress.LOOPBACK_IP_V4;
|
||||
int port = 3000;
|
||||
http.Client client;
|
||||
String url = "http://${host.address}:$port";
|
||||
|
||||
setUp(() async {
|
||||
app.registerMiddleware("foo", (req, res) async => res.write("Hello, "));
|
||||
app.registerMiddleware("bar", (req, res) async => res.write("world!"));
|
||||
app.get("/redirect", (req, ResponseContext res) async =>
|
||||
res.redirectToAction("TodoController@foo", {"foo": "world"}));
|
||||
await app.configure(new TodoController());
|
||||
|
||||
print(app.controllers);
|
||||
print("\nDUMPING ROUTES:");
|
||||
app.routes.forEach((Route route) {
|
||||
print("\t${route.method} ${route.path} -> ${route.handlers}");
|
||||
});
|
||||
print("\n");
|
||||
|
||||
server = await app.startServer(host, port);
|
||||
client = new http.Client();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await server.close(force: true);
|
||||
client.close();
|
||||
client = null;
|
||||
});
|
||||
|
||||
test("middleware", () async {
|
||||
var response = await client.get("$url/todos/0");
|
||||
print(response.body);
|
||||
|
||||
expect(response.body.indexOf("Hello, "), equals(0));
|
||||
|
||||
Map todo = JSON.decode(response.body.substring(7));
|
||||
expect(todo.keys.length, equals(2));
|
||||
expect(todo['text'], equals("Hello"));
|
||||
expect(todo['over'], equals("world"));
|
||||
});
|
||||
|
||||
test("named actions", () async {
|
||||
var response = await client.get("$url/redirect");
|
||||
print(response.body);
|
||||
|
||||
expect(response.body, equals("Hello, \"world!\""));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -2,13 +2,7 @@ import 'package:angel_framework/angel_framework.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class Todo {
|
||||
String text;
|
||||
String over;
|
||||
|
||||
Todo({String this.text, String this.over});
|
||||
}
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
group('Hooked', () {
|
||||
|
|
Loading…
Reference in a new issue