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:json_god/json_god.dart' as god;
|
||||||
import 'package:merge_map/merge_map.dart';
|
import 'package:merge_map/merge_map.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
import '../../defs.dart';
|
||||||
|
|
||||||
|
part 'controller.dart';
|
||||||
part 'extensible.dart';
|
part 'extensible.dart';
|
||||||
part 'errors.dart';
|
part 'errors.dart';
|
||||||
part 'metadata/metadata.dart';
|
part 'metadata/metadata.dart';
|
||||||
|
@ -22,3 +24,4 @@ part 'server.dart';
|
||||||
part 'service.dart';
|
part 'service.dart';
|
||||||
part 'service_hooked.dart';
|
part 'service_hooked.dart';
|
||||||
part 'services/memory.dart';
|
part 'services/memory.dart';
|
||||||
|
|
||||||
|
|
|
@ -10,4 +10,18 @@ class Middleware {
|
||||||
/// Annotation to set a service up to release hooks on every action.
|
/// Annotation to set a service up to release hooks on every action.
|
||||||
class Hooked {
|
class Hooked {
|
||||||
const 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)');
|
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.
|
/// Streams a file to this response as chunked data.
|
||||||
///
|
///
|
||||||
/// Useful for video sites.
|
/// Useful for video sites.
|
||||||
|
|
|
@ -37,6 +37,9 @@ class Routable extends Extensible {
|
||||||
/// A set of [Service] objects that have been mapped into routes.
|
/// A set of [Service] objects that have been mapped into routes.
|
||||||
Map <Pattern, Service> services = {};
|
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.
|
/// Assigns a middleware to a name for convenience.
|
||||||
registerMiddleware(String name, RequestMiddleware middleware) {
|
registerMiddleware(String name, RequestMiddleware middleware) {
|
||||||
this.requestMiddleware[name] = middleware;
|
this.requestMiddleware[name] = middleware;
|
||||||
|
@ -45,6 +48,9 @@ class Routable extends Extensible {
|
||||||
/// Retrieves the service assigned to the given path.
|
/// Retrieves the service assigned to the given path.
|
||||||
Service service(Pattern path) => services[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.
|
/// Incorporates another [Routable]'s routes into this one's.
|
||||||
///
|
///
|
||||||
/// If `hooked` is set to `true` and a [Service] is provided,
|
/// If `hooked` is set to `true` and a [Service] is provided,
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
part of angel_framework.http;
|
part of angel_framework.http;
|
||||||
|
|
||||||
/// Represents data that can be serialized into a MemoryService;
|
|
||||||
class MemoryModel {
|
|
||||||
int id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An in-memory [Service].
|
/// An in-memory [Service].
|
||||||
class MemoryService<T> extends Service {
|
class MemoryService<T> extends Service {
|
||||||
Map <int, MemoryModel> items = {};
|
Map <int, MemoryModel> items = {};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: angel_framework
|
name: angel_framework
|
||||||
version: 1.0.0-dev+5
|
version: 1.0.0-dev+6
|
||||||
description: Core libraries for the Angel framework.
|
description: Core libraries for the Angel framework.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_framework
|
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:http/http.dart' as http;
|
||||||
import 'package:json_god/json_god.dart' as god;
|
import 'package:json_god/json_god.dart' as god;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import 'common.dart';
|
||||||
class Todo {
|
|
||||||
String text;
|
|
||||||
String over;
|
|
||||||
|
|
||||||
Todo({String this.text, String this.over});
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
group('Hooked', () {
|
group('Hooked', () {
|
||||||
|
|
Loading…
Reference in a new issue