diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml
new file mode 100644
index 00000000..11ef66c4
--- /dev/null
+++ b/.idea/dbnavigator.xml
@@ -0,0 +1,451 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 48544501..e46408c2 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -2,9 +2,18 @@
+
+
+
+
+
-
-
+
+
+
+
+
+
@@ -17,7 +26,7 @@
-
+
@@ -27,122 +36,48 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -236,7 +187,7 @@
-
+
@@ -249,7 +200,92 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -357,19 +393,44 @@
false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -402,7 +463,8 @@
-
+
+
1481237183504
@@ -516,43 +578,50 @@
1497837231709
-
+
+ 1497985995955
+
+
+
+ 1497985995955
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
@@ -565,26 +634,29 @@
+
-
+
+
+
-
+
+
+
+
+
-
-
+
-
-
-
@@ -610,70 +682,14 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -774,13 +790,6 @@
-
-
-
-
-
-
-
@@ -795,13 +804,6 @@
-
-
-
-
-
-
-
@@ -809,20 +811,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -830,13 +818,6 @@
-
-
-
-
-
-
-
@@ -880,14 +861,6 @@
-
-
-
-
-
-
-
-
@@ -898,9 +871,9 @@
-
+
-
+
@@ -922,19 +895,9 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+
@@ -948,7 +911,7 @@
-
+
@@ -957,17 +920,9 @@
-
-
-
-
-
-
-
-
-
+
@@ -976,8 +931,8 @@
-
-
+
+
@@ -986,10 +941,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart
index 60b4124c..2c869965 100644
--- a/lib/src/http/angel_base.dart
+++ b/lib/src/http/angel_base.dart
@@ -28,6 +28,5 @@ class AngelBase extends Routable {
/// A function that renders views.
///
/// Called by [ResponseContext]@`render`.
- ViewGenerator viewGenerator = (String view,
- [Map data]) async => "No view engine has been configured yet.";
+ ViewGenerator viewGenerator = (String view, [Map data]) async => "No view engine has been configured yet.";
}
\ No newline at end of file
diff --git a/lib/src/http/angel_http_exception.dart b/lib/src/http/angel_http_exception.dart
index 54cb14a0..781b5643 100644
--- a/lib/src/http/angel_http_exception.dart
+++ b/lib/src/http/angel_http_exception.dart
@@ -32,7 +32,7 @@ class AngelHttpException implements Exception {
Map toJson() {
return {
'isError': true,
- 'statusCode': statusCode,
+ 'status_code': statusCode,
'message': message,
'errors': errors
};
@@ -47,7 +47,7 @@ class AngelHttpException implements Exception {
factory AngelHttpException.fromMap(Map data) {
return new AngelHttpException(null,
- statusCode: data['statusCode'],
+ statusCode: data['status_code'] ?? data['statusCode'],
message: data['message'],
errors: data['errors']);
}
diff --git a/lib/src/http/anonymous_service.dart b/lib/src/http/anonymous_service.dart
index dbe927eb..9e578be1 100644
--- a/lib/src/http/anonymous_service.dart
+++ b/lib/src/http/anonymous_service.dart
@@ -8,7 +8,7 @@ class AnonymousService extends Service {
Function _index, _read, _create, _modify, _update, _remove;
AnonymousService(
- {Future index([Map params]),
+ {Future index([Map params]),
Future read(id, [Map params]),
Future create(data, [Map params]),
Future modify(id, data, [Map params]),
diff --git a/lib/src/http/typed_service.dart b/lib/src/http/typed_service.dart
index eb883119..9ee37834 100644
--- a/lib/src/http/typed_service.dart
+++ b/lib/src/http/typed_service.dart
@@ -15,7 +15,7 @@ class TypedService extends Service {
deserialize(x) {
// print('DESERIALIZE: $x (${x.runtimeType})');
- if (x == dynamic || x == Object || x is T)
+ if (x is Type || x is T)
return x;
else if (x is Iterable)
return x.map(deserialize).toList();
@@ -23,7 +23,11 @@ class TypedService extends Service {
Map data = x.keys.fold({}, (map, key) {
var value = x[key];
- if ((key == 'createdAt' || key == 'updatedAt') && value is String) {
+ if ((key == 'createdAt' ||
+ key == 'updatedAt' ||
+ key == 'created_at' ||
+ key == 'updated_at') &&
+ value is String) {
return map..[key] = DateTime.parse(value);
} else {
return map..[key] = value;
@@ -32,16 +36,16 @@ class TypedService extends Service {
Model result = god.deserializeDatum(data, outputType: T);
- if (x['createdAt'] is String) {
- result.createdAt = DateTime.parse(x['createdAt']);
- } else if (x['createdAt'] is DateTime) {
- result.createdAt = x['createdAt'];
+ if (data['createdAt'] is DateTime) {
+ result.createdAt = data['createdAt'];
+ } else if (data['created_at'] is DateTime) {
+ result.createdAt = data['created_at'];
}
- if (x['updatedAt'] is String) {
- result.updatedAt = DateTime.parse(x['updatedAt']);
- } else if (x['updatedAt'] is DateTime) {
- result.updatedAt = x['updatedAt'];
+ if (data['updatedAt'] is DateTime) {
+ result.updatedAt = data['updatedAt'];
+ } else if (data['updated_at'] is DateTime) {
+ result.updatedAt = data['updated_at'];
}
// print('x: $x\nresult: $result');
diff --git a/test/all.dart b/test/all.dart
index c68df6c9..a4910443 100644
--- a/test/all.dart
+++ b/test/all.dart
@@ -1,23 +1,31 @@
+import 'anonymous_service_test.dart' as anonymous_service;
import 'controller_test.dart' as controller;
import 'di_test.dart' as di;
+import 'exception_test.dart' as exception;
import 'general_test.dart' as general;
import 'hooked_test.dart' as hooked;
import 'precontained_test.dart' as precontained;
import 'routing_test.dart' as routing;
import 'serialize_test.dart' as serialize;
import 'services_test.dart' as services;
+import 'typed_service_test.dart' as typed_service;
import 'util_test.dart' as util;
+import 'view_generator_test.dart' as view_generator;
import 'package:test/test.dart';
/// For running with coverage
main() {
+ group('anonymous service', anonymous_service.main);
group('controller', controller.main);
group('di', di.main);
+ group('exception', exception.main);
group('general', general.main);
group('hooked', hooked.main);
group('precontained', precontained.main);
group('routing', routing.main);
group('serialize', serialize.main);
group('services', services.main);
+ group('typed_service', typed_service.main);
group('util', util.main);
-}
\ No newline at end of file
+ group('view generator', view_generator.main);
+}
diff --git a/test/anonymous_service_test.dart b/test/anonymous_service_test.dart
new file mode 100644
index 00000000..7df3601b
--- /dev/null
+++ b/test/anonymous_service_test.dart
@@ -0,0 +1,44 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:test/test.dart';
+
+main() {
+ test('custom methods', () async {
+ var svc = new AnonymousService(
+ index: ([p]) async => 'index',
+ read: (id, [p]) async => 'read',
+ create: (data, [p]) async => 'create',
+ modify: (id, data, [p]) async => 'modify',
+ update: (id, data, [p]) async => 'update',
+ remove: (id, [p]) async => 'remove');
+ expect(await svc.index(), 'index');
+ expect(await svc.read(null), 'read');
+ expect(await svc.create(null), 'create');
+ expect(await svc.modify(null, null), 'modify');
+ expect(await svc.update(null, null), 'update');
+ expect(await svc.remove(null), 'remove');
+ });
+
+ test('defaults to throwing', () async {
+ try {
+ var svc = new AnonymousService();
+ await svc.read(1);
+ throw 'Should have thrown 405!';
+ } on AngelHttpException {
+ // print('Ok!');
+ }
+ try {
+ var svc = new AnonymousService();
+ await svc.modify(2, null);
+ throw 'Should have thrown 405!';
+ } on AngelHttpException {
+ // print('Ok!');
+ }
+ try {
+ var svc = new AnonymousService();
+ await svc.update(3, null);
+ throw 'Should have thrown 405!';
+ } on AngelHttpException {
+ // print('Ok!');
+ }
+ });
+}
diff --git a/test/controller_test.dart b/test/controller_test.dart
index c023ff38..e56000ae 100644
--- a/test/controller_test.dart
+++ b/test/controller_test.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:http/http.dart' as http;
+import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart';
import 'common.dart';
@@ -25,8 +26,19 @@ class TodoController extends Controller {
}
}
+class NoExposeController extends Controller {
+
+}
+
+@Expose('/named', as: 'foo')
+class NamedController extends Controller {
+ @Expose('/optional/:arg?', allowNull: const ['arg'])
+ optional() => 2;
+}
+
main() {
Angel app;
+ TodoController ctrl;
HttpServer server;
http.Client client = new http.Client();
String url;
@@ -39,7 +51,7 @@ main() {
"/redirect",
(req, ResponseContext res) async =>
res.redirectToAction("TodoController@foo", {"foo": "world"}));
- await app.configure(new TodoController());
+ await app.configure(ctrl = new TodoController());
print(app.controllers);
app.dumpTree();
@@ -54,6 +66,39 @@ main() {
url = null;
});
+ test('basic', () {
+ expect(ctrl.app, app);
+ });
+
+ test('require expose', () async {
+ try {
+ var app = new Angel();
+ await app.configure(new NoExposeController());
+ throw 'Should require @Expose';
+ } on Exception {
+ // :)
+ }
+ });
+
+ test('create dynamic handler', () async {
+ var app = new Angel();
+ app.get('/foo', createDynamicHandler(({String bar}) {
+ return 2;
+ }, optional: [
+ 'bar'
+ ]));
+ var rq = new MockHttpRequest('GET', new Uri(path: 'foo'));
+ await app.handleRequest(rq);
+ var body = await rq.response.transform(UTF8.decoder).join();
+ expect(JSON.decode(body), 2);
+ });
+
+ test('optional name', () async {
+ var app = new Angel();
+ await app.configure(new NamedController());
+ expect(app.controllers['foo'], new isInstanceOf());
+ });
+
test("middleware", () async {
var rgx = new RegExp("^Hello, world!");
var response = await client.get("$url/todos/0");
diff --git a/test/exception_test.dart b/test/exception_test.dart
new file mode 100644
index 00000000..f1db47ff
--- /dev/null
+++ b/test/exception_test.dart
@@ -0,0 +1,97 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart';
+
+main() {
+ test('named constructors', () {
+ expect(new AngelHttpException.badRequest(),
+ isException(HttpStatus.BAD_REQUEST, '400 Bad Request'));
+ expect(new AngelHttpException.BadRequest(),
+ isException(HttpStatus.BAD_REQUEST, '400 Bad Request'));
+ expect(new AngelHttpException.notAuthenticated(),
+ isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated'));
+ expect(new AngelHttpException.NotAuthenticated(),
+ isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated'));
+ expect(new AngelHttpException.paymentRequired(),
+ isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required'));
+ expect(new AngelHttpException.PaymentRequired(),
+ isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required'));
+ expect(new AngelHttpException.forbidden(),
+ isException(HttpStatus.FORBIDDEN, '403 Forbidden'));
+ expect(new AngelHttpException.Forbidden(),
+ isException(HttpStatus.FORBIDDEN, '403 Forbidden'));
+ expect(new AngelHttpException.notFound(),
+ isException(HttpStatus.NOT_FOUND, '404 Not Found'));
+ expect(new AngelHttpException.NotFound(),
+ isException(HttpStatus.NOT_FOUND, '404 Not Found'));
+ expect(new AngelHttpException.methodNotAllowed(),
+ isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed'));
+ expect(new AngelHttpException.MethodNotAllowed(),
+ isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed'));
+ expect(new AngelHttpException.notAcceptable(),
+ isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable'));
+ expect(new AngelHttpException.NotAcceptable(),
+ isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable'));
+ expect(new AngelHttpException.methodTimeout(),
+ isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout'));
+ expect(new AngelHttpException.MethodTimeout(),
+ isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout'));
+ expect(new AngelHttpException.conflict(),
+ isException(HttpStatus.CONFLICT, '409 Conflict'));
+ expect(new AngelHttpException.Conflict(),
+ isException(HttpStatus.CONFLICT, '409 Conflict'));
+ expect(new AngelHttpException.notProcessable(),
+ isException(422, '422 Not Processable'));
+ expect(new AngelHttpException.NotProcessable(),
+ isException(422, '422 Not Processable'));
+ expect(new AngelHttpException.notImplemented(),
+ isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented'));
+ expect(new AngelHttpException.NotImplemented(),
+ isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented'));
+ expect(new AngelHttpException.unavailable(),
+ isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable'));
+ expect(new AngelHttpException.Unavailable(),
+ isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable'));
+ });
+
+ test('fromMap', () {
+ expect(new AngelHttpException.fromMap({'status_code': -1, 'message': 'ok'}),
+ isException(-1, 'ok'));
+ });
+
+ test('toMap = toJson', () {
+ var exc = new AngelHttpException.badRequest();
+ expect(exc.toMap(), exc.toJson());
+ var json = JSON.encode(exc.toJson());
+ var exc2 = new AngelHttpException.fromJson(json);
+ expect(exc2.toJson(), exc.toJson());
+ });
+
+ test('toString', () {
+ expect(
+ new AngelHttpException(null, statusCode: 420, message: 'Blaze It')
+ .toString(),
+ '420: Blaze It');
+ });
+}
+
+Matcher isException(int statusCode, String message) =>
+ new _IsException(statusCode, message);
+
+class _IsException extends Matcher {
+ final int statusCode;
+ final String message;
+
+ _IsException(this.statusCode, this.message);
+
+ @override
+ Description describe(Description description) =>
+ description.add('has status code $statusCode and message "$message"');
+
+ @override
+ bool matches(AngelHttpException item, Map matchState) {
+ return item.statusCode == statusCode && item.message == message;
+ }
+}
diff --git a/test/typed_service_test.dart b/test/typed_service_test.dart
new file mode 100644
index 00000000..f084de04
--- /dev/null
+++ b/test/typed_service_test.dart
@@ -0,0 +1,65 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/common.dart';
+import 'package:test/test.dart';
+
+main() {
+ var svc = new TypedService(new MapService());
+
+ test('force model', () {
+ expect(() => new TypedService(null), throwsException);
+ });
+
+ test('serialize', () {
+ expect(svc.serialize({'foo': 'bar'}), {'foo': 'bar'});
+ expect(
+ svc.serialize([
+ {'foo': 'bar'}
+ ]),
+ [
+ {'foo': 'bar'}
+ ]);
+ expect(() => svc.serialize(2), throwsArgumentError);
+ var now = new DateTime.now();
+ var t =
+ new Todo(text: 'a', completed: false, createdAt: now, updatedAt: now);
+ var m = svc.serialize(t);
+ print(m);
+ expect(m, {
+ 'id': null,
+ 'createdAt': now.toIso8601String(),
+ 'updatedAt': now.toIso8601String(),
+ 'text': 'a',
+ 'completed': false
+ });
+ });
+
+ test('deserialize date', () {
+ var now = new DateTime.now();
+ var m = svc.deserialize({
+ 'createdAt': now.toIso8601String(),
+ 'updatedAt': now.toIso8601String()
+ });
+ expect(m, new isInstanceOf());
+ var t = m as Todo;
+ expect(t.createdAt.millisecondsSinceEpoch, now.millisecondsSinceEpoch);
+ });
+
+ test('deserialize date w/ underscore', () {
+ var now = new DateTime.now();
+ var m = svc.deserialize({
+ 'created_at': now.toIso8601String(),
+ 'updated_at': now.toIso8601String()
+ });
+ expect(m, new isInstanceOf());
+ var t = m as Todo;
+ expect(t.createdAt.millisecondsSinceEpoch, now.millisecondsSinceEpoch);
+ });
+}
+
+class Todo extends Model {
+ String text;
+ bool completed;
+ @override
+ DateTime createdAt, updatedAt;
+ Todo({this.text, this.completed, this.createdAt, this.updatedAt});
+}
diff --git a/test/view_generator_test.dart b/test/view_generator_test.dart
new file mode 100644
index 00000000..680536dc
--- /dev/null
+++ b/test/view_generator_test.dart
@@ -0,0 +1,10 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:test/test.dart';
+
+main() {
+ test('default view generator', () async {
+ var app = new Angel();
+ var view = await app.viewGenerator('foo', {'bar': 'baz'});
+ expect(view, contains('No view engine'));
+ });
+}
\ No newline at end of file