Almost done.
This commit is contained in:
parent
31607686be
commit
95369495e4
12 changed files with 681 additions and 20 deletions
|
@ -2,10 +2,18 @@
|
||||||
library angel_framework.http;
|
library angel_framework.http;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:mirrors';
|
||||||
|
import 'package:body_parser/body_parser.dart';
|
||||||
import 'package:json_god/json_god.dart';
|
import 'package:json_god/json_god.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
import 'package:route/server.dart';
|
import 'package:route/server.dart';
|
||||||
|
|
||||||
|
part 'request_context.dart';
|
||||||
|
part 'response_context.dart';
|
||||||
part 'route.dart';
|
part 'route.dart';
|
||||||
part 'routable.dart';
|
part 'routable.dart';
|
||||||
part 'server.dart';
|
part 'server.dart';
|
||||||
|
part 'service.dart';
|
||||||
|
part 'services/memory.dart';
|
88
lib/src/http/request_context.dart
Normal file
88
lib/src/http/request_context.dart
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
part of angel_framework.http;
|
||||||
|
|
||||||
|
/// A function that intercepts a request and determines whether handling of it should continue.
|
||||||
|
typedef Future<bool> Middleware(RequestContext req, ResponseContext res);
|
||||||
|
|
||||||
|
/// A function that receives an incoming [RequestContext] and responds to it.
|
||||||
|
typedef Future RequestHandler(RequestContext req, ResponseContext res);
|
||||||
|
|
||||||
|
/// A function that handles an [HttpRequest].
|
||||||
|
typedef Future RawRequestHandler(HttpRequest request);
|
||||||
|
|
||||||
|
/// A convenience wrapper around an incoming HTTP request.
|
||||||
|
class RequestContext {
|
||||||
|
/// The [Angel] instance that is responding to this request.
|
||||||
|
Angel app;
|
||||||
|
|
||||||
|
/// Any cookies sent with this request.
|
||||||
|
List<Cookie> get cookies => underlyingRequest.cookies;
|
||||||
|
|
||||||
|
/// All HTTP headers sent with this request.
|
||||||
|
HttpHeaders get headers => underlyingRequest.headers;
|
||||||
|
|
||||||
|
/// The requested hostname.
|
||||||
|
String get hostname => underlyingRequest.headers.value(HttpHeaders.HOST);
|
||||||
|
|
||||||
|
/// The user's IP.
|
||||||
|
String get ip => remoteAddress.address;
|
||||||
|
|
||||||
|
/// This request's HTTP method.
|
||||||
|
String get method => underlyingRequest.method;
|
||||||
|
|
||||||
|
/// All post data submitted to the server.
|
||||||
|
Map body = {};
|
||||||
|
|
||||||
|
/// The content type of an incoming request.
|
||||||
|
ContentType contentType;
|
||||||
|
|
||||||
|
/// Any and all files sent to the server with this request.
|
||||||
|
List<FileUploadInfo> files = [];
|
||||||
|
|
||||||
|
/// The URL parameters extracted from the request URI.
|
||||||
|
Map params = {};
|
||||||
|
|
||||||
|
/// The requested path.
|
||||||
|
String path;
|
||||||
|
|
||||||
|
/// The parsed request query string.
|
||||||
|
Map query = {};
|
||||||
|
|
||||||
|
/// The remote address requesting this resource.
|
||||||
|
InternetAddress remoteAddress;
|
||||||
|
|
||||||
|
/// The route that matched this request.
|
||||||
|
Route route;
|
||||||
|
|
||||||
|
/// The user's HTTP session.
|
||||||
|
HttpSession session;
|
||||||
|
|
||||||
|
/// Is this an **XMLHttpRequest**?
|
||||||
|
bool get xhr => underlyingRequest.headers.value("X-Requested-With")
|
||||||
|
?.trim()
|
||||||
|
?.toLowerCase() == 'xmlhttprequest';
|
||||||
|
|
||||||
|
/// The underlying [HttpRequest] instance underneath this context.
|
||||||
|
HttpRequest underlyingRequest;
|
||||||
|
|
||||||
|
/// Magically transforms an [HttpRequest] into a RequestContext.
|
||||||
|
static Future<RequestContext> from(HttpRequest request,
|
||||||
|
Map parameters, Angel app, Route sourceRoute) async {
|
||||||
|
RequestContext context = new RequestContext();
|
||||||
|
|
||||||
|
context.app = app;
|
||||||
|
context.contentType = request.headers.contentType;
|
||||||
|
context.remoteAddress = request.connectionInfo.remoteAddress;
|
||||||
|
context.params = parameters;
|
||||||
|
context.path = request.uri.toString();
|
||||||
|
context.route = sourceRoute;
|
||||||
|
context.session = request.session;
|
||||||
|
context.underlyingRequest = request;
|
||||||
|
|
||||||
|
BodyParseResult bodyParseResult = await parseBody(request);
|
||||||
|
context.query = bodyParseResult.query;
|
||||||
|
context.body = bodyParseResult.body;
|
||||||
|
context.files = bodyParseResult.files;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
129
lib/src/http/response_context.dart
Normal file
129
lib/src/http/response_context.dart
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
part of angel_framework.http;
|
||||||
|
|
||||||
|
/// A function that asynchronously generates a view from the given path and data.
|
||||||
|
typedef Future<String> ViewGenerator(String path, {Map data});
|
||||||
|
|
||||||
|
/// A convenience wrapper around an outgoing HTTP request.
|
||||||
|
class ResponseContext {
|
||||||
|
/// The [Angel] instance that is sending a response.
|
||||||
|
Angel app;
|
||||||
|
|
||||||
|
God god = new God();
|
||||||
|
|
||||||
|
/// Can we still write to this response?
|
||||||
|
bool isOpen = true;
|
||||||
|
|
||||||
|
/// A set of UTF-8 encoded bytes that will be written to the response.
|
||||||
|
List<List<int>> responseData = [];
|
||||||
|
|
||||||
|
/// Sets the status code to be sent with this response.
|
||||||
|
status(int code) {
|
||||||
|
underlyingResponse.statusCode = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The underlying [HttpResponse] under this instance.
|
||||||
|
HttpResponse underlyingResponse;
|
||||||
|
|
||||||
|
ResponseContext(this.underlyingResponse);
|
||||||
|
|
||||||
|
/// Any and all cookies to be sent to the user.
|
||||||
|
List<Cookie> get cookies => underlyingResponse.cookies;
|
||||||
|
|
||||||
|
/// Set this to true if you will manually close the response.
|
||||||
|
bool willCloseItself = false;
|
||||||
|
|
||||||
|
/// Sends a download as a response.
|
||||||
|
download(File file, {String filename}) {
|
||||||
|
header("Content-Disposition",
|
||||||
|
'Content-Disposition: attachment; filename="${filename ?? file.path}"');
|
||||||
|
header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path));
|
||||||
|
header(HttpHeaders.CONTENT_LENGTH, file.lengthSync().toString());
|
||||||
|
responseData.add(file.readAsBytesSync());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prevents more data from being written to the response.
|
||||||
|
end() => isOpen = false;
|
||||||
|
|
||||||
|
/// Sets a response header to the given value, or retrieves its value.
|
||||||
|
header(String key, [String value]) {
|
||||||
|
if (value == null) return underlyingResponse.headers[key];
|
||||||
|
else underlyingResponse.headers.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes JSON to the response.
|
||||||
|
json(value) {
|
||||||
|
write(god.serialize(value));
|
||||||
|
header(HttpHeaders.CONTENT_TYPE, ContentType.JSON.toString());
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a JSONP response.
|
||||||
|
jsonp(value, {String callbackName: "callback"}) {
|
||||||
|
write("$callbackName(${god.serialize(value)})");
|
||||||
|
header(HttpHeaders.CONTENT_TYPE, "application/javascript");
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a view to the response stream, and closes the response.
|
||||||
|
Future render(String view, {Map data}) async {
|
||||||
|
/// TODO: app.viewGenerator
|
||||||
|
var generator = app.viewGenerator(view, data: data);
|
||||||
|
write(await generator);
|
||||||
|
header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString());
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redirects to user to the given URL.
|
||||||
|
redirect(String url, {int code: 301}) {
|
||||||
|
header(HttpHeaders.LOCATION, url);
|
||||||
|
status(code);
|
||||||
|
write('''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<meta http-equiv="refresh" content="0; url=$url">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Currently redirecting you...</h1>
|
||||||
|
<br />
|
||||||
|
Click <a href="$url"></a> if you are not automatically redirected...
|
||||||
|
<script>
|
||||||
|
window.location = "$url";
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''');
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams a file to this response as chunked data.
|
||||||
|
///
|
||||||
|
/// Useful for video sites.
|
||||||
|
streamFile(File file,
|
||||||
|
{int chunkSize, int sleepMs: 0, bool resumable: true}) async {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path));
|
||||||
|
willCloseItself = true;
|
||||||
|
await file.openRead().pipe(underlyingResponse);
|
||||||
|
/*await chunked(file.openRead(), chunkSize: chunkSize,
|
||||||
|
sleepMs: sleepMs,
|
||||||
|
resumable: resumable);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes data to the response.
|
||||||
|
write(value) {
|
||||||
|
if (isOpen)
|
||||||
|
responseData.add(UTF8.encode(value.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Magically transforms an [HttpResponse] object into a ResponseContext.
|
||||||
|
static Future<ResponseContext> from
|
||||||
|
(HttpResponse response, Angel app) async
|
||||||
|
{
|
||||||
|
ResponseContext context = new ResponseContext(response);
|
||||||
|
context.app = app;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,59 @@
|
||||||
part of angel_framework.http;
|
part of angel_framework.http;
|
||||||
|
|
||||||
|
typedef RouteAssigner(Pattern path, handler, {List middleware});
|
||||||
|
|
||||||
/// A routable server that can handle dynamic requests.
|
/// A routable server that can handle dynamic requests.
|
||||||
class Routable {
|
class Routable {
|
||||||
/// Additional filters to be run on designated requests.
|
/// Additional filters to be run on designated requests.
|
||||||
Map <String, Object> middleware = {};
|
Map <String, Middleware> middleware = {};
|
||||||
|
|
||||||
/// Dynamic request paths that this server will respond to.
|
/// Dynamic request paths that this server will respond to.
|
||||||
Map <Route, Object> routes = {};
|
List<Route> routes = [];
|
||||||
|
|
||||||
|
/// A set of [Service] objects that have been mapped into routes.
|
||||||
|
Map <Pattern, Service> services = {};
|
||||||
|
|
||||||
|
_makeRouteAssigner(String method) {
|
||||||
|
return (Pattern path, Object handler, {List middleware}) {
|
||||||
|
var route = new Route(method, path, (middleware ?? [])
|
||||||
|
..add(handler));
|
||||||
|
routes.add(route);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assigns a middleware to a name for convenience.
|
||||||
|
registerMiddleware(String name, Middleware middleware) {
|
||||||
|
this.middleware[name] = middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the service assigned to the given path.
|
||||||
|
Service service(Pattern path) => services[path];
|
||||||
|
|
||||||
|
/// Incorporates another routable's routes into this one's.
|
||||||
|
use(Pattern path, Routable routable) {
|
||||||
|
middleware.addAll(routable.middleware);
|
||||||
|
for (Route route in routable.routes) {
|
||||||
|
Route provisional = new Route('', path);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routable is Service) {
|
||||||
|
services[path.toString()] = routable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteAssigner get, post, patch, delete;
|
||||||
|
|
||||||
|
Routable() {
|
||||||
|
this.get = _makeRouteAssigner('GET');
|
||||||
|
this.post = _makeRouteAssigner('POST');
|
||||||
|
this.patch = _makeRouteAssigner('PATCH');
|
||||||
|
this.delete = _makeRouteAssigner('DELETE');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,14 +1,56 @@
|
||||||
part of angel_framework.http;
|
part of angel_framework.http;
|
||||||
|
|
||||||
class Route {
|
class Route {
|
||||||
Pattern matcher;
|
RegExp matcher;
|
||||||
String method;
|
String method;
|
||||||
|
List handlers = [];
|
||||||
|
String path;
|
||||||
|
|
||||||
Route(String method, Pattern path, [List handlers]) {
|
Route(String method, Pattern path, [List handlers]) {
|
||||||
this.method = method;
|
this.method = method;
|
||||||
if (path is RegExp) this.matcher = path;
|
if (path is RegExp) {
|
||||||
else this.matcher = new RegExp('^' +
|
this.matcher = path;
|
||||||
path.toString().replaceAll(new RegExp('\/'), r'\/').replaceAll(
|
this.path = path.pattern;
|
||||||
new RegExp(':[a-zA-Z_]+'), '([^\/]+)') + r'$');
|
}
|
||||||
|
else {
|
||||||
|
this.matcher = new RegExp('^' +
|
||||||
|
path.toString()
|
||||||
|
.replaceAll(new RegExp('\/'), r'\/')
|
||||||
|
.replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)')
|
||||||
|
.replaceAll(new RegExp('\\*'), '.*')
|
||||||
|
+ r'$');
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handlers != null) {
|
||||||
|
this.handlers.addAll(handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,136 @@
|
||||||
part of angel_framework.http;
|
part of angel_framework.http;
|
||||||
|
|
||||||
/// A function that binds
|
/// 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);
|
||||||
|
|
||||||
|
/// A function that configures an [Angel] server in some way.
|
||||||
|
typedef AngelConfigurer(Angel app);
|
||||||
|
|
||||||
/// A powerful real-time/REST/MVC server class.
|
/// A powerful real-time/REST/MVC server class.
|
||||||
class Angel extends Routable {
|
class Angel extends Routable {
|
||||||
ServerGenerator _serverGenerator;
|
ServerGenerator _serverGenerator = (address, port) async => await HttpServer
|
||||||
|
.bind(address, port);
|
||||||
|
var viewGenerator = (String view, {Map data}) => {};
|
||||||
|
|
||||||
_startServer(InternetAddress address, int port) async {
|
HttpServer httpServer;
|
||||||
|
God god = new God();
|
||||||
|
|
||||||
|
/// A set of custom properties that can be assigned to the server.
|
||||||
|
///
|
||||||
|
/// Useful for configuration and extension.
|
||||||
|
Map properties = {};
|
||||||
|
|
||||||
|
startServer(InternetAddress address, int port) async {
|
||||||
var server = await _serverGenerator(
|
var server = await _serverGenerator(
|
||||||
address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
||||||
|
this.httpServer = server;
|
||||||
var router = new Router(server);
|
var router = new Router(server);
|
||||||
|
|
||||||
this.routes.forEach((Route route, value) {
|
this.routes.forEach((Route route) {
|
||||||
router.serve(route.matcher, method: route.method).listen((
|
router.serve(route.matcher, method: route.method).listen((
|
||||||
HttpRequest request) {
|
HttpRequest request) async {
|
||||||
|
RequestContext req = await RequestContext.from(
|
||||||
|
request, route.parseParameters(request.uri.toString()), this,
|
||||||
|
route);
|
||||||
|
ResponseContext res = await ResponseContext.from(
|
||||||
|
request.response, this);
|
||||||
|
bool canContinue = true;
|
||||||
|
|
||||||
|
for (var handler in route.handlers) {
|
||||||
|
if (canContinue) {
|
||||||
|
canContinue = await new Future<bool>.sync(() async {
|
||||||
|
return _applyHandler(handler, req, res);
|
||||||
|
}).catchError((e) {
|
||||||
|
stderr.write(e.error);
|
||||||
|
canContinue = false;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.willCloseItself) {
|
||||||
|
res.responseData.forEach((blob) => request.response.add(blob));
|
||||||
|
await request.response.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _applyHandler(handler, RequestContext req,
|
||||||
|
ResponseContext res) async {
|
||||||
|
if (handler is Middleware) {
|
||||||
|
return await handler(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (handler is RequestHandler) {
|
||||||
|
await handler(req, res);
|
||||||
|
return res.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (handler is RawRequestHandler) {
|
||||||
|
var result = await handler(req.underlyingRequest);
|
||||||
|
return result is bool && result == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (handler is Function || handler is Future) {
|
||||||
|
var result = await handler();
|
||||||
|
return result is bool && result == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (middleware.containsKey(handler)) {
|
||||||
|
return await _applyHandler(middleware[handler], req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
res.willCloseItself = true;
|
||||||
|
res.underlyingResponse.write(god.serialize(handler));
|
||||||
|
await res.underlyingResponse.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies an [AngelConfigurer] to this instance.
|
||||||
|
void configure(AngelConfigurer configurer) {
|
||||||
|
configurer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the server.
|
/// Starts the server.
|
||||||
void listen({InternetAddress address, int port: 3000}) {
|
void listen({InternetAddress address, int port: 3000}) {
|
||||||
runZoned(() async {
|
runZoned(() async {
|
||||||
await _startServer(address, port);
|
await startServer(address, port);
|
||||||
}, onError: onError);
|
}, onError: onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a server error.
|
/// Handles a server error.
|
||||||
void onError(e, [StackTrace stackTrace]) {
|
var onError = (e, [StackTrace stackTrace]) {
|
||||||
|
stderr.write(e.toString());
|
||||||
|
if (stackTrace != null)
|
||||||
|
stderr.write(stackTrace.toString());
|
||||||
|
};
|
||||||
|
|
||||||
}
|
Angel() : super() {}
|
||||||
|
|
||||||
Angel() {}
|
|
||||||
|
|
||||||
/// Creates an HTTPS server.
|
/// Creates an HTTPS server.
|
||||||
Angel.secure() {}
|
Angel.secure() : super() {}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
54
lib/src/http/service.dart
Normal file
54
lib/src/http/service.dart
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
part of angel_framework.http;
|
||||||
|
|
||||||
|
/// A data store exposed to the Internet.
|
||||||
|
class Service extends Routable {
|
||||||
|
|
||||||
|
/// Retrieves all resources.
|
||||||
|
Future<List> index([Map params]) {
|
||||||
|
throw new MethodNotAllowedError('find');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the desired resource.
|
||||||
|
Future<Object> read(id, [Map params]) {
|
||||||
|
throw new MethodNotAllowedError('get');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a resource.
|
||||||
|
Future<Object> create(Map data, [Map params]) {
|
||||||
|
throw new MethodNotAllowedError('create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifies a resource.
|
||||||
|
Future<Object> update(id, Map data, [Map params]) {
|
||||||
|
throw new MethodNotAllowedError('update');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the given resource.
|
||||||
|
Future<Object> remove(id, [Map params]) {
|
||||||
|
throw new MethodNotAllowedError('remove');
|
||||||
|
}
|
||||||
|
|
||||||
|
Service() : super() {
|
||||||
|
get('/', (req, res) async => res.json(await this.index(req.query)));
|
||||||
|
get('/:id', (req, res) async =>
|
||||||
|
res.json(await this.read(req.params['id'], req.query)));
|
||||||
|
post('/', (req, res) async => res.json(await this.create(req.body))g);
|
||||||
|
post('/:id', (req, res) async =>
|
||||||
|
res.json(await this.update(req.params['id'], req.body)));
|
||||||
|
delete('/:id', (req, res) async =>
|
||||||
|
res.json(await this.remove(req.params['id'], req.body)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thrown when an unimplemented method is called.
|
||||||
|
class MethodNotAllowedError extends Error {
|
||||||
|
/// The action that threw the error.
|
||||||
|
///
|
||||||
|
/// Ex. 'get', 'remove'
|
||||||
|
String action;
|
||||||
|
|
||||||
|
/// A description of this error.
|
||||||
|
String get error => 'This service does not support the "$action" action.';
|
||||||
|
|
||||||
|
MethodNotAllowedError(String this.action);
|
||||||
|
}
|
31
lib/src/http/services/memory.dart
Normal file
31
lib/src/http/services/memory.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
part of angel_framework.http;
|
||||||
|
|
||||||
|
/// An in-memory [Service].
|
||||||
|
class MemoryService<T> extends Service {
|
||||||
|
God god = new God();
|
||||||
|
Map <int, T> items = {};
|
||||||
|
|
||||||
|
Future<List> index([Map params]) async => items.values.toList();
|
||||||
|
|
||||||
|
Future<Object> read(id, [Map params]) async => items[int.parse(id)];
|
||||||
|
|
||||||
|
Future<Object> create(Map data, [Map params]) async {
|
||||||
|
data['id'] = items.length;
|
||||||
|
items[items.length] = god.deserializeFromMap(data, T);
|
||||||
|
return items[items.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object> update(id, Map data, [Map params]) async {
|
||||||
|
data['id'] = int.parse(id);
|
||||||
|
items[int.parse(id)] = god.deserializeFromMap(data, T);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object> remove(id, [Map params]) async {
|
||||||
|
var item = items[int.parse(id)];
|
||||||
|
items.remove(int.parse(id));
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryService() : super();
|
||||||
|
}
|
|
@ -3,5 +3,10 @@ version: 0.0.1-dev
|
||||||
description: Core libraries for the Angel framework.
|
description: Core libraries for the Angel framework.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
dependencies:
|
dependencies:
|
||||||
|
body_parser: ^1.0.0-dev
|
||||||
json_god: any
|
json_god: any
|
||||||
|
mime: ^0.9.3
|
||||||
route: any
|
route: any
|
||||||
|
dev_dependencies:
|
||||||
|
http: any
|
||||||
|
test: any
|
72
test/routing.dart
Normal file
72
test/routing.dart
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:json_god/json_god.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
group('routing', () {
|
||||||
|
Angel angel;
|
||||||
|
Angel nested;
|
||||||
|
Angel todos;
|
||||||
|
String url;
|
||||||
|
http.Client client;
|
||||||
|
God god;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
god = new God();
|
||||||
|
angel = new Angel();
|
||||||
|
nested = new Angel();
|
||||||
|
todos = new Angel();
|
||||||
|
|
||||||
|
todos.get('/action/:action', (req, res) => res.json(req.params));
|
||||||
|
|
||||||
|
nested.post('/ted/:route', (req, res) => res.json(req.params));
|
||||||
|
|
||||||
|
angel.get('/hello', 'world');
|
||||||
|
angel.get('/name/:first/last/:last', (req, res) => res.json(req.params));
|
||||||
|
angel.use('/nes', nested);
|
||||||
|
angel.use('/todos/:id', todos);
|
||||||
|
|
||||||
|
client = new http.Client();
|
||||||
|
await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
||||||
|
url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}";
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await angel.httpServer.close(force: true);
|
||||||
|
angel = null;
|
||||||
|
nested = null;
|
||||||
|
todos = null;
|
||||||
|
client.close();
|
||||||
|
client = null;
|
||||||
|
url = null;
|
||||||
|
god = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can match basic url', () async {
|
||||||
|
var response = await client.get("$url/hello");
|
||||||
|
expect(response.body, equals('"world"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can match url with multiple parameters', () async {
|
||||||
|
var response = await client.get('$url/name/HELLO/last/WORLD');
|
||||||
|
var json = god.deserialize(response.body);
|
||||||
|
expect(json['first'], equals('HELLO'));
|
||||||
|
expect(json['last'], equals('WORLD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can nest another Angel instance', () async {
|
||||||
|
var response = await client.post('$url/nes/ted/foo');
|
||||||
|
var json = god.deserialize(response.body);
|
||||||
|
expect(json['route'], equals('foo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can parse parameters from a nested Angel instance', () async {
|
||||||
|
var response = await client.get('$url/todos/1337/action/test');
|
||||||
|
var json = god.deserialize(response.body);
|
||||||
|
expect(json['id'], equals(1337));
|
||||||
|
expect(json['action'], equals('test'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
54
test/services.dart
Normal file
54
test/services.dart
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:json_god/json_god.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class Todo {
|
||||||
|
int id;
|
||||||
|
String text;
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
group('Utilities', () {
|
||||||
|
Map headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
Angel angel;
|
||||||
|
String url;
|
||||||
|
http.Client client;
|
||||||
|
God god;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
angel = new Angel();
|
||||||
|
client = new http.Client();
|
||||||
|
god = new God();
|
||||||
|
angel.use('/todos', new MemoryService<Todo>());
|
||||||
|
await angel.startServer(null, 0);
|
||||||
|
url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}";
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
angel = null;
|
||||||
|
url = null;
|
||||||
|
client.close();
|
||||||
|
client = null;
|
||||||
|
god = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
group('memory', () {
|
||||||
|
test('can index an empty service', () async {
|
||||||
|
var response = await client.get("$url/todos/");
|
||||||
|
expect(response.body, equals('[]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can create data', () async {
|
||||||
|
String postData = god.serialize({'text': 'Hello, world!'});
|
||||||
|
var response = await client.post(
|
||||||
|
"$url/todos/", headers: headers, body: postData);
|
||||||
|
var json = god.deserialize(response.body);
|
||||||
|
print(json);
|
||||||
|
expect(json['text'], equals('Hello, world!'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
32
test/util.dart
Normal file
32
test/util.dart
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
String name;
|
||||||
|
|
||||||
|
Foo(String this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
group('Utilities', () {
|
||||||
|
Angel angel;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
angel = new Angel();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
angel = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can use app.properties like members', () {
|
||||||
|
angel.properties['hello'] = 'world';
|
||||||
|
angel.properties['foo'] = () => 'bar';
|
||||||
|
angel.properties['Foo'] = new Foo('bar');
|
||||||
|
|
||||||
|
expect(angel.hello, equals('world'));
|
||||||
|
expect(angel.foo(), equals('bar'));
|
||||||
|
expect(angel.Foo.name, equals('bar'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue