Added Controllers, omg

This commit is contained in:
regiostech 2016-06-26 20:20:42 -04:00
parent 2459c82bf9
commit a7c8a95af7
11 changed files with 238 additions and 13 deletions

6
lib/defs.dart Normal file
View file

@ -0,0 +1,6 @@
library angel_framework.defs;
/// Represents data that can be serialized into a MemoryService;
class MemoryModel {
int id;
}

View 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;
}
}
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {};

View file

@ -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
View 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
View 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!\""));
});
});
}

View file

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