Migrated angel_client to 4.0.0

This commit is contained in:
thomashii 2021-04-10 21:22:20 +08:00
parent 5e9172aca9
commit 037e82d699
13 changed files with 220 additions and 200 deletions

View file

@ -15,7 +15,7 @@
* Updated angel_configuration to 4.0.0 (6/8 test passed) * Updated angel_configuration to 4.0.0 (6/8 test passed)
* Updated angel_validate to 4.0.0 (6/7 test passed) * Updated angel_validate to 4.0.0 (6/7 test passed)
* Updated json_god to 4.0.0 (13/13 test passed) * Updated json_god to 4.0.0 (13/13 test passed)
* Updated angel_client to 3.0.0 (in progress) * Updated angel_client to 4.0.0 (6/13 test passed)
* Updated angel_websocket to 3.0.0 (in progress) * Updated angel_websocket to 3.0.0 (in progress)
* Updated test to 3.0.0 (in progress) * Updated test to 3.0.0 (in progress)
* Updated jael to 3.0.0 (in progress) * Updated jael to 3.0.0 (in progress)

View file

@ -6,16 +6,16 @@ Future doSomething(Angel app) async {
.service<String, Map<String, dynamic>>('api/users') .service<String, Map<String, dynamic>>('api/users')
.map(User.fromMap, User.toMap); .map(User.fromMap, User.toMap);
var users = await userService.index(); var users = await (userService.index() as FutureOr<List<User>>);
print('Name: ${users.first.name}'); print('Name: ${users.first.name}');
} }
class User { class User {
final String name; final String? name;
User({this.name}); User({this.name});
static User fromMap(Map data) => User(name: data['name'] as String); static User fromMap(Map data) => User(name: data['name'] as String?);
static Map<String, String> toMap(User user) => {'name': user.name}; static Map<String, String?> toMap(User user) => {'name': user.name};
} }

View file

