Route API change is breaking, haha.

This commit is contained in:
thosakwe 2016-10-22 16:41:36 -04:00
parent 1bb077a3d9
commit 551a7f086f
14 changed files with 447 additions and 571 deletions

View file

@ -1,28 +1,3 @@
library angel_framework.extensible; library angel_framework.extensible;
import 'dart:mirrors'; export 'package:angel_route/src/extensible.dart';
/// Supports accessing members of a Map as though they were actual members.
class Extensible {
/// A set of custom properties that can be assigned to the server.
///
/// Useful for configuration and extension.
Map properties = {};
noSuchMethod(Invocation invocation) {
if (invocation.memberName != null) {
String name = MirrorSystem.getName(invocation.memberName);
if (properties.containsKey(name)) {
if (invocation.isGetter)
return properties[name];
else if (invocation.isMethod) {
return Function.apply(
properties[name], invocation.positionalArguments,
invocation.namedArguments);
}
}
}
super.noSuchMethod(invocation);
}
}

View file

@ -8,7 +8,10 @@ import 'routable.dart';
typedef Future<String> ViewGenerator(String path, [Map data]); typedef Future<String> ViewGenerator(String path, [Map data]);
class AngelBase extends Routable { class AngelBase extends Routable {
AngelBase({bool debug: false}):super(debug: debug);
Container _container = new Container(); Container _container = new Container();
/// A [Container] used to inject dependencies. /// A [Container] used to inject dependencies.
Container get container => _container; Container get container => _container;

View file

@ -2,106 +2,20 @@ library angel_framework.http.controller;
import 'dart:async'; import 'dart:async';
import 'dart:mirrors'; import 'dart:mirrors';
import 'package:angel_route/angel_route.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'angel_http_exception.dart'; import 'angel_http_exception.dart';
import 'metadata.dart'; import 'metadata.dart';
import 'request_context.dart'; import 'request_context.dart';
import 'response_context.dart'; import 'response_context.dart';
import 'routable.dart'; import 'routable.dart';
import 'route.dart';
class Controller { class Controller {
AngelBase app; AngelBase app;
List middleware = []; List middleware = [];
List<Route> routes = [];
Map<String, Route> routeMappings = {}; Map<String, Route> routeMappings = {};
Expose exposeDecl; Expose exposeDecl;
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 = [];
// 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) {
if (parameter.type.reflectedType != dynamic) {
try {
arg = app.container.make(parameter.type.reflectedType);
if (arg != null) {
args.add(arg);
continue;
}
} catch(e) {
//
}
}
if (!exposeMirror.reflectee.allowNull.contain(name))
throw new AngelHttpException.BadRequest(message: "Missing parameter '$name'");
} else args.add(arg);
}
}
return await instanceMirror
.invoke(key, args)
.reflectee;
};
Route route = new Route(
exposeMirror.reflectee.method,
exposeMirror.reflectee.path,
[]
..addAll(exposeMirror.reflectee.middleware)
..add(handler));
routes.add(route);
String name = exposeMirror.reflectee.as;
if (name == null || name.isEmpty)
name = MirrorSystem.getName(key);
routeMappings[name] = route;
}
}
});
}
Future call(AngelBase app) async { Future call(AngelBase app) async {
this.app = app; this.app = app;
app.use(exposeDecl.path, generateRoutable()); app.use(exposeDecl.path, generateRoutable());
@ -115,5 +29,94 @@ class Controller {
app.controllers[name] = this; app.controllers[name] = this;
} }
Routable generateRoutable() => new Routable()..routes.addAll(routes); Routable generateRoutable() {
final routable = new Routable();
// 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.");
}
final handlers = []..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 = [];
// 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) {
if (parameter.type.reflectedType != dynamic) {
try {
arg = app.container.make(parameter.type.reflectedType);
if (arg != null) {
args.add(arg);
continue;
}
} catch (e) {
//
}
}
if (!exposeMirror.reflectee.allowNull.contain(name))
throw new AngelHttpException.BadRequest(
message: "Missing parameter '$name'");
} else
args.add(arg);
}
}
return await instanceMirror.invoke(key, args).reflectee;
};
final route = routable.addRoute(exposeMirror.reflectee.method,
exposeMirror.reflectee.path, handler,
middleware: []
..addAll(handlers)
..addAll(exposeMirror.reflectee.middleware));
String name = exposeMirror.reflectee.as;
if (name == null || name.isEmpty) name = MirrorSystem.getName(key);
routeMappings[name] = route;
}
}
});
return routable;
}
} }

View file

