From ca68427a211e9522d6a151743bf8d4712a0588b4 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 21:34:21 -0400 Subject: [PATCH] 2.0.0-alpha.2 --- CHANGELOG.md | 4 ++ lib/angel_client.dart | 131 +++++++++++++++++++++++++++++-------- lib/base_angel_client.dart | 49 ++++++++------ test/common.dart | 7 +- 4 files changed, 141 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b78b1bb3..b5c510a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.0.0-alpha.2 +* Make Service `index` always return `List`. +* Add `Service.map`. + # 2.0.0-alpha.1 * Refactor `params` to `Map`. diff --git a/lib/angel_client.dart b/lib/angel_client.dart index fa1ff0bf..edcc52d1 100644 --- a/lib/angel_client.dart +++ b/lib/angel_client.dart @@ -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 { /// Fired on `indexed` events. - Stream get onIndexed; + Stream> get onIndexed; /// Fired on `read` events. Stream get onRead; @@ -118,60 +118,133 @@ abstract class Service { Future close(); /// Retrieves all resources. - Future index([Map params]); + Future> index([Map params]); /// Retrieves the desired resource. - Future read(Id id, [Map params]); + Future read(Id id, [Map params]); /// Creates a resource. - Future create(Data data, [Map params]); + Future create(Data data, [Map params]); /// Modifies a resource. - Future modify(Id id, Data data, [Map params]); + Future modify(Id id, Data data, [Map params]); /// Overwrites a resource. - Future update(Id id, Data data, [Map params]); + Future update(Id id, Data data, [Map params]); /// Removes the given resource. - Future remove(Id id, [Map params]); + Future remove(Id id, [Map 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 map(U Function(Data) encoder, Data Function(U) decoder) { + return new _MappedService(this, encoder, decoder); + } +} + +class _MappedService extends Service { + final Service 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 create(U data, [Map params]) { + return inner.create(decoder(data)).then(encoder); + } + + @override + Future> index([Map params]) { + return inner.index(params).then((l) => l.map(encoder).toList()); + } + + @override + Future modify(Id id, U data, [Map params]) { + return inner.modify(id, decoder(data), params).then(encoder); + } + + @override + Stream get onCreated => inner.onCreated.map(encoder); + + @override + Stream> get onIndexed => + inner.onIndexed.map((l) => l.map(encoder).toList()); + + @override + Stream get onModified => inner.onModified.map(encoder); + + @override + Stream get onRead => inner.onRead.map(encoder); + + @override + Stream get onRemoved => inner.onRemoved.map(encoder); + + @override + Stream get onUpdated => inner.onUpdated.map(encoder); + + @override + Future read(Id id, [Map params]) { + return inner.read(id, params).then(encoder); + } + + @override + Future remove(Id id, [Map params]) { + return inner.remove(id, params).then(encoder); + } + + @override + Future update(Id id, U data, [Map 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 extends DelegatingList { +class ServiceList extends DelegatingList { /// 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 get equality => _equality; + + Equality _equality; final Service service; final StreamController> _onChange = new StreamController(); + final List _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 equality}) + : super([]) { + _equality = equality; + _equality ??= new EqualityBy((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 extends DelegatingList { var indices = []; 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 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> get onChange => _onChange.stream; diff --git a/lib/base_angel_client.dart b/lib/base_angel_client.dart index 13f7f79b..4704085a 100644 --- a/lib/base_angel_client.dart +++ b/lib/base_angel_client.dart @@ -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 extends Service { final http.BaseClient client; final AngelDeserializer deserializer; - final StreamController _onIndexed = new StreamController(); + final StreamController> _onIndexed = new StreamController(); final StreamController _onRead = new StreamController(), _onCreated = new StreamController(), _onModified = new StreamController(), @@ -256,7 +259,7 @@ class BaseAngelService extends Service { _onRemoved = new StreamController(); @override - Stream get onIndexed => _onIndexed.stream; + Stream> get onIndexed => _onIndexed.stream; @override Stream get onRead => _onRead.stream; @@ -302,7 +305,7 @@ class BaseAngelService extends Service { } @override - Future index([Map params]) async { + Future> index([Map params]) async { final response = await app.sendUnstreamed( 'GET', '$basePath${_buildQuery(params)}', _readHeaders); @@ -314,13 +317,7 @@ class BaseAngelService extends Service { 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 extends Service { else throw failure(response, error: e, stack: st); } + + return null; } @override - Future read(id, [Map params]) async { + Future read(id, [Map params]) async { final response = await app.sendUnstreamed( 'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders); @@ -354,10 +353,12 @@ class BaseAngelService extends Service { else throw failure(response, error: e, stack: st); } + + return null; } @override - Future create(data, [Map params]) async { + Future create(data, [Map params]) async { final response = await app.sendUnstreamed('POST', '$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data)); @@ -378,10 +379,12 @@ class BaseAngelService extends Service { else throw failure(response, error: e, stack: st); } + + return null; } @override - Future modify(id, data, [Map params]) async { + Future modify(id, data, [Map params]) async { final response = await app.sendUnstreamed('PATCH', '$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data)); @@ -402,10 +405,12 @@ class BaseAngelService extends Service { else throw failure(response, error: e, stack: st); } + + return null; } @override - Future update(id, data, [Map params]) async { + Future update(id, data, [Map params]) async { final response = await app.sendUnstreamed('POST', '$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data)); @@ -426,10 +431,12 @@ class BaseAngelService extends Service { else throw failure(response, error: e, stack: st); } + + return null; } @override - Future remove(id, [Map params]) async { + Future remove(id, [Map params]) async { final response = await app.sendUnstreamed( 'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders); @@ -450,5 +457,7 @@ class BaseAngelService extends Service { else throw failure(response, error: e, stack: st); } + + return null; } } diff --git a/test/common.dart b/test/common.dart index 3a7921d5..bc54f5cf 100644 --- a/test/common.dart +++ b/test/common.dart @@ -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': '', 'data': {'username': 'password'} }; + } else if (request.url.path == '/api/todos' && request.method == 'GET') { + data = [data]; + } return new Future.value(new http.StreamedResponse( new Stream>.fromIterable([utf8.encode(json.encode(data))]),