@ -9,13 +9,13 @@ export 'package:angel_http_exception/angel_http_exception.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
/// A function that configures an [Angel] client in some way. /// A function that configures an [Angel] client in some way.
typedef FutureOr<void> AngelConfigurer(Angel app); typedef AngelConfigurer = FutureOr<void> Function(Angel app);
/// A function that deserializes data received from the server. /// A function that deserializes data received from the server.
/// ///
/// This is only really necessary in the browser, where `json_god` /// This is only really necessary in the browser, where `json_god`
/// doesn't work. /// doesn't work.
typedef T AngelDeserializer<T>(x); typedef AngelDeserializer<T> = T? Function(dynamic x);
/// Represents an Angel server that we are querying. /// Represents an Angel server that we are querying.
abstract class Angel extends http.BaseClient { abstract class Angel extends http.BaseClient {
@ -23,7 +23,7 @@ abstract class Angel extends http.BaseClient {
/// that is automatically attached to every request sent. /// that is automatically attached to every request sent.
/// ///
/// This is designed with `package:angel_auth` in mind. /// This is designed with `package:angel_auth` in mind.
String authToken; String? authToken;
/// The root URL at which the target server. /// The root URL at which the target server.
final Uri baseUrl; final Uri baseUrl;
@ -46,7 +46,7 @@ abstract class Angel extends http.BaseClient {
/// ///
/// The given [credentials] are sent to server as-is; the request body is sent as JSON. /// The given [credentials] are sent to server as-is; the request body is sent as JSON.
Future<AngelAuthResult> authenticate( Future<AngelAuthResult> authenticate(
{@required String type, {required String type,
credentials, credentials,
String authEndpoint = '/auth', String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}); @deprecated String reviveEndpoint = '/auth/token'});
@ -85,57 +85,59 @@ abstract class Angel extends http.BaseClient {
/// You can pass a custom [deserializer], which is typically necessary in cases where /// You can pass a custom [deserializer], which is typically necessary in cases where
/// `dart:mirrors` does not exist. /// `dart:mirrors` does not exist.
Service<Id, Data> service<Id, Data>(String path, Service<Id, Data> service<Id, Data>(String path,
{@deprecated Type type, AngelDeserializer<Data> deserializer}); {@deprecated Type? type, AngelDeserializer<Data>? deserializer});
//@override //@override
//Future<http.Response> delete(url, {Map<String, String> headers}); //Future<http.Response> delete(url, {Map<String, String> headers});
@override @override
Future<http.Response> get(url, {Map<String, String> headers}); Future<http.Response> get(url, {Map<String, String>? headers});
@override @override
Future<http.Response> head(url, {Map<String, String> headers}); Future<http.Response> head(url, {Map<String, String>? headers});
@override @override
Future<http.Response> patch(url, Future<http.Response> patch(url,
{body, Map<String, String> headers, Encoding encoding}); {body, Map<String, String>? headers, Encoding? encoding});
@override @override
Future<http.Response> post(url, Future<http.Response> post(url,
{body, Map<String, String> headers, Encoding encoding}); {body, Map<String, String>? headers, Encoding? encoding});
@override @override
Future<http.Response> put(url, Future<http.Response> put(url,
{body, Map<String, String> headers, Encoding encoding}); {body, Map<String, String>? headers, Encoding? encoding});
} }
/// Represents the result of authentication with an Angel server. /// Represents the result of authentication with an Angel server.
class AngelAuthResult { class AngelAuthResult {
String _token; String? _token;
final Map<String, dynamic> data = {}; final Map<String, dynamic> data = {};
/// The JSON Web token that was sent with this response. /// The JSON Web token that was sent with this response.
String get token => _token; String? get token => _token;
AngelAuthResult({String token, Map<String, dynamic> data = const {}}) { AngelAuthResult({String? token, Map<String, dynamic> data = const {}}) {
_token = token; _token = token;
this.data.addAll(data ?? {}); this.data.addAll(data);
} }
/// Attempts to deserialize a response from a [Map]. /// Attempts to deserialize a response from a [Map].
factory AngelAuthResult.fromMap(Map data) { factory AngelAuthResult.fromMap(Map? data) {
final result = AngelAuthResult(); final result = AngelAuthResult();
if (data is Map && data.containsKey('token') && data['token'] is String) if (data is Map && data.containsKey('token') && data['token'] is String) {
result._token = data['token'].toString(); result._token = data['token'].toString();
}
if (data is Map) if (data is Map) {
result.data.addAll((data['data'] as Map<String, dynamic>) ?? {}); result.data.addAll((data['data'] as Map<String, dynamic>?) ?? {});
}
if (result.token == null) { if (result.token == null) {
throw FormatException( throw FormatException(
'The required "token" field was not present in the given data.'); 'The required "token" field was not present in the given data.');
} else if (data['data'] is! Map) { } else if (data!['data'] is! Map) {
throw FormatException( throw FormatException(
'The required "data" field in the given data was not a map; instead, it was ${data['data']}.'); 'The required "data" field in the given data was not a map; instead, it was ${data['data']}.');
} }
@ -145,7 +147,7 @@ class AngelAuthResult {
/// Attempts to deserialize a response from a [String]. /// Attempts to deserialize a response from a [String].
factory AngelAuthResult.fromJson(String s) => factory AngelAuthResult.fromJson(String s) =>
AngelAuthResult.fromMap(json.decode(s) as Map); AngelAuthResult.fromMap(json.decode(s) as Map?);
/// Converts this instance into a JSON-friendly representation. /// Converts this instance into a JSON-friendly representation.
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -179,22 +181,22 @@ abstract class Service<Id, Data> {
Future close(); Future close();
/// Retrieves all resources. /// Retrieves all resources.
Future<List<Data>> index([Map<String, dynamic> params]); Future<List<Data>?> index([Map<String, dynamic>? params]);
/// Retrieves the desired resource. /// Retrieves the desired resource.
Future<Data> read(Id id, [Map<String, dynamic> params]); Future<Data> read(Id id, [Map<String, dynamic>? params]);
/// Creates a resource. /// Creates a resource.
Future<Data> create(Data data, [Map<String, dynamic> params]); Future<Data> create(Data data, [Map<String, dynamic>? params]);
/// Modifies a resource. /// Modifies a resource.
Future<Data> 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<Data> 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<Data> 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. /// Creates a [Service] that wraps over this one, and maps input and output using two converter functions.
/// ///
@ -218,17 +220,17 @@ class _MappedService<Id, Data, U> extends Service<Id, U> {
Future close() => Future.value(); Future close() => Future.value();
@override @override
Future<U> create(U data, [Map<String, dynamic> params]) { Future<U> create(U data, [Map<String, dynamic>? params]) {
return inner.create(decoder(data)).then(encoder); return inner.create(decoder(data)).then(encoder);
} }
@override @override
Future<List<U>> index([Map<String, dynamic> params]) { Future<List<U>> index([Map<String, dynamic>? params]) {
return inner.index(params).then((l) => l.map(encoder).toList()); return inner.index(params).then((l) => l!.map(encoder).toList());
} }
@override @override
Future<U> modify(Id id, U data, [Map<String, dynamic> params]) { Future<U> modify(Id id, U data, [Map<String, dynamic>? params]) {
return inner.modify(id, decoder(data), params).then(encoder); return inner.modify(id, decoder(data), params).then(encoder);
} }
@ -252,17 +254,17 @@ class _MappedService<Id, Data, U> extends Service<Id, U> {
Stream<U> get onUpdated => inner.onUpdated.map(encoder); Stream<U> get onUpdated => inner.onUpdated.map(encoder);
@override @override
Future<U> read(Id id, [Map<String, dynamic> params]) { Future<U> read(Id id, [Map<String, dynamic>? params]) {
return inner.read(id, params).then(encoder); return inner.read(id, params).then(encoder);
} }
@override @override
Future<U> remove(Id id, [Map<String, dynamic> params]) { Future<U> remove(Id id, [Map<String, dynamic>? params]) {
return inner.remove(id, params).then(encoder); return inner.remove(id, params).then(encoder);
} }
@override @override
Future<U> update(Id id, U data, [Map<String, dynamic> params]) { Future<U> update(Id id, U data, [Map<String, dynamic>? params]) {
return inner.update(id, decoder(data), params).then(encoder); return inner.update(id, decoder(data), params).then(encoder);
} }
} }
@ -275,9 +277,9 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
/// 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.
Equality<Data> get equality => _equality; Equality<Data>? get equality => _equality;
Equality<Data> _equality; Equality<Data>? _equality;
final Service<Id, Data> service; final Service<Id, Data> service;
@ -285,15 +287,16 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
final List<StreamSubscription> _subs = []; final List<StreamSubscription> _subs = [];
ServiceList(this.service, {this.idField = 'id', Equality<Data> equality}) ServiceList(this.service, {this.idField = 'id', Equality<Data>? equality})
: super([]) { : super([]) {
_equality = equality; _equality = equality;
_equality ??= EqualityBy<Data, Id>((map) { _equality ??= EqualityBy<Data, Id?>((map) {
if (map is Map) if (map is Map) {
return map[idField ?? 'id'] as Id; return map[idField] as Id?;
else } else {
throw UnsupportedError( throw UnsupportedError(
'ServiceList only knows how to find the id from a Map object. Provide a custom `Equality` in your call to the constructor.'); '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.where(_notNull).listen((data) { _subs.add(service.onIndexed.where(_notNull).listen((data) {
@ -310,15 +313,17 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
})); }));
// Modified/Updated // Modified/Updated
handleModified(Data item) { void handleModified(Data item) {
var indices = <int>[]; var indices = <int>[];
for (int i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if (_equality.equals(item, this[i])) indices.add(i); if (_equality!.equals(item, this[i])) indices.add(i);
} }
if (indices.isNotEmpty) { if (indices.isNotEmpty) {
for (var i in indices) this[i] = item; for (var i in indices) {
this[i] = item;
}
_onChange.add(this); _onChange.add(this);
} }
@ -331,7 +336,7 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
// Removed // Removed
_subs.add(service.onRemoved.where(_notNull).listen((item) { _subs.add(service.onRemoved.where(_notNull).listen((item) {
removeWhere((x) => _equality.equals(item, x)); removeWhere((x) => _equality!.equals(item, x));
_onChange.add(this); _onChange.add(this);
})); }));
} }

View file

@ -16,7 +16,7 @@ const Map<String, String> _writeHeaders = {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}; };
Map<String, String> _buildQuery(Map<String, dynamic> params) { Map<String, String>? _buildQuery(Map<String, dynamic>? params) {
return params?.map((k, v) => MapEntry(k, v.toString())); return params?.map((k, v) => MapEntry(k, v.toString()));
} }
@ -26,7 +26,7 @@ bool _invalid(http.Response response) =>
response.statusCode >= 300; response.statusCode >= 300;
AngelHttpException failure(http.Response response, AngelHttpException failure(http.Response response,
{error, String message, StackTrace stack}) { {error, String? message, StackTrace? stack}) {
try { try {
var v = json.decode(response.body); var v = json.decode(response.body);
@ -52,7 +52,7 @@ abstract class BaseAngelClient extends Angel {
final StreamController<AngelAuthResult> _onAuthenticated = final StreamController<AngelAuthResult> _onAuthenticated =
StreamController<AngelAuthResult>(); StreamController<AngelAuthResult>();
final List<Service> _services = []; final List<Service> _services = [];
final http.BaseClient client; final http.BaseClient? client;
@override @override
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream; Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
@ -61,7 +61,7 @@ abstract class BaseAngelClient extends Angel {
@override @override
Future<AngelAuthResult> authenticate( Future<AngelAuthResult> authenticate(
{String type, {String? type,
credentials, credentials,
String authEndpoint = '/auth', String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}) async { @deprecated String reviveEndpoint = '/auth/token'}) async {
@ -92,14 +92,12 @@ abstract class BaseAngelClient extends Angel {
//var v = json.decode(response.body); //var v = json.decode(response.body);
var v = jsonDecode(response.body); var v = jsonDecode(response.body);
if (v is! Map || if (v is! Map || !v.containsKey('data') || !v.containsKey('token')) {
!(v as Map).containsKey('data') ||
!(v as Map).containsKey('token')) {
throw AngelHttpException.notAuthenticated( throw AngelHttpException.notAuthenticated(
message: "Auth endpoint '$url' did not return a proper response."); message: "Auth endpoint '$url' did not return a proper response.");
} }
var r = AngelAuthResult.fromMap(v as Map); var r = AngelAuthResult.fromMap(v);
_onAuthenticated.add(r); _onAuthenticated.add(r);
return r; return r;
} on AngelHttpException { } on AngelHttpException {
@ -111,7 +109,7 @@ abstract class BaseAngelClient extends Angel {
@override @override
Future<void> close() async { Future<void> close() async {
client.close(); client!.close();
await _onAuthenticated.close(); await _onAuthenticated.close();
await Future.wait(_services.map((s) => s.close())).then((_) { await Future.wait(_services.map((s) => s.close())).then((_) {
_services.clear(); _services.clear();
@ -128,13 +126,13 @@ abstract class BaseAngelClient extends Angel {
if (authToken?.isNotEmpty == true) { if (authToken?.isNotEmpty == true) {
request.headers['authorization'] ??= 'Bearer $authToken'; request.headers['authorization'] ??= 'Bearer $authToken';
} }
return client.send(request); return client!.send(request);
} }
/// Sends a non-streaming [Request] and returns a non-streaming [Response]. /// Sends a non-streaming [Request] and returns a non-streaming [Response].
Future<http.Response> sendUnstreamed( Future<http.Response> sendUnstreamed(
String method, url, Map<String, String> headers, String method, url, Map<String, String>? headers,
[body, Encoding encoding]) async { [body, Encoding? encoding]) async {
var request = var request =
http.Request(method, url is Uri ? url : Uri.parse(url.toString())); http.Request(method, url is Uri ? url : Uri.parse(url.toString()));
@ -160,12 +158,12 @@ abstract class BaseAngelClient extends Angel {
@override @override
Service<Id, Data> service<Id, Data>(String path, Service<Id, Data> service<Id, Data>(String path,
{Type type, AngelDeserializer<Data> deserializer}) { {Type? type, AngelDeserializer<Data>? deserializer}) {
var url = baseUrl.replace(path: p.join(baseUrl.path, path)); var url = baseUrl.replace(path: p.join(baseUrl.path, path));
var s = BaseAngelService<Id, Data>(client, this, url, var s = BaseAngelService<Id, Data>(client, this, url,
deserializer: deserializer); deserializer: deserializer);
_services.add(s); _services.add(s);
return s; return s as Service<Id, Data>;
} }
Uri _join(url) { Uri _join(url) {
@ -180,65 +178,65 @@ abstract class BaseAngelClient extends Angel {
//} //}
@override @override
Future<http.Response> get(url, {Map<String, String> headers}) async { Future<http.Response> get(url, {Map<String, String>? headers}) async {
return sendUnstreamed('GET', _join(url), headers); return sendUnstreamed('GET', _join(url), headers);
} }
@override @override
Future<http.Response> head(url, {Map<String, String> headers}) async { Future<http.Response> head(url, {Map<String, String>? headers}) async {
return sendUnstreamed('HEAD', _join(url), headers); return sendUnstreamed('HEAD', _join(url), headers);
} }
@override @override
Future<http.Response> patch(url, Future<http.Response> patch(url,
{body, Map<String, String> headers, Encoding encoding}) async { {body, Map<String, String>? headers, Encoding? encoding}) async {
return sendUnstreamed('PATCH', _join(url), headers, body, encoding); return sendUnstreamed('PATCH', _join(url), headers, body, encoding);
} }
@override @override
Future<http.Response> post(url, Future<http.Response> post(url,
{body, Map<String, String> headers, Encoding encoding}) async { {body, Map<String, String>? headers, Encoding? encoding}) async {
return sendUnstreamed('POST', _join(url), headers, body, encoding); return sendUnstreamed('POST', _join(url), headers, body, encoding);
} }
@override @override
Future<http.Response> put(url, Future<http.Response> put(url,
{body, Map<String, String> headers, Encoding encoding}) async { {body, Map<String, String>? headers, Encoding? encoding}) async {
return sendUnstreamed('PUT', _join(url), headers, body, encoding); return sendUnstreamed('PUT', _join(url), headers, body, encoding);
} }
} }
class BaseAngelService<Id, Data> extends Service<Id, Data> { class BaseAngelService<Id, Data> extends Service<Id, Data?> {
@override @override
final BaseAngelClient app; final BaseAngelClient app;
final Uri baseUrl; final Uri baseUrl;
final http.BaseClient client; final http.BaseClient? client;
final AngelDeserializer<Data> deserializer; final AngelDeserializer<Data>? deserializer;
final StreamController<List<Data>> _onIndexed = StreamController(); final StreamController<List<Data?>> _onIndexed = StreamController();
final StreamController<Data> _onRead = StreamController(), final StreamController<Data?> _onRead = StreamController(),
_onCreated = StreamController(), _onCreated = StreamController(),
_onModified = StreamController(), _onModified = StreamController(),
_onUpdated = StreamController(), _onUpdated = StreamController(),
_onRemoved = StreamController(); _onRemoved = StreamController();
@override @override
Stream<List<Data>> 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;
@override @override
Stream<Data> get onCreated => _onCreated.stream; Stream<Data?> get onCreated => _onCreated.stream;
@override @override
Stream<Data> get onModified => _onModified.stream; Stream<Data?> get onModified => _onModified.stream;
@override @override
Stream<Data> get onUpdated => _onUpdated.stream; Stream<Data?> get onUpdated => _onUpdated.stream;
@override @override
Stream<Data> get onRemoved => _onRemoved.stream; Stream<Data?> get onRemoved => _onRemoved.stream;
@override @override
Future close() async { Future close() async {
@ -257,8 +255,8 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
@deprecated @deprecated
String get basePath => baseUrl.toString(); String get basePath => baseUrl.toString();
Data deserialize(x) { Data? deserialize(x) {
return deserializer != null ? deserializer(x) : x as Data; return deserializer != null ? deserializer!(x) : x as Data?;
} }
String makeBody(x) { String makeBody(x) {
@ -267,15 +265,15 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
} }
Future<http.StreamedResponse> send(http.BaseRequest request) { Future<http.StreamedResponse> send(http.BaseRequest request) {
if (app.authToken != null && app.authToken.isNotEmpty) { if (app.authToken != null && app.authToken!.isNotEmpty) {
request.headers['Authorization'] = 'Bearer ${app.authToken}'; request.headers['Authorization'] = 'Bearer ${app.authToken}';
} }
return client.send(request); return client!.send(request);
} }
@override @override
Future<List<Data>> index([Map<String, dynamic> params]) async { Future<List<Data?>?> index([Map<String, dynamic>? params]) async {
var url = baseUrl.replace(queryParameters: _buildQuery(params)); var url = baseUrl.replace(queryParameters: _buildQuery(params));
var response = await app.sendUnstreamed('GET', url, _readHeaders); var response = await app.sendUnstreamed('GET', url, _readHeaders);
@ -304,7 +302,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
} }
@override @override
Future<Data> read(id, [Map<String, dynamic> params]) async { Future<Data?> read(id, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -313,54 +311,58 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onRead.hasListener) if (_onRead.hasListener) {
_onRead.addError(failure(response)); _onRead.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onRead.add(r); _onRead.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onRead.hasListener) if (_onRead.hasListener) {
_onRead.addError(e, st); _onRead.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @override
Future<Data> create(data, [Map<String, dynamic> params]) async { Future<Data?> create(data, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace(queryParameters: _buildQuery(params)); var url = baseUrl.replace(queryParameters: _buildQuery(params));
var response = var response =
await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data)); await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data));
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onCreated.hasListener) if (_onCreated.hasListener) {
_onCreated.addError(failure(response)); _onCreated.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onCreated.add(r); _onCreated.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onCreated.hasListener) if (_onCreated.hasListener) {
_onCreated.addError(e, st); _onCreated.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @override
Future<Data> modify(id, data, [Map<String, dynamic> params]) async { Future<Data?> modify(id, data, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -370,27 +372,29 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onModified.hasListener) if (_onModified.hasListener) {
_onModified.addError(failure(response)); _onModified.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onModified.add(r); _onModified.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onModified.hasListener) if (_onModified.hasListener) {
_onModified.addError(e, st); _onModified.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @override
Future<Data> update(id, data, [Map<String, dynamic> params]) async { Future<Data?> update(id, data, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -400,27 +404,29 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onUpdated.hasListener) if (_onUpdated.hasListener) {
_onUpdated.addError(failure(response)); _onUpdated.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onUpdated.add(r); _onUpdated.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onUpdated.hasListener) if (_onUpdated.hasListener) {
_onUpdated.addError(e, st); _onUpdated.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @override
Future<Data> remove(id, [Map<String, dynamic> params]) async { Future<Data?> remove(id, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -429,20 +435,22 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onRemoved.hasListener) if (_onRemoved.hasListener) {
_onRemoved.addError(failure(response)); _onRemoved.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onRemoved.add(r); _onRemoved.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onRemoved.hasListener) if (_onRemoved.hasListener) {
_onRemoved.addError(e, st); _onRemoved.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;

View file

@ -13,20 +13,21 @@ export 'angel_client.dart';
/// Queries an Angel server via REST. /// Queries an Angel server via REST.
class Rest extends BaseAngelClient { class Rest extends BaseAngelClient {
Rest(String basePath) : super(new http.BrowserClient(), basePath); Rest(String basePath) : super(http.BrowserClient(), basePath);
@override
Future<AngelAuthResult> authenticate( Future<AngelAuthResult> authenticate(
{String type, {String? type,
credentials, credentials,
String authEndpoint = '/auth', String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}) async { @deprecated String reviveEndpoint = '/auth/token'}) async {
if (type == null || type == 'token') { if (type == null || type == 'token') {
if (!window.localStorage.containsKey('token')) { if (!window.localStorage.containsKey('token')) {
throw new Exception( throw Exception(
'Cannot revive token from localStorage - there is none.'); 'Cannot revive token from localStorage - there is none.');
} }
var token = json.decode(window.localStorage['token']); var token = json.decode(window.localStorage['token']!);
credentials ??= {'token': token}; credentials ??= {'token': token};
} }
@ -39,33 +40,34 @@ class Rest extends BaseAngelClient {
@override @override
Stream<String> authenticateViaPopup(String url, Stream<String> authenticateViaPopup(String url,
{String eventName = 'token', String errorMessage}) { {String eventName = 'token', String? errorMessage}) {
var ctrl = new StreamController<String>(); var ctrl = StreamController<String>();
var wnd = window.open(url, 'angel_client_auth_popup'); var wnd = window.open(url, 'angel_client_auth_popup');
Timer t; Timer t;
StreamSubscription sub; StreamSubscription? sub;
t = new Timer.periodic(new Duration(milliseconds: 500), (timer) { t = Timer.periodic(Duration(milliseconds: 500), (timer) {
if (!ctrl.isClosed) { if (!ctrl.isClosed) {
if (wnd.closed) { if (wnd.closed!) {
ctrl.addError(new AngelHttpException.notAuthenticated( ctrl.addError(AngelHttpException.notAuthenticated(
message: message:
errorMessage ?? 'Authentication via popup window failed.')); errorMessage ?? 'Authentication via popup window failed.'));
ctrl.close(); ctrl.close();
timer.cancel(); timer.cancel();
sub?.cancel(); sub?.cancel();
} }
} else } else {
timer.cancel(); timer.cancel();
}
}); });
sub = window.on[eventName ?? 'token'].listen((Event ev) { sub = window.on[eventName].listen((Event ev) {
var e = ev as CustomEvent; var e = ev as CustomEvent;
if (!ctrl.isClosed) { if (!ctrl.isClosed) {
ctrl.add(e.detail.toString()); ctrl.add(e.detail.toString());
t.cancel(); t.cancel();
ctrl.close(); ctrl.close();
sub.cancel(); sub!.cancel();
} }
}); });

View file

@ -8,12 +8,12 @@ export 'angel_client.dart';
/// Queries an Angel server via REST. /// Queries an Angel server via REST.
class Rest extends BaseAngelClient { class Rest extends BaseAngelClient {
Rest(String basePath) : super(new http.Client() as http.BaseClient, basePath); Rest(String basePath) : super(http.Client() as http.BaseClient, basePath);
@override @override
Stream<String> authenticateViaPopup(String url, Stream<String> authenticateViaPopup(String url,
{String eventName = 'token'}) { {String eventName = 'token'}) {
throw new UnimplementedError( throw UnimplementedError(
'Opening popup windows is not supported in the `flutter` client.'); 'Opening popup windows is not supported in the `flutter` client.');
} }
} }

View file

@ -17,11 +17,11 @@ class Rest extends BaseAngelClient {
@override @override
Service<Id, Data> service<Id, Data>(String path, Service<Id, Data> service<Id, Data>(String path,
{Type type, AngelDeserializer deserializer}) { {Type? type, AngelDeserializer? deserializer}) {
var url = baseUrl.replace(path: p.join(baseUrl.path, path)); var url = baseUrl.replace(path: p.join(baseUrl.path, path));
var s = RestService<Id, Data>(client, this, url, type); var s = RestService<Id, Data>(client, this, url, type);
_services.add(s); _services.add(s);
return s; return s as Service<Id, Data>;
} }
@override @override
@ -42,21 +42,21 @@ class Rest extends BaseAngelClient {
/// Queries an Angel service via REST. /// Queries an Angel service via REST.
class RestService<Id, Data> extends BaseAngelService<Id, Data> { class RestService<Id, Data> extends BaseAngelService<Id, Data> {
final Type type; final Type? type;
RestService(http.BaseClient client, BaseAngelClient app, url, this.type) RestService(http.BaseClient? client, BaseAngelClient app, url, this.type)
: super(client, app, url); : super(client, app, url);
@override @override
Data deserialize(x) { Data? deserialize(x) {
print(x); print(x);
if (type != null) { if (type != null) {
return x.runtimeType == type return x.runtimeType == type
? x as Data ? x as Data?
: god.deserializeDatum(x, outputType: type) as Data; : god.deserializeDatum(x, outputType: type) as Data?;
} }
return x as Data; return x as Data?;
} }
@override @override

View file

@ -1,41 +1,45 @@
name: angel_client name: angel_client
version: 3.0.0 version: 4.0.0
description: Support for querying Angel servers in the browser, Flutter, and command-line. description: Support for querying Angel servers in the browser, Flutter, and command-line.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_client homepage: https://github.com/angel-dart/angel_client
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel_http_exception: angel_http_exception:
git: git:
url: https://github.com/dukefirehawk/angel.git url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x ref: sdk-2.12.x_nnbd
path: packages/http_exception path: packages/http_exception
collection: ^1.0.0 collection: ^1.15.0
http: ^0.13.0 http: ^0.13.1
json_god: json_god:
git: git:
url: https://github.com/dukefirehawk/angel.git url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x ref: sdk-2.12.x_nnbd
path: packages/json_god path: packages/json_god
#dart_json_mapper: ^1.7.0 #dart_json_mapper: ^1.7.0
meta: ^1.0.0 meta: ^1.3.0
path: ^1.0.0 path: ^1.8.0
dev_dependencies: dev_dependencies:
angel_framework: angel_framework:
git: git:
url: https://github.com/dukefirehawk/angel.git url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x ref: sdk-2.12.x_nnbd
path: packages/framework path: packages/framework
angel_model: angel_model:
git: git:
url: https://github.com/dukefirehawk/angel.git url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x ref: sdk-2.12.x_nnbd
path: packages/model path: packages/model
async: ^2.0.0 async: ^2.5.0
build_runner: ^1.0.0 build_runner: ^1.12.2
build_web_compilers: ^2.12.2 build_web_compilers: ^2.16.5
mock_request: ^1.0.0 mock_request:
git:
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x_nnbd
path: packages/mock_request
pedantic: ^1.11.0 pedantic: ^1.11.0
test: ^1.16.5 test: ^1.16.8

View file

@ -9,75 +9,75 @@ void main() {
test('sets method,body,headers,path', () async { test('sets method,body,headers,path', () async {
await app.post(Uri.parse('/post'), await app.post(Uri.parse('/post'),
headers: {'method': 'post'}, body: 'post'); headers: {'method': 'post'}, body: 'post');
expect(app.client.spec.method, 'POST'); expect(app.client.spec!.method, 'POST');
expect(app.client.spec.path, '/post'); expect(app.client.spec!.path, '/post');
expect(app.client.spec.headers['method'], 'post'); expect(app.client.spec!.headers['method'], 'post');
expect(await read(app.client.spec.request.finalize()), 'post'); expect(await read(app.client.spec!.request.finalize()), 'post');
}); });
group('service methods', () { group('service methods', () {
test('index', () async { test('index', () async {
await todoService.index(); await todoService.index();
expect(app.client.spec.method, 'GET'); expect(app.client.spec!.method, 'GET');
expect(app.client.spec.path, '/api/todos'); expect(app.client.spec!.path, '/api/todos');
}); });
test('read', () async { test('read', () async {
await todoService.read('sleep'); await todoService.read('sleep');
expect(app.client.spec.method, 'GET'); expect(app.client.spec!.method, 'GET');
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
}); });
test('create', () async { test('create', () async {
await todoService.create({}); await todoService.create({});
expect(app.client.spec.method, 'POST'); expect(app.client.spec!.method, 'POST');
expect(app.client.spec.headers['content-type'], expect(app.client.spec!.headers['content-type'],
startsWith('application/json')); startsWith('application/json'));
expect(app.client.spec.path, '/api/todos'); expect(app.client.spec!.path, '/api/todos');
expect(await read(app.client.spec.request.finalize()), '{}'); expect(await read(app.client.spec!.request.finalize()), '{}');
}); });
test('modify', () async { test('modify', () async {
await todoService.modify('sleep', {}); await todoService.modify('sleep', {});
expect(app.client.spec.method, 'PATCH'); expect(app.client.spec!.method, 'PATCH');
expect(app.client.spec.headers['content-type'], expect(app.client.spec!.headers['content-type'],
startsWith('application/json')); startsWith('application/json'));
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
expect(await read(app.client.spec.request.finalize()), '{}'); expect(await read(app.client.spec!.request.finalize()), '{}');
}); });
test('update', () async { test('update', () async {
await todoService.update('sleep', {}); await todoService.update('sleep', {});
expect(app.client.spec.method, 'POST'); expect(app.client.spec!.method, 'POST');
expect(app.client.spec.headers['content-type'], expect(app.client.spec!.headers['content-type'],
startsWith('application/json')); startsWith('application/json'));
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
expect(await read(app.client.spec.request.finalize()), '{}'); expect(await read(app.client.spec!.request.finalize()), '{}');
}); });
test('remove', () async { test('remove', () async {
await todoService.remove('sleep'); await todoService.remove('sleep');
expect(app.client.spec.method, 'DELETE'); expect(app.client.spec!.method, 'DELETE');
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
}); });
}); });
group('authentication', () { group('authentication', () {
test('no type defaults to token', () async { test('no type defaults to token', () async {
await app.authenticate(credentials: '<jwt>'); await app.authenticate(credentials: '<jwt>');
expect(app.client.spec.path, '/auth/token'); expect(app.client.spec!.path, '/auth/token');
}); });
test('sets type', () async { test('sets type', () async {
await app.authenticate(type: 'local'); await app.authenticate(type: 'local');
expect(app.client.spec.path, '/auth/local'); expect(app.client.spec!.path, '/auth/local');
}); });
test('credentials send right body', () async { test('credentials send right body', () async {
await app await app
.authenticate(type: 'local', credentials: {'username': 'password'}); .authenticate(type: 'local', credentials: {'username': 'password'});
expect( expect(
await read(app.client.spec.request.finalize()), await read(app.client.spec!.request.finalize()),
json.encode({'username': 'password'}), json.encode({'username': 'password'}),
); );
}); });

View file

@ -10,24 +10,25 @@ Future<String> read(Stream<List<int>> stream) =>
class MockAngel extends BaseAngelClient { class MockAngel extends BaseAngelClient {
@override @override
final SpecClient client = new SpecClient(); final SpecClient client = SpecClient();
MockAngel() : super(null, 'http://localhost:3000'); MockAngel() : super(null, 'http://localhost:3000');
@override @override
authenticateViaPopup(String url, {String eventName = 'token'}) { Stream<String> authenticateViaPopup(String url,
throw new UnsupportedError('Nope'); {String eventName = 'token'}) {
throw UnsupportedError('Nope');
} }
} }
class SpecClient extends http.BaseClient { class SpecClient extends http.BaseClient {
Spec _spec; Spec? _spec;
Spec get spec => _spec; Spec? get spec => _spec;
@override @override
send(http.BaseRequest request) { Future<http.StreamedResponse> send(http.BaseRequest request) {
_spec = new Spec(request, request.method, request.url.path, request.headers, _spec = Spec(request, request.method, request.url.path, request.headers,
request.contentLength); request.contentLength);
dynamic data = {'text': 'Clean your room!', 'completed': true}; dynamic data = {'text': 'Clean your room!', 'completed': true};
@ -40,8 +41,8 @@ class SpecClient extends http.BaseClient {
data = [data]; data = [data];
} }
return new Future<http.StreamedResponse>.value(new http.StreamedResponse( return Future<http.StreamedResponse>.value(http.StreamedResponse(
new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]), Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),
200, 200,
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',
@ -54,7 +55,7 @@ class Spec {
final http.BaseRequest request; final http.BaseRequest request;
final String method, path; final String method, path;
final Map<String, String> headers; final Map<String, String> headers;
final int contentLength; final int? contentLength;
Spec(this.request, this.method, this.path, this.headers, this.contentLength); Spec(this.request, this.method, this.path, this.headers, this.contentLength);

View file

@ -6,22 +6,22 @@ import 'package:angel_framework/http.dart' as s;
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { void main() {
HttpServer server; late HttpServer server;
c.Angel app; late c.Angel app;
c.ServiceList list; late c.ServiceList list;
StreamQueue queue; late StreamQueue queue;
setUp(() async { setUp(() async {
var serverApp = new s.Angel(); var serverApp = s.Angel();
var http = new s.AngelHttp(serverApp); var http = s.AngelHttp(serverApp);
serverApp.use('/api/todos', new s.MapService(autoIdAndDateFields: false)); serverApp.use('/api/todos', s.MapService(autoIdAndDateFields: false));
server = await http.startServer(); server = await http.startServer();
var uri = 'http://${server.address.address}:${server.port}'; var uri = 'http://${server.address.address}:${server.port}';
app = new c.Rest(uri); app = c.Rest(uri);
list = new c.ServiceList(app.service('api/todos')); list = c.ServiceList(app.service('api/todos'));
queue = new StreamQueue(list.onChange); queue = StreamQueue(list.onChange);
}); });
tearDown(() async { tearDown(() async {

View file

@ -1,14 +1,14 @@
import 'package:angel_model/angel_model.dart'; import 'package:angel_model/angel_model.dart';
class Postcard extends Model { class Postcard extends Model {
String location; String? location;
String message; String? message;
Postcard({String id, this.location, this.message}) { Postcard({String? id, this.location, this.message}) {
this.id = id; this.id = id;
} }
factory Postcard.fromJson(Map data) => new Postcard( factory Postcard.fromJson(Map data) => Postcard(
id: data['id'].toString(), id: data['id'].toString(),
location: data['location'].toString(), location: data['location'].toString(),
message: data['message'].toString()); message: data['message'].toString());

View file

@ -2,7 +2,7 @@ import 'dart:html';
import 'package:angel_client/browser.dart'; import 'package:angel_client/browser.dart';
/// Dummy app to ensure client works with DDC. /// Dummy app to ensure client works with DDC.
main() { void main() {
var app = new Rest(window.location.origin); var app = Rest(window.location.origin);
window.alert(app.baseUrl.toString()); window.alert(app.baseUrl.toString());
} }