2.0.0-alpha.2
This commit is contained in:
parent
c2c4ae6d6b
commit
ca68427a21
4 changed files with 141 additions and 50 deletions
|
@ -1,3 +1,7 @@
|
|||
# 2.0.0-alpha.2
|
||||
* Make Service `index` always return `List<Data>`.
|
||||
* Add `Service.map`.
|
||||
|
||||
# 2.0.0-alpha.1
|
||||
* Refactor `params` to `Map<String, dynamic>`.
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import 'package:http/src/response.dart' as http;
|
|||
export 'package:angel_http_exception/angel_http_exception.dart';
|
||||
|
||||
/// 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.
|
||||
///
|
||||
|
@ -95,7 +95,7 @@ class AngelAuthResult {
|
|||
/// Queries a service on an Angel server, with the same API.
|
||||
abstract class Service<Id, Data> {
|
||||
/// Fired on `indexed` events.
|
||||
Stream get onIndexed;
|
||||
Stream<List<Data>> get onIndexed;
|
||||
|
||||
/// Fired on `read` events.
|
||||
Stream<Data> get onRead;
|
||||
|
@ -118,60 +118,133 @@ abstract class Service<Id, Data> {
|
|||
Future close();
|
||||
|
||||
/// Retrieves all resources.
|
||||
Future index([Map<String, dynamic> params]);
|
||||
Future<List<Data>> index([Map<String, dynamic> params]);
|
||||
|
||||
/// Retrieves the desired resource.
|
||||
Future read(Id id, [Map<String, dynamic> params]);
|
||||
Future<Data> read(Id id, [Map<String, dynamic> params]);
|
||||
|
||||
/// Creates a resource.
|
||||
Future create(Data data, [Map<String, dynamic> params]);
|
||||
Future<Data> create(Data data, [Map<String, dynamic> params]);
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
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.
|
||||
class ServiceList<Id, Data> extends DelegatingList {
|
||||
class ServiceList<Id, Data> extends DelegatingList<Data> {
|
||||
/// A field name used to compare [Map] by ID.
|
||||
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.
|
||||
///
|
||||
/// 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 StreamController<ServiceList<Id, Data>> _onChange =
|
||||
new StreamController();
|
||||
|
||||
final List<StreamSubscription> _subs = [];
|
||||
|
||||
ServiceList(this.service,
|
||||
{this.idField, this.asPaginated: false, Equality compare})
|
||||
: _compare = compare ?? new EqualityBy((map) => map[idField ?? 'id']),
|
||||
super([]) {
|
||||
ServiceList(this.service, {this.idField, Equality<Data> equality})
|
||||
: super([]) {
|
||||
_equality = equality;
|
||||
_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
|
||||
_subs.add(service.onIndexed.listen((data) {
|
||||
var items = asPaginated == true ? data['data'] : data;
|
||||
_subs.add(service.onIndexed.where(_notNull).listen((data) {
|
||||
this
|
||||
..clear()
|
||||
..addAll(items as Iterable);
|
||||
..addAll(data);
|
||||
_onChange.add(this);
|
||||
}));
|
||||
|
||||
// Created
|
||||
_subs.add(service.onCreated.listen((item) {
|
||||
_subs.add(service.onCreated.where(_notNull).listen((item) {
|
||||
add(item);
|
||||
_onChange.add(this);
|
||||
}));
|
||||
|
@ -181,7 +254,7 @@ class ServiceList<Id, Data> extends DelegatingList {
|
|||
var indices = <int>[];
|
||||
|
||||
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) {
|
||||
|
@ -192,17 +265,19 @@ class ServiceList<Id, Data> extends DelegatingList {
|
|||
}
|
||||
|
||||
_subs.addAll([
|
||||
service.onModified.listen(handleModified),
|
||||
service.onUpdated.listen(handleModified),
|
||||
service.onModified.where(_notNull).listen(handleModified),
|
||||
service.onUpdated.where(_notNull).listen(handleModified),
|
||||
]);
|
||||
|
||||
// Removed
|
||||
_subs.add(service.onRemoved.listen((item) {
|
||||
removeWhere((x) => _compare.equals(item, x));
|
||||
_subs.add(service.onRemoved.where(_notNull).listen((item) {
|
||||
removeWhere((x) => _equality.equals(item, x));
|
||||
_onChange.add(this);
|
||||
}));
|
||||
}
|
||||
|
||||
static bool _notNull(x) => x != null;
|
||||
|
||||
/// Fires whenever the underlying [service] fires a change event.
|
||||
Stream<ServiceList<Id, Data>> get onChange => _onChange.stream;
|
||||
|
||||
|
|
|
@ -33,21 +33,24 @@ bool _invalid(http.Response response) =>
|
|||
response.statusCode < 200 ||
|
||||
response.statusCode >= 300;
|
||||
|
||||
AngelHttpException failure(http.Response response, {error, StackTrace stack}) {
|
||||
AngelHttpException failure(http.Response response,
|
||||
{error, String message, StackTrace stack}) {
|
||||
try {
|
||||
final v = json.decode(response.body);
|
||||
|
||||
if (v is Map && v['isError'] == true) {
|
||||
return new AngelHttpException.fromMap(v);
|
||||
if (v is Map && (v['is_error'] == true) || v['isError'] == true) {
|
||||
return new AngelHttpException.fromMap(v as Map);
|
||||
} else {
|
||||
return new AngelHttpException(error,
|
||||
message: 'Unhandled exception while connecting to Angel backend.',
|
||||
message: message ??
|
||||
'Unhandled exception while connecting to Angel backend.',
|
||||
statusCode: response.statusCode,
|
||||
stackTrace: stack);
|
||||
}
|
||||
} catch (e, st) {
|
||||
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,
|
||||
stackTrace: stack ?? st);
|
||||
}
|
||||
|
@ -248,7 +251,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
final http.BaseClient client;
|
||||
final AngelDeserializer<Data> deserializer;
|
||||
|
||||
final StreamController _onIndexed = new StreamController();
|
||||
final StreamController<List<Data>> _onIndexed = new StreamController();
|
||||
final StreamController<Data> _onRead = new StreamController(),
|
||||
_onCreated = new StreamController(),
|
||||
_onModified = new StreamController(),
|
||||
|
@ -256,7 +259,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
_onRemoved = new StreamController();
|
||||
|
||||
@override
|
||||
Stream get onIndexed => _onIndexed.stream;
|
||||
Stream<List<Data>> get onIndexed => _onIndexed.stream;
|
||||
|
||||
@override
|
||||
Stream<Data> get onRead => _onRead.stream;
|
||||
|
@ -302,7 +305,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future index([Map<String, dynamic> params]) async {
|
||||
Future<List<Data>> index([Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed(
|
||||
'GET', '$basePath${_buildQuery(params)}', _readHeaders);
|
||||
|
||||
|
@ -314,13 +317,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
throw failure(response);
|
||||
}
|
||||
|
||||
final v = json.decode(response.body);
|
||||
|
||||
if (v is! List) {
|
||||
_onIndexed.add(v as Data);
|
||||
return v;
|
||||
}
|
||||
|
||||
final v = json.decode(response.body) as List;
|
||||
var r = v.map(deserialize).toList();
|
||||
_onIndexed.add(r);
|
||||
return r;
|
||||
|
@ -330,10 +327,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
else
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future read(id, [Map<String, dynamic> params]) async {
|
||||
Future<Data> read(id, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed(
|
||||
'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders);
|
||||
|
||||
|
@ -354,10 +353,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
else
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future create(data, [Map<String, dynamic> params]) async {
|
||||
Future<Data> create(data, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed('POST',
|
||||
'$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
||||
|
||||
|
@ -378,10 +379,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
else
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@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',
|
||||
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
||||
|
||||
|
@ -402,10 +405,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
else
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@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',
|
||||
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
||||
|
||||
|
@ -426,10 +431,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
else
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future remove(id, [Map<String, dynamic> params]) async {
|
||||
Future<Data> remove(id, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed(
|
||||
'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders);
|
||||
|
||||
|
@ -450,5 +457,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
else
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,16 @@ class SpecClient extends http.BaseClient {
|
|||
send(http.BaseRequest request) {
|
||||
_spec = new Spec(request, request.method, request.url.path, request.headers,
|
||||
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 = {
|
||||
'token': '<jwt>',
|
||||
'data': {'username': 'password'}
|
||||
};
|
||||
} else if (request.url.path == '/api/todos' && request.method == 'GET') {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
return new Future<http.StreamedResponse>.value(new http.StreamedResponse(
|
||||
new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),
|
||||
|
|
Loading…
Reference in a new issue