Route API change is breaking, haha.
This commit is contained in:
parent
1bb077a3d9
commit
551a7f086f
14 changed files with 447 additions and 571 deletions
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
||||||
|
|
|
@ -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'));
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue