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;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:mirrors';
|
||||
import 'package:body_parser/body_parser.dart';
|
||||
import 'package:json_god/json_god.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:route/server.dart';
|
||||
|
||||
part 'request_context.dart';
|
||||
part 'response_context.dart';
|
||||
part 'route.dart';
|
||||
part 'routable.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;
|
||||
|
||||
typedef RouteAssigner(Pattern path, handler, {List middleware});
|
||||
|
||||
/// A routable server that can handle dynamic requests.
|
||||
class Routable {
|
||||
/// 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.
|
||||
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;
|
||||
|
||||
class Route {
|
||||
Pattern matcher;
|
||||
RegExp matcher;
|
||||
String method;
|
||||
List handlers = [];
|
||||
String path;
|
||||
|
||||
Route(String method, Pattern path, [List handlers]) {
|
||||
this.method = method;
|
||||
if (path is RegExp) this.matcher = path;
|
||||
else this.matcher = new RegExp('^' +
|
||||
path.toString().replaceAll(new RegExp('\/'), r'\/').replaceAll(
|
||||
new RegExp(':[a-zA-Z_]+'), '([^\/]+)') + r'$');
|
||||
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(':[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;
|
||||
|
||||
/// 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);
|
||||
|
||||
/// A function that configures an [Angel] server in some way.
|
||||
typedef AngelConfigurer(Angel app);
|
||||
|
||||
/// A powerful real-time/REST/MVC server class.
|
||||
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(
|
||||
address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
||||
this.httpServer = 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((
|
||||
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.
|
||||
void listen({InternetAddress address, int port: 3000}) {
|
||||
runZoned(() async {
|
||||
await _startServer(address, port);
|
||||
await startServer(address, port);
|
||||
}, onError: onError);
|
||||
}
|
||||
|
||||
/// 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() {}
|
||||
Angel() : super() {}
|
||||
|
||||
/// 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.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
dependencies:
|
||||
body_parser: ^1.0.0-dev
|
||||
json_god: any
|
||||
mime: ^0.9.3
|
||||
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