2.0.0-alpha.2

This commit is contained in:
Tobe O 2018-11-03 21:34:21 -04:00
parent c2c4ae6d6b
commit ca68427a21
4 changed files with 141 additions and 50 deletions

View file

@ -1,3 +1,7 @@
# 2.0.0-alpha.2
* Make Service `index` always return `List<Data>`.
* Add `Service.map`.
# 2.0.0-alpha.1 # 2.0.0-alpha.1
* Refactor `params` to `Map<String, dynamic>`. * Refactor `params` to `Map<String, dynamic>`.

View file

@ -8,7 +8,7 @@ import 'package:http/src/response.dart' as http;
export 'package:angel_http_exception/angel_http_exception.dart'; export 'package:angel_http_exception/angel_http_exception.dart';
/// A function that configures an [Angel] client in some way. /// A function that configures an [Angel] client in some way.
typedef FutureOr AngelConfigurer(Angel app); typedef Future AngelConfigurer(Angel app);
/// A function that deserializes data received from the server. /// A function that deserializes data received from the server.
/// ///
@ -95,7 +95,7 @@ class AngelAuthResult {
/// Queries a service on an Angel server, with the same API. /// Queries a service on an Angel server, with the same API.
abstract class Service<Id, Data> { abstract class Service<Id, Data> {
/// Fired on `indexed` events. /// Fired on `indexed` events.
Stream get onIndexed; Stream<List<Data>> get onIndexed;
/// Fired on `read` events. /// Fired on `read` events.
Stream<Data> get onRead; Stream<Data> get onRead;
@ -118,60 +118,133 @@ abstract class Service<Id, Data> {
Future close(); Future close();
/// Retrieves all resources. /// Retrieves all resources.
Future index([Map<String, dynamic> params]); Future<List<Data>> index([Map<String, dynamic> params]);
/// Retrieves the desired resource. /// Retrieves the desired resource.
Future read(Id id, [Map<String, dynamic> params]); Future<Data> read(Id id, [Map<String, dynamic> params]);
/// Creates a resource. /// Creates a resource.
Future create(Data data, [Map<String, dynamic> params]); Future<Data> create(Data data, [Map<String, dynamic> params]);
/// Modifies a resource. /// Modifies a resource.
Future modify(Id id, Data data, [Map<String, dynamic> params]); Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]);
/// Overwrites a resource. /// Overwrites a resource.
Future update(Id id, Data data, [Map<String, dynamic> params]); Future<Data> update(Id id, Data data, [Map<String, dynamic> params]);
/// Removes the given resource. /// Removes the given resource.
Future remove(Id id, [Map<String, dynamic> params]); Future<Data> remove(Id id, [Map<String, dynamic> params]);
/// Creates a [Service] that wraps over this one, and maps input and output using two converter functions.
///
/// Handy utility for handling data in a type-safe manner.
Service<Id, U> map<U>(U Function(Data) encoder, Data Function(U) decoder) {
return new _MappedService(this, encoder, decoder);
}
}
class _MappedService<Id, Data, U> extends Service<Id, U> {
final Service<Id, Data> inner;
final U Function(Data) encoder;
final Data Function(U) decoder;
_MappedService(this.inner, this.encoder, this.decoder);
@override
Angel get app => inner.app;
@override
Future close() => new Future.value();
@override
Future<U> create(U data, [Map<String, dynamic> params]) {
return inner.create(decoder(data)).then(encoder);
}
@override
Future<List<U>> index([Map<String, dynamic> params]) {
return inner.index(params).then((l) => l.map(encoder).toList());
}
@override
Future<U> modify(Id id, U data, [Map<String, dynamic> params]) {
return inner.modify(id, decoder(data), params).then(encoder);
}
@override
Stream<U> get onCreated => inner.onCreated.map(encoder);
@override
Stream<List<U>> get onIndexed =>
inner.onIndexed.map((l) => l.map(encoder).toList());
@override
Stream<U> get onModified => inner.onModified.map(encoder);
@override
Stream<U> get onRead => inner.onRead.map(encoder);
@override
Stream<U> get onRemoved => inner.onRemoved.map(encoder);
@override
Stream<U> get onUpdated => inner.onUpdated.map(encoder);
@override
Future<U> read(Id id, [Map<String, dynamic> params]) {
return inner.read(id, params).then(encoder);
}
@override
Future<U> remove(Id id, [Map<String, dynamic> params]) {
return inner.remove(id, params).then(encoder);
}
@override
Future<U> update(Id id, U data, [Map<String, dynamic> params]) {
return inner.update(id, decoder(data), params).then(encoder);
}
} }
/// A [List] that automatically updates itself whenever the referenced [service] fires an event. /// A [List] that automatically updates itself whenever the referenced [service] fires an event.
class ServiceList<Id, Data> extends DelegatingList { class ServiceList<Id, Data> extends DelegatingList<Data> {
/// A field name used to compare [Map] by ID. /// A field name used to compare [Map] by ID.
final String idField; final String idField;
/// If `true` (default: `false`), then `index` events will be handled as a [Map] containing a `data` field.
///
/// See https://github.com/angel-dart/paginate.
final bool asPaginated;
/// A function used to compare the ID's two items for equality. /// A function used to compare the ID's two items for equality.
/// ///
/// Defaults to comparing the [idField] of `Map` instances. /// Defaults to comparing the [idField] of `Map` instances.
final Equality _compare; Equality<Data> get equality => _equality;
Equality<Data> _equality;
final Service<Id, Data> service; final Service<Id, Data> service;
final StreamController<ServiceList<Id, Data>> _onChange = final StreamController<ServiceList<Id, Data>> _onChange =
new StreamController(); new StreamController();
final List<StreamSubscription> _subs = []; final List<StreamSubscription> _subs = [];
ServiceList(this.service, ServiceList(this.service, {this.idField, Equality<Data> equality})
{this.idField, this.asPaginated: false, Equality compare}) : super([]) {
: _compare = compare ?? new EqualityBy((map) => map[idField ?? 'id']), _equality = equality;
super([]) { _equality ??= new EqualityBy<Data, Id>((map) {
if (map is Map)
return map[idField ?? 'id'] as Id;
else
throw new UnsupportedError(
'ServiceList only knows how to find the id from a Map object. Provide a custom `Equality` in your call to the constructor.');
});
// Index // Index
_subs.add(service.onIndexed.listen((data) { _subs.add(service.onIndexed.where(_notNull).listen((data) {
var items = asPaginated == true ? data['data'] : data;
this this
..clear() ..clear()
..addAll(items as Iterable); ..addAll(data);
_onChange.add(this); _onChange.add(this);
})); }));
// Created // Created
_subs.add(service.onCreated.listen((item) { _subs.add(service.onCreated.where(_notNull).listen((item) {
add(item); add(item);
_onChange.add(this); _onChange.add(this);
})); }));
@ -181,7 +254,7 @@ class ServiceList<Id, Data> extends DelegatingList {
var indices = <int>[]; var indices = <int>[];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
if (_compare.equals(item, this[i])) indices.add(i); if (_equality.equals(item, this[i])) indices.add(i);
} }
if (indices.isNotEmpty) { if (indices.isNotEmpty) {
@ -192,17 +265,19 @@ class ServiceList<Id, Data> extends DelegatingList {
} }
_subs.addAll([ _subs.addAll([
service.onModified.listen(handleModified), service.onModified.where(_notNull).listen(handleModified),
service.onUpdated.listen(handleModified), service.onUpdated.where(_notNull).listen(handleModified),
]); ]);
// Removed // Removed
_subs.add(service.onRemoved.listen((item) { _subs.add(service.onRemoved.where(_notNull).listen((item) {
removeWhere((x) => _compare.equals(item, x)); removeWhere((x) => _equality.equals(item, x));
_onChange.add(this); _onChange.add(this);
})); }));
} }
static bool _notNull(x) => x != null;
/// Fires whenever the underlying [service] fires a change event. /// Fires whenever the underlying [service] fires a change event.
Stream<ServiceList<Id, Data>> get onChange => _onChange.stream; Stream<ServiceList<Id, Data>> get onChange => _onChange.stream;

View file

@ -33,21 +33,24 @@ bool _invalid(http.Response response) =>
response.statusCode < 200 || response.statusCode < 200 ||
response.statusCode >= 300; response.statusCode >= 300;
AngelHttpException failure(http.Response response, {error, StackTrace stack}) { AngelHttpException failure(http.Response response,
{error, String message, StackTrace stack}) {
try { try {
final v = json.decode(response.body); final v = json.decode(response.body);
if (v is Map && v['isError'] == true) { if (v is Map && (v['is_error'] == true) || v['isError'] == true) {
return new AngelHttpException.fromMap(v); return new AngelHttpException.fromMap(v as Map);
} else { } else {
return new AngelHttpException(error, return new AngelHttpException(error,
message: 'Unhandled exception while connecting to Angel backend.', message: message ??
'Unhandled exception while connecting to Angel backend.',
statusCode: response.statusCode, statusCode: response.statusCode,
stackTrace: stack); stackTrace: stack);
} }
} catch (e, st) { } catch (e, st) {
return new AngelHttpException(error ?? e, return new AngelHttpException(error ?? e,
message: 'Unhandled exception while connecting to Angel backend.', message: message ??
'Angel backend did not return JSON - an error likely occurred.',
statusCode: response.statusCode, statusCode: response.statusCode,
stackTrace: stack ?? st); stackTrace: stack ?? st);
} }
@ -248,7 +251,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
final http.BaseClient client; final http.BaseClient client;
final AngelDeserializer<Data> deserializer; final AngelDeserializer<Data> deserializer;
final StreamController _onIndexed = new StreamController(); final StreamController<List<Data>> _onIndexed = new StreamController();
final StreamController<Data> _onRead = new StreamController(), final StreamController<Data> _onRead = new StreamController(),
_onCreated = new StreamController(), _onCreated = new StreamController(),
_onModified = new StreamController(), _onModified = new StreamController(),
@ -256,7 +259,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
_onRemoved = new StreamController(); _onRemoved = new StreamController();
@override @override
Stream get onIndexed => _onIndexed.stream; Stream<List<Data>> get onIndexed => _onIndexed.stream;
@override @override
Stream<Data> get onRead => _onRead.stream; Stream<Data> get onRead => _onRead.stream;
@ -302,7 +305,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
} }
@override @override
Future index([Map<String, dynamic> params]) async { Future<List<Data>> index([Map<String, dynamic> params]) async {
final response = await app.sendUnstreamed( final response = await app.sendUnstreamed(
'GET', '$basePath${_buildQuery(params)}', _readHeaders); 'GET', '$basePath${_buildQuery(params)}', _readHeaders);
@ -314,13 +317,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
throw failure(response); throw failure(response);
} }
final v = json.decode(response.body); final v = json.decode(response.body) as List;
if (v is! List) {
_onIndexed.add(v as Data);
return v;
}
var r = v.map(deserialize).toList(); var r = v.map(deserialize).toList();
_onIndexed.add(r); _onIndexed.add(r);
return r; return r;
@ -330,10 +327,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
else else
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
} }
return null;
} }
@override @override
Future read(id, [Map<String, dynamic> params]) async { Future<Data> read(id, [Map<String, dynamic> params]) async {
final response = await app.sendUnstreamed( final response = await app.sendUnstreamed(
'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders); 'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders);
@ -354,10 +353,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
else else
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
} }
return null;
} }
@override @override
Future create(data, [Map<String, dynamic> params]) async { Future<Data> create(data, [Map<String, dynamic> params]) async {
final response = await app.sendUnstreamed('POST', final response = await app.sendUnstreamed('POST',
'$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data)); '$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data));
@ -378,10 +379,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
else else
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
} }
return null;
} }
@override @override
Future modify(id, data, [Map<String, dynamic> params]) async { Future<Data> modify(id, data, [Map<String, dynamic> params]) async {
final response = await app.sendUnstreamed('PATCH', final response = await app.sendUnstreamed('PATCH',
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data)); '$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
@ -402,10 +405,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
else else
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
} }
return null;
} }
@override @override
Future update(id, data, [Map<String, dynamic> params]) async { Future<Data> update(id, data, [Map<String, dynamic> params]) async {
final response = await app.sendUnstreamed('POST', final response = await app.sendUnstreamed('POST',
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data)); '$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
@ -426,10 +431,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
else else
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
} }
return null;
} }
@override @override
Future remove(id, [Map<String, dynamic> params]) async { Future<Data> remove(id, [Map<String, dynamic> params]) async {
final response = await app.sendUnstreamed( final response = await app.sendUnstreamed(
'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders); 'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders);
@ -450,5 +457,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
else else
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
} }
return null;
} }
} }

View file

@ -29,13 +29,16 @@ class SpecClient extends http.BaseClient {
send(http.BaseRequest request) { send(http.BaseRequest request) {
_spec = new Spec(request, request.method, request.url.path, request.headers, _spec = new Spec(request, request.method, request.url.path, request.headers,
request.contentLength); request.contentLength);
var data = {'text': 'Clean your room!', 'completed': true}; dynamic data = {'text': 'Clean your room!', 'completed': true};
if (request.url.path.contains('auth')) if (request.url.path.contains('auth')) {
data = { data = {
'token': '<jwt>', 'token': '<jwt>',
'data': {'username': 'password'} 'data': {'username': 'password'}
}; };
} else if (request.url.path == '/api/todos' && request.method == 'GET') {
data = [data];
}
return new Future<http.StreamedResponse>.value(new http.StreamedResponse( return new Future<http.StreamedResponse>.value(new http.StreamedResponse(
new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]), new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),