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
|
# 2.0.0-alpha.1
|
||||||
* Refactor `params` to `Map<String, dynamic>`.
|
* 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';
|
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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))]),
|
||||||
|
|
Loading…
Reference in a new issue