@ -4,7 +4,6 @@ import 'dart:async';
import 'package:merge_map/merge_map.dart'; import 'package:merge_map/merge_map.dart';
import '../util.dart'; import '../util.dart';
import 'metadata.dart'; import 'metadata.dart';
import 'route.dart';
import 'service.dart'; import 'service.dart';
/// Wraps another service in a service that broadcasts events on actions. /// Wraps another service in a service that broadcasts events on actions.
@ -13,84 +12,95 @@ class HookedService extends Service {
final Service inner; final Service inner;
HookedServiceEventDispatcher beforeIndexed = HookedServiceEventDispatcher beforeIndexed =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeRead = new HookedServiceEventDispatcher(); HookedServiceEventDispatcher beforeRead = new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeCreated = HookedServiceEventDispatcher beforeCreated =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeModified = HookedServiceEventDispatcher beforeModified =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeUpdated = HookedServiceEventDispatcher beforeUpdated =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeRemoved = HookedServiceEventDispatcher beforeRemoved =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterIndexed = HookedServiceEventDispatcher afterIndexed =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterRead = new HookedServiceEventDispatcher(); HookedServiceEventDispatcher afterRead = new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterCreated = HookedServiceEventDispatcher afterCreated =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterModified = HookedServiceEventDispatcher afterModified =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterUpdated = HookedServiceEventDispatcher afterUpdated =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterRemoved = HookedServiceEventDispatcher afterRemoved =
new HookedServiceEventDispatcher(); new HookedServiceEventDispatcher();
HookedService(Service this.inner) { HookedService(Service this.inner) {
// Clone app instance // Clone app instance
if (inner.app != null) if (inner.app != null) this.app = inner.app;
this.app = inner.app;
routes.clear();
// Set up our routes. We still need to copy middleware from inner service // Set up our routes. We still need to copy middleware from inner service
Map restProvider = {'provider': Providers.REST}; Map restProvider = {'provider': Providers.REST};
// Add global middleware if declared on the instance itself // Add global middleware if declared on the instance itself
Middleware before = getAnnotation(inner, Middleware); Middleware before = getAnnotation(inner, Middleware);
if (before != null) { final handlers = [];
routes.add(new Route("*", "*", before.handlers));
} if (before != null) handlers.add(before.handlers);
Middleware indexMiddleware = getAnnotation(inner.index, Middleware); Middleware indexMiddleware = getAnnotation(inner.index, Middleware);
get('/', (req, res) async { get('/', (req, res) async {
return await this.index(mergeMap([req.query, restProvider])); return await this.index(mergeMap([req.query, restProvider]));
}, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers); },
middleware: []
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(inner.create, Middleware); Middleware createMiddleware = getAnnotation(inner.create, Middleware);
post('/', (req, res) async => await this.create(req.body, restProvider), post('/', (req, res) async => await this.create(req.body, restProvider),
middleware: middleware: []
(createMiddleware == null) ? [] : createMiddleware.handlers); ..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(inner.read, Middleware); Middleware readMiddleware = getAnnotation(inner.read, Middleware);
get( get(
'/:id', '/:id',
(req, res) async => await this (req, res) async => await this
.read(req.params['id'], mergeMap([req.query, restProvider])), .read(req.params['id'], mergeMap([req.query, restProvider])),
middleware: (readMiddleware == null) ? [] : readMiddleware.handlers); middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(inner.modify, Middleware); Middleware modifyMiddleware = getAnnotation(inner.modify, Middleware);
patch( patch(
'/:id', '/:id',
(req, res) async => (req, res) async =>
await this.modify(req.params['id'], req.body, restProvider), await this.modify(req.params['id'], req.body, restProvider),
middleware: middleware: []
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers); ..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(inner.update, Middleware); Middleware updateMiddleware = getAnnotation(inner.update, Middleware);
post( post(
'/:id', '/:id',
(req, res) async => (req, res) async =>
await this.update(req.params['id'], req.body, restProvider), await this.update(req.params['id'], req.body, restProvider),
middleware: middleware: []
(updateMiddleware == null) ? [] : updateMiddleware.handlers); ..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(inner.remove, Middleware); Middleware removeMiddleware = getAnnotation(inner.remove, Middleware);
delete( delete(
'/:id', '/:id',
(req, res) async => await this (req, res) async => await this
.remove(req.params['id'], mergeMap([req.query, restProvider])), .remove(req.params['id'], mergeMap([req.query, restProvider])),
middleware: middleware: []
(removeMiddleware == null) ? [] : removeMiddleware.handlers); ..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
} }
@override @override

View file

@ -1,6 +1,7 @@
/// Various libraries useful for creating highly-extensible servers. /// Various libraries useful for creating highly-extensible servers.
library angel_framework.http; library angel_framework.http;
export 'package:angel_route/angel_route.dart';
export 'angel_base.dart'; export 'angel_base.dart';
export 'angel_http_exception.dart'; export 'angel_http_exception.dart';
export 'base_middleware.dart'; export 'base_middleware.dart';
@ -12,7 +13,6 @@ export 'memory_service.dart';
export 'request_context.dart'; export 'request_context.dart';
export 'response_context.dart'; export 'response_context.dart';
export 'routable.dart'; export 'routable.dart';
export 'route.dart';
export 'server.dart'; export 'server.dart';
export 'service.dart'; export 'service.dart';

View file

@ -1,13 +1,18 @@
library angel_framework.http.request_context; library angel_framework.http.request_context;
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:angel_route/src/extensible.dart';
import 'package:body_parser/body_parser.dart'; import 'package:body_parser/body_parser.dart';
import '../../src/extensible.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'route.dart';
/// A convenience wrapper around an incoming HTTP request. /// A convenience wrapper around an incoming HTTP request.
class RequestContext extends Extensible { class RequestContext extends Extensible {
BodyParseResult _body;
ContentType _contentType;
String _path;
HttpRequest _underlyingRequest;
/// The [Angel] instance that is responding to this request. /// The [Angel] instance that is responding to this request.
AngelBase app; AngelBase app;
@ -27,59 +32,58 @@ class RequestContext extends Extensible {
String get method => underlyingRequest.method; String get method => underlyingRequest.method;
/// All post data submitted to the server. /// All post data submitted to the server.
Map body = {}; Map get body => _body.body;
/// The content type of an incoming request. /// The content type of an incoming request.
ContentType contentType; ContentType get contentType => _contentType;
/// Any and all files sent to the server with this request. /// Any and all files sent to the server with this request.
List<FileUploadInfo> files = []; List<FileUploadInfo> get files => _body.files;
/// The URL parameters extracted from the request URI. /// The URL parameters extracted from the request URI.
Map params = {}; Map params = {};
/// The requested path. /// The requested path.
String path; String get path => _path;
/// The parsed request query string. /// The parsed request query string.
Map query = {}; Map get query => _body.query;
/// The remote address requesting this resource. /// The remote address requesting this resource.
InternetAddress remoteAddress; InternetAddress get remoteAddress =>
underlyingRequest.connectionInfo.remoteAddress;
/// The route that matched this request.
Route route;
/// The user's HTTP session. /// The user's HTTP session.
HttpSession session; HttpSession get session => underlyingRequest.session;
/// The [Uri] instance representing the path this request is responding to.
Uri get uri => underlyingRequest.uri;
/// Is this an **XMLHttpRequest**? /// Is this an **XMLHttpRequest**?
bool get xhr => underlyingRequest.headers.value("X-Requested-With") bool get xhr =>
?.trim() underlyingRequest.headers
?.toLowerCase() == 'xmlhttprequest'; .value("X-Requested-With")
?.trim()
?.toLowerCase() ==
'xmlhttprequest';
/// The underlying [HttpRequest] instance underneath this context. /// The underlying [HttpRequest] instance underneath this context.
HttpRequest underlyingRequest; HttpRequest get underlyingRequest => _underlyingRequest;
/// Magically transforms an [HttpRequest] into a RequestContext. /// Magically transforms an [HttpRequest] into a [RequestContext].
static Future<RequestContext> from(HttpRequest request, static Future<RequestContext> from(HttpRequest request, AngelBase app) async {
Map parameters, AngelBase app, Route sourceRoute) async { RequestContext ctx = new RequestContext();
RequestContext context = new RequestContext();
context.app = app; ctx.app = app;
context.contentType = request.headers.contentType; ctx._contentType = request.headers.contentType;
context.remoteAddress = request.connectionInfo.remoteAddress; ctx._path = request.uri
context.params = parameters; .toString()
context.path = request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), ''); .replaceAll("?" + request.uri.query, "")
context.route = sourceRoute; .replaceAll(new RegExp(r'/+$'), '');
context.session = request.session; ctx._underlyingRequest = request;
context.underlyingRequest = request;
BodyParseResult bodyParseResult = await parseBody(request); ctx._body = await parseBody(request);
context.query = bodyParseResult.query;
context.body = bodyParseResult.body;
context.files = bodyParseResult.files;
return context; return ctx;
} }
} }

View file

@ -3,33 +3,35 @@ library angel_framework.http.response_context;
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import '../extensible.dart'; import '../extensible.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'controller.dart'; import 'controller.dart';
import 'route.dart';
/// A convenience wrapper around an outgoing HTTP request. /// A convenience wrapper around an outgoing HTTP request.
class ResponseContext extends Extensible { class ResponseContext extends Extensible {
bool _isOpen = true;
/// The [Angel] instance that is sending a response. /// The [Angel] instance that is sending a response.
AngelBase app; AngelBase app;
/// Can we still write to this response? /// Can we still write to this response?
bool isOpen = true; bool get isOpen => _isOpen;
/// A set of UTF-8 encoded bytes that will be written to the response. /// A set of UTF-8 encoded bytes that will be written to the response.
List<List<int>> responseData = []; final BytesBuilder buffer = new BytesBuilder();
/// Sets the status code to be sent with this response. /// Sets the status code to be sent with this response.
status(int code) { void status(int code) {
underlyingResponse.statusCode = code; underlyingResponse.statusCode = code;
} }
/// The underlying [HttpResponse] under this instance. /// The underlying [HttpResponse] under this instance.
HttpResponse underlyingResponse; final HttpResponse underlyingResponse;
ResponseContext(this.underlyingResponse); ResponseContext(this.underlyingResponse, this.app);
/// Any and all cookies to be sent to the user. /// Any and all cookies to be sent to the user.
List<Cookie> get cookies => underlyingResponse.cookies; List<Cookie> get cookies => underlyingResponse.cookies;
@ -38,31 +40,37 @@ class ResponseContext extends Extensible {
bool willCloseItself = false; bool willCloseItself = false;
/// Sends a download as a response. /// Sends a download as a response.
download(File file, {String filename}) { download(File file, {String filename}) async {
header("Content-Disposition", 'attachment; filename="${filename ?? file.path}"'); header("Content-Disposition",
'attachment; filename="${filename ?? file.path}"');
header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)); header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path));
header(HttpHeaders.CONTENT_LENGTH, file.lengthSync().toString()); header(HttpHeaders.CONTENT_LENGTH, file.lengthSync().toString());
responseData.add(file.readAsBytesSync()); buffer.add(await file.readAsBytes());
end();
} }
/// Prevents more data from being written to the response. /// Prevents more data from being written to the response.
end() => isOpen = false; void end() {
_isOpen = false;
}
/// Sets a response header to the given value, or retrieves its value. /// Sets a response header to the given value, or retrieves its value.
header(String key, [String value]) { header(String key, [String value]) {
if (value == null) return underlyingResponse.headers[key]; if (value == null)
else underlyingResponse.headers.set(key, value); return underlyingResponse.headers[key];
else
underlyingResponse.headers.set(key, value);
} }
/// Serializes JSON to the response. /// Serializes JSON to the response.
json(value) { void json(value) {
write(god.serialize(value)); write(god.serialize(value));
header(HttpHeaders.CONTENT_TYPE, ContentType.JSON.toString()); header(HttpHeaders.CONTENT_TYPE, ContentType.JSON.toString());
end(); end();
} }
/// Returns a JSONP response. /// Returns a JSONP response.
jsonp(value, {String callbackName: "callback"}) { void jsonp(value, {String callbackName: "callback"}) {
write("$callbackName(${god.serialize(value)})"); write("$callbackName(${god.serialize(value)})");
header(HttpHeaders.CONTENT_TYPE, "application/javascript"); header(HttpHeaders.CONTENT_TYPE, "application/javascript");
end(); end();
@ -76,7 +84,7 @@ class ResponseContext extends Extensible {
} }
/// Redirects to user to the given URL. /// Redirects to user to the given URL.
redirect(String url, {int code: 301}) { void redirect(String url, {int code: 301}) {
header(HttpHeaders.LOCATION, url); header(HttpHeaders.LOCATION, url);
status(code ?? 301); status(code ?? 301);
write(''' write('''
@ -100,22 +108,26 @@ class ResponseContext extends Extensible {
} }
/// Redirects to the given named [Route]. /// Redirects to the given named [Route].
redirectTo(String name, [Map params, int code]) { void redirectTo(String name, [Map params, int code]) {
// Todo: Need to recurse route hierarchy, but also efficiently :)
Route matched = app.routes.firstWhere((Route route) => route.name == name); Route matched = app.routes.firstWhere((Route route) => route.name == name);
if (matched != null) { if (matched != null) {
return redirect(matched.makeUri(params), code: code); redirect(matched.makeUri(params), code: code);
return;
} }
throw new ArgumentError.notNull('Route to redirect to ($name)'); throw new ArgumentError.notNull('Route to redirect to ($name)');
} }
/// Redirects to the given [Controller] action. /// Redirects to the given [Controller] action.
redirectToAction(String action, [Map params, int code]) { void redirectToAction(String action, [Map params, int code]) {
// UserController@show // UserController@show
List<String> split = action.split("@"); List<String> split = action.split("@");
// Todo: AngelResponseException
if (split.length < 2) if (split.length < 2)
throw new Exception("Controller redirects must take the form of 'Controller@action'. You gave: $action"); throw new Exception(
"Controller redirects must take the form of 'Controller@action'. You gave: $action");
Controller controller = app.controller(split[0]); Controller controller = app.controller(split[0]);
@ -125,38 +137,30 @@ class ResponseContext extends Extensible {
Route matched = controller.routeMappings[split[1]]; Route matched = controller.routeMappings[split[1]];
if (matched == null) if (matched == null)
throw new Exception("Controller '${split[0]}' does not contain any action named '${split[1]}'"); throw new Exception(
"Controller '${split[0]}' does not contain any action named '${split[1]}'");
return redirect(matched.makeUri(params), code: code); 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.
streamFile(File file, Future streamFile(File file,
{int chunkSize, int sleepMs: 0, bool resumable: true}) async { {int chunkSize, int sleepMs: 0, bool resumable: true}) async {
if (!isOpen) return; if (!isOpen) return;
header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)); header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path));
willCloseItself = true; willCloseItself = true;
await file.openRead().pipe(underlyingResponse); await file.openRead().pipe(underlyingResponse);
/*await chunked(file.openRead(), chunkSize: chunkSize,
sleepMs: sleepMs,
resumable: resumable);*/
} }
/// Writes data to the response. /// Writes data to the response.
write(value) { void write(value, {Encoding encoding: UTF8}) {
if (isOpen) if (isOpen) {
responseData.add(UTF8.encode(value.toString())); if (value is List<int>)
} buffer.add(value);
else buffer.add(encoding.encode(value.toString()));
/// Magically transforms an [HttpResponse] object into a ResponseContext. }
static Future<ResponseContext> from
(HttpResponse response, AngelBase app) async
{
ResponseContext context = new ResponseContext(response);
context.app = app;
return context;
} }
} }

View file

@ -2,8 +2,7 @@ library angel_framework.http.routable;
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:mirrors'; import 'package:angel_route/angel_route.dart';
import '../extensible.dart';
import '../util.dart'; import '../util.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'controller.dart'; import 'controller.dart';
@ -11,11 +10,8 @@ import 'hooked_service.dart';
import 'metadata.dart'; import 'metadata.dart';
import 'request_context.dart'; import 'request_context.dart';
import 'response_context.dart'; import 'response_context.dart';
import 'route.dart';
import 'service.dart'; import 'service.dart';
typedef Route RouteAssigner(Pattern path, handler, {List middleware});
/// A function that intercepts a request and determines whether handling of it should continue. /// A function that intercepts a request and determines whether handling of it should continue.
typedef Future<bool> RequestMiddleware(RequestContext req, ResponseContext res); typedef Future<bool> RequestMiddleware(RequestContext req, ResponseContext res);
@ -26,20 +22,26 @@ typedef Future RequestHandler(RequestContext req, ResponseContext res);
typedef Future RawRequestHandler(HttpRequest request); typedef Future RawRequestHandler(HttpRequest request);
/// A routable server that can handle dynamic requests. /// A routable server that can handle dynamic requests.
class Routable extends Extensible { class Routable extends Router {
/// Additional filters to be run on designated requests. final Map<String, Controller> _controllers = {};
Map <String, RequestMiddleware> requestMiddleware = {}; final Map<Pattern, Service> _services = {};
/// Dynamic request paths that this server will respond to. Routable({bool debug: false}) : super(debug: debug);
List<Route> routes = [];
/// Additional filters to be run on designated requests.
@override
final Map<String, RequestMiddleware> requestMiddleware = {};
/// 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> get services =>
new Map<Pattern, Service>.unmodifiable(_services);
/// A set of [Controller] objects that have been loaded into the application. /// A set of [Controller] objects that have been loaded into the application.
Map<String, Controller> controllers = {}; Map<String, Controller> get controllers =>
new Map<String, Controller>.unmodifiable(_controllers);
StreamController<Service> _onService = new StreamController<Service>.broadcast(); StreamController<Service> _onService =
new StreamController<Service>.broadcast();
/// Fired whenever a service is added to this instance. /// Fired whenever a service is added to this instance.
/// ///
@ -47,133 +49,80 @@ class Routable extends Extensible {
Stream<Service> get onService => _onService.stream; Stream<Service> get onService => _onService.stream;
/// Assigns a middleware to a name for convenience. /// Assigns a middleware to a name for convenience.
registerMiddleware(String name, RequestMiddleware middleware) { @override
this.requestMiddleware[name] = middleware; registerMiddleware(String name, RequestMiddleware middleware) =>
} super.registerMiddleware(name, middleware);
/// 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. /// Retrieves the controller with the given name.
Controller controller(String name) => controllers[name]; Controller controller(String name) => controllers[name];
/// Incorporates another [Routable]'s routes into this one's. @override
///
/// If `hooked` is set to `true` and a [Service] is provided,
/// then that service will be wired to a [HookedService] proxy.
/// If a `middlewareNamespace` is provided, then any middleware
/// from the provided [Routable] will be prefixed by that namespace,
/// with a dot.
/// For example, if the [Routable] has a middleware 'y', and the `middlewareNamespace`
/// is 'x', then that middleware will be available as 'x.y' in the main application.
/// These namespaces can be nested.
void use(Pattern path, Routable routable,
{bool hooked: true, String middlewareNamespace: null}) {
Routable _routable = routable;
// If we need to hook this service, do it here. It has to be first, or
// else all routes will point to the old service.
if (_routable is Service) {
Hooked hookedDeclaration = getAnnotation(_routable, Hooked);
Service service = (hookedDeclaration != null || hooked)
? new HookedService(_routable)
: _routable;
services[path.toString().trim().replaceAll(
new RegExp(r'(^\/+)|(\/+$)'), '')] = service;
_routable = service;
}
if (_routable is AngelBase) {
all(path, (RequestContext req, ResponseContext res) async {
req.app = _routable;
res.app = _routable;
return true;
});
}
for (Route route in _routable.routes) {
Route provisional = new Route('', path);
if (route.path == '/') {
route.path = '';
route.matcher = new RegExp(r'^\/?$');
}
route.matcher = new RegExp(route.matcher.pattern.replaceAll(
new RegExp('^\\^'),
provisional.matcher.pattern.replaceAll(new RegExp(r'\$$'), '')));
route.path = "$path${route.path}";
routes.add(route);
}
// Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = "";
if (middlewareNamespace != null)
middlewarePrefix = "$middlewareNamespace.";
for (String middlewareName in _routable.requestMiddleware.keys) {
requestMiddleware["$middlewarePrefix$middlewareName"] =
_routable.requestMiddleware[middlewareName];
}
// Copy services, too. :)
for (Pattern servicePath in _routable.services.keys) {
String newServicePath = path.toString().trim().replaceAll(
new RegExp(r'(^\/+)|(\/+$)'), '') + '/$servicePath';
services[newServicePath] = _routable.services[servicePath];
}
if (routable is Service)
_onService.add(routable);
}
/// Adds a route that responds to the given path
/// for requests with the given method (case-insensitive).
/// Provide '*' as the method to respond to all methods.
Route addRoute(String method, Pattern path, Object handler, Route addRoute(String method, Pattern path, Object handler,
{List middleware}) { {List middleware}) {
List handlers = []; final List handlers = [];
// Merge @Middleware declaration, if any // Merge @Middleware declaration, if any
Middleware middlewareDeclaration = getAnnotation( Middleware middlewareDeclaration = getAnnotation(handler, Middleware);
handler, Middleware);
if (middlewareDeclaration != null) { if (middlewareDeclaration != null) {
handlers.addAll(middlewareDeclaration.handlers); handlers.addAll(middlewareDeclaration.handlers);
} }
handlers return super.addRoute(method, path, handler,
..addAll(middleware ?? []) middleware: []..addAll(middleware ?? [])..addAll(handlers));
..add(handler);
var route = new Route(method.toUpperCase().trim(), path, handlers);
routes.add(route);
return route;
} }
/// Adds a route that responds to any request matching the given path. void use(Pattern path, Router router,
Route all(Pattern path, Object handler, {List middleware}) { {bool hooked: true, String namespace: null}) {
return addRoute('*', path, handler, middleware: middleware); Router _router = router;
} Service service;
/// Adds a route that responds to a GET request. // If we need to hook this service, do it here. It has to be first, or
Route get(Pattern path, Object handler, {List middleware}) { // else all routes will point to the old service.
return addRoute('GET', path, handler, middleware: middleware); if (router is Service) {
} Hooked hookedDeclaration = getAnnotation(router, Hooked);
_router = service = (hookedDeclaration != null || hooked)
? new HookedService(router)
: router;
_services[path
.toString()
.trim()
.replaceAll(new RegExp(r'(^/+)|(/+$)'), '')] = service;
}
/// Adds a route that responds to a POST request. final handlers = [];
Route post(Pattern path, Object handler, {List middleware}) { if (_router is AngelBase) {
return addRoute('POST', path, handler, middleware: middleware); handlers.add((RequestContext req, ResponseContext res) async {
} req.app = _router;
res.app = _router;
return true;
});
}
/// Adds a route that responds to a PATCH request. // Let's copy middleware, heeding the optional middleware namespace.
Route patch(Pattern path, Object handler, {List middleware}) { String middlewarePrefix = namespace != null ? "$namespace." : "";
return addRoute('PATCH', path, handler, middleware: middleware);
}
/// Adds a route that responds to a DELETE request. Map copiedMiddleware = new Map.from(router.requestMiddleware);
Route delete(Pattern path, Object handler, {List middleware}) { for (String middlewareName in copiedMiddleware.keys) {
return addRoute('DELETE', path, handler, middleware: middleware); requestMiddleware["$middlewarePrefix$middlewareName"] =
} copiedMiddleware[middlewareName];
}
Routable() { root.child(path, debug: debug, handlers: handlers).addChild(router.root);
}
_router.dumpTree(header: 'Mounting on "$path":');
if (router is Routable) {
// Copy services, too. :)
for (Pattern servicePath in _router._services.keys) {
String newServicePath =
path.toString().trim().replaceAll(new RegExp(r'(^/+)|(/+$)'), '') +
'/$servicePath';
_services[newServicePath] = _router._services[servicePath];
}
}
if (service != null) _onService.add(service);
}
} }

View file

@ -1,91 +0,0 @@
library angel_framework.http.route;
/// Represents an endpoint open for connection via the Internet.
class Route {
/// A regular expression used to match URI's to this route.
RegExp matcher;
/// The HTTP method this route responds to.
String method;
/// An array of functions, Futures and objects that can respond to this route.
List handlers = [];
/// The path this route is mounted on.
String path;
/// (Optional) - A name for this route.
String name;
Route(String method, Pattern path, [List handlers]) {
this.method = method;
if (path is RegExp) {
this.matcher = path;
this.path = path.pattern;
}
else {
this.matcher = new RegExp('^' +
path.toString()
.replaceAll(new RegExp(r'\/\*$'), "*")
.replaceAll(new RegExp('\/'), r'\/')
.replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)')
.replaceAll(new RegExp('\\*'), '.*')
+ r'$');
this.path = path;
}
if (handlers != null) {
this.handlers.addAll(handlers);
}
}
/// Assigns a name to this Route.
as(String name) {
this.name = name;
return this;
}
/// Generates a URI to this route with the given parameters.
String makeUri([Map<String, dynamic> params]) {
String result = path;
if (params != null) {
for (String key in (params.keys)) {
result = result.replaceAll(new RegExp(":$key" + r"\??"), params[key].toString());
}
}
return result.replaceAll("*", "");
}
/// Enables one or more handlers to be called whenever this route is visited.
Route middleware(handler) {
if (handler is Iterable)
handlers.addAll(handler);
else handlers.add(handler);
return this;
}
/// Extracts route parameters from a given path.
Map parseParameters(String requestPath) {
Map result = {};
Iterable<String> values = _parseParameters(requestPath);
RegExp rgx = new RegExp(':([a-zA-Z_]+)');
Iterable<Match> matches = rgx.allMatches(
path.replaceAll(new RegExp('\/'), r'\/'));
for (int i = 0; i < matches.length; i++) {
Match match = matches.elementAt(i);
String paramName = match.group(1);
String value = values.elementAt(i);
num numValue = num.parse(value, (_) => double.NAN);
if (!numValue.isNaN)
result[paramName] = numValue;
else
result[paramName] = value;
}
return result;
}
_parseParameters(String requestPath) sync* {
Match routeMatch = matcher.firstMatch(requestPath);
for (int i = 1; i <= routeMatch.groupCount; i++)
yield routeMatch.group(i);
}
}

View file

@ -11,10 +11,11 @@ import 'controller.dart';
import 'request_context.dart'; import 'request_context.dart';
import 'response_context.dart'; import 'response_context.dart';
import 'routable.dart'; import 'routable.dart';
import 'route.dart';
import 'service.dart'; import 'service.dart';
export 'package:container/container.dart'; export 'package:container/container.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// A function that binds an [Angel] server to an Internet address and port. /// A function that binds an [Angel] server to an Internet address and port.
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port); typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
@ -31,6 +32,7 @@ class Angel extends AngelBase {
var _beforeProcessed = new StreamController<HttpRequest>.broadcast(); var _beforeProcessed = new StreamController<HttpRequest>.broadcast();
var _fatalErrorStream = new StreamController<Map>.broadcast(); var _fatalErrorStream = new StreamController<Map>.broadcast();
var _onController = new StreamController<Controller>.broadcast(); var _onController = new StreamController<Controller>.broadcast();
final Random _rand = new Random.secure();
ServerGenerator _serverGenerator = ServerGenerator _serverGenerator =
(address, port) async => await HttpServer.bind(address, port); (address, port) async => await HttpServer.bind(address, port);
@ -74,6 +76,23 @@ class Angel extends AngelBase {
/// The native HttpServer running this instancce. /// The native HttpServer running this instancce.
HttpServer httpServer; HttpServer httpServer;
/// Handles a server error.
_onError(e, [StackTrace stackTrace]) {
_fatalErrorStream.add({"error": e, "stack": stackTrace});
}
void _printDebug(x) {
if (debug) print(x);
}
String _randomString(int length) {
var codeUnits = new List.generate(length, (index) {
return _rand.nextInt(33) + 89;
});
return new String.fromCharCodes(codeUnits);
}
/// Starts the server. /// Starts the server.
/// ///
/// Returns false on failure; otherwise, returns the HttpServer. /// Returns false on failure; otherwise, returns the HttpServer.
@ -81,10 +100,7 @@ class Angel extends AngelBase {
var server = await _serverGenerator( var server = await _serverGenerator(
address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0); address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0);
this.httpServer = server; this.httpServer = server;
return server..listen(handleRequest);
server.listen(handleRequest);
return server;
} }
/// Loads some base dependencies into the service container. /// Loads some base dependencies into the service container.
@ -95,75 +111,7 @@ class Angel extends AngelBase {
if (runtimeType != Angel) container.singleton(this, as: Angel); if (runtimeType != Angel) container.singleton(this, as: Angel);
} }
Future handleRequest(HttpRequest request) async { Future<bool> executeHandler(
_beforeProcessed.add(request);
String requestedUrl = request.uri
.toString()
.replaceAll("?" + request.uri.query, "")
.replaceAll(new RegExp(r'\/+$'), '');
if (requestedUrl.isEmpty) requestedUrl = '/';
RequestContext req = await RequestContext.from(request, {}, this, null);
ResponseContext res = await ResponseContext.from(request.response, this);
bool canContinue = true;
executeHandler(handler, req) async {
if (canContinue) {
try {
canContinue = await _applyHandler(handler, req, res);
} catch (e, stackTrace) {
if (e is AngelHttpException) {
// Special handling for AngelHttpExceptions :)
try {
res.status(e.statusCode);
String accept = request.headers.value(HttpHeaders.ACCEPT);
if (accept == "*/*" ||
accept.contains(ContentType.JSON.mimeType) ||
accept.contains("application/javascript")) {
res.json(e.toMap());
} else {
await _errorHandler(e, req, res);
}
_finalizeResponse(request, res);
} catch (_) {}
}
_onError(e, stackTrace);
canContinue = false;
return false;
}
} else
return false;
}
for (var handler in before) {
await executeHandler(handler, req);
}
for (Route route in routes) {
if (!canContinue) break;
if (route.matcher.hasMatch(requestedUrl) &&
(request.method == route.method || route.method == '*')) {
req.params = route.parseParameters(requestedUrl);
req.route = route;
for (var handler in route.handlers) {
await executeHandler(handler, req);
}
}
}
for (var handler in after) {
await executeHandler(handler, req);
}
_finalizeResponse(request, res);
}
Future<bool> _applyHandler(
handler, RequestContext req, ResponseContext res) async { handler, RequestContext req, ResponseContext res) async {
if (handler is RequestMiddleware) { if (handler is RequestMiddleware) {
var result = await handler(req, res); var result = await handler(req, res);
@ -216,7 +164,7 @@ class Angel extends AngelBase {
} }
if (requestMiddleware.containsKey(handler)) { if (requestMiddleware.containsKey(handler)) {
return await _applyHandler(requestMiddleware[handler], req, res); return await executeHandler(requestMiddleware[handler], req, res);
} }
res.willCloseItself = true; res.willCloseItself = true;
@ -225,11 +173,76 @@ class Angel extends AngelBase {
return false; return false;
} }
_finalizeResponse(HttpRequest request, ResponseContext res) async { Future handleRequest(HttpRequest request) async {
_beforeProcessed.add(request);
final req = await RequestContext.from(request, this);
final res = new ResponseContext(request.response, this);
String requestedUrl = request.uri
.toString()
.replaceAll("?" + request.uri.query, "")
.replaceAll(_straySlashes, '');
if (requestedUrl.isEmpty) requestedUrl = '/';
final route = resolve(requestedUrl,
(route) => route.method == request.method || route.method == '*');
print('Resolve ${requestedUrl} -> $route');
req.params.addAll(route?.parseParameters(requestedUrl) ?? {});
final handlerSequence = []..addAll(before);
if (route != null) handlerSequence.addAll(route.handlerSequence);
handlerSequence.addAll(after);
_printDebug('Handler sequence on $requestedUrl: $handlerSequence');
for (final handler in handlerSequence) {
try {
_printDebug('Executing handler: $handler');
final result = await executeHandler(handler, req, res);
_printDebug('Result: $result');
if (!result) {
_printDebug('Last executed handler: $handler');
break;
} else {
_printDebug(
'Handler completed successfully, did not terminate response: $handler');
}
} catch (e, st) {
_printDebug('Caught error in handler $handler: $e');
_printDebug(st);
if (e is AngelHttpException) {
// Special handling for AngelHttpExceptions :)
try {
res.status(e.statusCode);
String accept = request.headers.value(HttpHeaders.ACCEPT);
if (accept == "*/*" ||
accept.contains(ContentType.JSON.mimeType) ||
accept.contains("application/javascript")) {
res.json(e.toMap());
} else {
await _errorHandler(e, req, res);
}
_finalizeResponse(request, res);
} catch (_) {
// Todo: This exception needs to be caught as well.
}
} else {
// Todo: Uncaught exceptions need to be... Caught.
}
_onError(e, st);
break;
}
}
try { try {
_afterProcessed.add(request); _afterProcessed.add(request);
if (!res.willCloseItself) { if (!res.willCloseItself) {
res.responseData.forEach((blob) => request.response.add(blob)); request.response.add(res.buffer.takeBytes());
await request.response.close(); await request.response.close();
} }
} catch (e) { } catch (e) {
@ -237,15 +250,6 @@ class Angel extends AngelBase {
} }
} }
String _randomString(int length) {
var rand = new Random();
var codeUnits = new List.generate(length, (index) {
return rand.nextInt(33) + 89;
});
return new String.fromCharCodes(codeUnits);
}
// Run a function after injecting from service container // Run a function after injecting from service container
Future runContained(Function handler, RequestContext req, ResponseContext res, Future runContained(Function handler, RequestContext req, ResponseContext res,
{Map<String, dynamic> namedParameters, {Map<String, dynamic> namedParameters,
@ -302,12 +306,12 @@ class Angel extends AngelBase {
@override @override
use(Pattern path, Routable routable, use(Pattern path, Routable routable,
{bool hooked: true, String middlewareNamespace: null}) { {bool hooked: true, String namespace: null}) {
if (routable is Service) { if (routable is Service) {
routable.app = this; routable.app = this;
} }
return super.use(path, routable,
hooked: hooked, middlewareNamespace: middlewareNamespace); return super.use(path, routable, hooked: hooked, namespace: namespace);
} }
/// Registers a callback to run upon errors. /// Registers a callback to run upon errors.
@ -315,15 +319,7 @@ class Angel extends AngelBase {
_errorHandler = handler; _errorHandler = handler;
} }
/// Handles a server error. Angel({bool debug: false}) : super(debug: debug) {
_onError(e, [StackTrace stackTrace]) {
_fatalErrorStream.add({
"error": e,
"stack": stackTrace
});
}
Angel() : super() {
bootstrapContainer(); bootstrapContainer();
} }
@ -332,8 +328,8 @@ class Angel extends AngelBase {
/// If no password is provided, a random one will be generated upon running /// If no password is provided, a random one will be generated upon running
/// the server. /// the server.
Angel.secure(String certificateChainPath, String serverKeyPath, Angel.secure(String certificateChainPath, String serverKeyPath,
{String password}) {bool debug: false, String password})
: super() { : super(debug: debug) {
bootstrapContainer(); bootstrapContainer();
_serverGenerator = (InternetAddress address, int port) async { _serverGenerator = (InternetAddress address, int port) async {
var certificateChain = var certificateChain =

View file

@ -2,13 +2,11 @@ library angel_framework.http.service;
import 'dart:async'; import 'dart:async';
import 'package:merge_map/merge_map.dart'; import 'package:merge_map/merge_map.dart';
import '../defs.dart';
import '../util.dart'; import '../util.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'angel_http_exception.dart'; import 'angel_http_exception.dart';
import 'metadata.dart'; import 'metadata.dart';
import 'routable.dart'; import 'routable.dart';
import 'route.dart';
/// Indicates how the service was accessed. /// Indicates how the service was accessed.
/// ///
@ -72,19 +70,24 @@ class Service extends Routable {
// Add global middleware if declared on the instance itself // Add global middleware if declared on the instance itself
Middleware before = getAnnotation(this, Middleware); Middleware before = getAnnotation(this, Middleware);
if (before != null) { final handlers = [];
routes.add(new Route("*", "*", before.handlers));
} if (before != null) handlers.add(before.handlers);
Middleware indexMiddleware = getAnnotation(this.index, Middleware); Middleware indexMiddleware = getAnnotation(this.index, Middleware);
get('/', (req, res) async { get('/', (req, res) async {
return await this.index(mergeMap([req.query, restProvider])); return await this.index(mergeMap([req.query, restProvider]));
}, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers); },
middleware: []
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(this.create, Middleware); Middleware createMiddleware = getAnnotation(this.create, Middleware);
post('/', (req, res) async => await this.create(req.body, restProvider), post('/', (req, res) async => await this.create(req.body, restProvider),
middleware: middleware: []
(createMiddleware == null) ? [] : createMiddleware.handlers); ..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(this.read, Middleware); Middleware readMiddleware = getAnnotation(this.read, Middleware);
@ -92,30 +95,38 @@ class Service extends Routable {
'/:id', '/:id',
(req, res) async => await this (req, res) async => await this
.read(req.params['id'], mergeMap([req.query, restProvider])), .read(req.params['id'], mergeMap([req.query, restProvider])),
middleware: (readMiddleware == null) ? [] : readMiddleware.handlers); middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(this.modify, Middleware); Middleware modifyMiddleware = getAnnotation(this.modify, Middleware);
patch( patch(
'/:id', '/:id',
(req, res) async => (req, res) async =>
await this.modify(req.params['id'], req.body, restProvider), await this.modify(req.params['id'], req.body, restProvider),
middleware: middleware: []
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers); ..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(this.update, Middleware); Middleware updateMiddleware = getAnnotation(this.update, Middleware);
post( post(
'/:id', '/:id',
(req, res) async => (req, res) async =>
await this.update(req.params['id'], req.body, restProvider), await this.update(req.params['id'], req.body, restProvider),
middleware: middleware: []
(updateMiddleware == null) ? [] : updateMiddleware.handlers); ..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(this.remove, Middleware); Middleware removeMiddleware = getAnnotation(this.remove, Middleware);
delete( delete(
'/:id', '/:id',
(req, res) async => await this (req, res) async => await this
.remove(req.params['id'], mergeMap([req.query, restProvider])), .remove(req.params['id'], mergeMap([req.query, restProvider])),
middleware: middleware: []
(removeMiddleware == null) ? [] : removeMiddleware.handlers); ..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
} }
} }

View file

@ -4,6 +4,8 @@ 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
dependencies: dependencies:
angel_route:
path: ../angel_route
body_parser: ">=1.0.0-dev <2.0.0" body_parser: ">=1.0.0-dev <2.0.0"
container: ">=0.1.2 <1.0.0" container: ">=0.1.2 <1.0.0"
json_god: ">=2.0.0-beta <3.0.0" json_god: ">=2.0.0-beta <3.0.0"

View file

@ -26,32 +26,43 @@ main() {
http.Client client; http.Client client;
setUp(() async { setUp(() async {
angel = new Angel(); final debug = false;
nested = new Angel(); angel = new Angel(debug: debug);
todos = new Angel(); nested = new Angel(debug: debug);
todos = new Angel(debug: debug);
angel..registerMiddleware('interceptor', (req, res) async { angel
res.write('Middleware'); ..registerMiddleware('interceptor', (req, res) async {
return false; res.write('Middleware');
})..registerMiddleware('intercept_service', return false;
(RequestContext req, res) async { })
print("Intercepting a service!"); ..registerMiddleware('intercept_service',
return true; (RequestContext req, res) async {
}); print("Intercepting a service!");
return true;
});
todos.get('/action/:action', (req, res) => res.json(req.params)); todos.get('/action/:action', (req, res) => res.json(req.params));
nested.post('/ted/:route', (req, res) => res.json(req.params));
Route ted;
ted = nested.post('/ted/:route', (RequestContext req, res) {
print('Params: ${req.params}');
print('Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}');
return req.params;
});
angel.use('/nes', nested);
angel.get('/meta', testMiddlewareMetadata); angel.get('/meta', testMiddlewareMetadata);
angel.get('/intercepted', 'This should not be shown', angel.get('/intercepted', 'This should not be shown',
middleware: ['interceptor']); middleware: ['interceptor']);
angel.get('/hello', 'world'); angel.get('/hello', 'world');
angel.get('/name/:first/last/:last', (req, res) => req.params); angel.get('/name/:first/last/:last', (req, res) => req.params);
angel.post('/lambda', (req, res) => req.body); angel.post('/lambda', (req, res) => req.body);
angel.use('/nes', nested);
angel.use('/todos/:id', todos); angel.use('/todos/:id', todos);
angel angel
.get('/greet/:name', .get('/greet/:name',
(RequestContext req, res) async => "Hello ${req.params['name']}") (RequestContext req, res) async => "Hello ${req.params['name']}")
.as('Named routes'); .as('Named routes');
angel.get('/named', (req, ResponseContext res) async { angel.get('/named', (req, ResponseContext res) async {
res.redirectTo('Named routes', {'name': 'tests'}); res.redirectTo('Named routes', {'name': 'tests'});
@ -60,13 +71,11 @@ main() {
print("Query: ${req.query}"); print("Query: ${req.query}");
return "Logged"; return "Logged";
}); });
angel.use('/query', new QueryService()); angel.use('/query', new QueryService());
angel.get('*', 'MJ'); angel.get('*', 'MJ');
print("DUMPING ROUTES: "); angel.dumpTree(header: "DUMPING ROUTES:");
for (Route route in angel.routes) {
print("${route.method} ${route.path} - ${route.handlers}");
}
client = new http.Client(); client = new http.Client();
await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
@ -90,6 +99,7 @@ main() {
test('Can match url with multiple parameters', () async { test('Can match url with multiple parameters', () async {
var response = await client.get('$url/name/HELLO/last/WORLD'); var response = await client.get('$url/name/HELLO/last/WORLD');
print(response.body);
var json = god.deserialize(response.body); var json = god.deserialize(response.body);
expect(json['first'], equals('HELLO')); expect(json['first'], equals('HELLO'));
expect(json['last'], equals('WORLD')); expect(json['last'], equals('WORLD'));
@ -104,6 +114,7 @@ main() {
test('Can parse parameters from a nested Angel instance', () async { test('Can parse parameters from a nested Angel instance', () async {
var response = await client.get('$url/todos/1337/action/test'); var response = await client.get('$url/todos/1337/action/test');
var json = god.deserialize(response.body); var json = god.deserialize(response.body);
print('JSON: $json');
expect(json['id'], equals(1337)); expect(json['id'], equals(1337));
expect(json['action'], equals('test')); expect(json['action'], equals('test'));
}); });
@ -123,7 +134,7 @@ main() {
Map headers = {'Content-Type': 'application/json'}; Map headers = {'Content-Type': 'application/json'};
String postData = god.serialize({'it': 'works'}); String postData = god.serialize({'it': 'works'});
var response = var response =
await client.post("$url/lambda", headers: headers, body: postData); await client.post("$url/lambda", headers: headers, body: postData);
expect(god.deserialize(response.body)['it'], equals('works')); expect(god.deserialize(response.body)['it'], equals('works'));
}); });
@ -133,10 +144,11 @@ main() {
}); });
test('Can name routes', () { test('Can name routes', () {
Route foo = angel.get('/framework/:id', 'Angel').as('frm'); Route foo = new Route('/framework/:id', name: 'frm');
print('Foo: $foo');
String uri = foo.makeUri({'id': 'angel'}); String uri = foo.makeUri({'id': 'angel'});
print(uri); print(uri);
expect(uri, equals('/framework/angel')); expect(uri, equals('framework/angel'));
}); });
test('Redirect to named routes', () async { test('Redirect to named routes', () async {
@ -147,7 +159,7 @@ main() {
test('Match routes, even with query params', () async { test('Match routes, even with query params', () async {
var response = var response =
await client.get("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo"); await client.get("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo");
print(response.body); print(response.body);
expect(god.deserialize(response.body), equals('Logged')); expect(god.deserialize(response.body), equals('Logged'));

View file

@ -24,11 +24,9 @@ main() {
angel.properties['foo'] = () => 'bar'; angel.properties['foo'] = () => 'bar';
angel.properties['Foo'] = new Foo('bar'); angel.properties['Foo'] = new Foo('bar');
/**
expect(angel.hello, equals('world')); expect(angel.hello, equals('world'));
expect(angel.foo(), equals('bar')); expect(angel.foo(), equals('bar'));
expect(angel.Foo.name, equals('bar')); expect(angel.Foo.name, equals('bar'));
*/
}); });
}); });
} }