2016-12-09 00:24:07 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:angel_framework/src/http/angel_http_exception.dart';
|
|
|
|
import 'package:collection/collection.dart';
|
|
|
|
import 'package:http/src/base_client.dart' as http;
|
|
|
|
import 'package:http/src/base_request.dart' as http;
|
|
|
|
import 'package:http/src/request.dart' as http;
|
|
|
|
import 'package:http/src/response.dart' as http;
|
|
|
|
import 'package:http/src/streamed_response.dart' as http;
|
|
|
|
import 'angel_client.dart';
|
|
|
|
|
2016-12-10 14:45:22 +00:00
|
|
|
final RegExp straySlashes = new RegExp(r"(^/)|(/+$)");
|
2016-12-09 00:24:07 +00:00
|
|
|
const Map<String, String> _readHeaders = const {'Accept': 'application/json'};
|
2016-12-13 16:34:22 +00:00
|
|
|
const Map<String, String> _writeHeaders = const {
|
|
|
|
'Accept': 'application/json',
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
};
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
_buildQuery(Map params) {
|
2017-02-22 22:20:30 +00:00
|
|
|
if (params == null || params.isEmpty || params['query'] is! Map) return "";
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
List<String> query = [];
|
|
|
|
|
2017-02-22 22:20:30 +00:00
|
|
|
params['query'].forEach((k, v) {
|
2016-12-10 14:45:22 +00:00
|
|
|
query.add('$k=${Uri.encodeQueryComponent(v.toString())}');
|
2016-12-09 00:24:07 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return '?' + query.join('&');
|
|
|
|
}
|
|
|
|
|
2017-03-29 01:52:19 +00:00
|
|
|
bool _invalid(http.Response response) =>
|
|
|
|
response.statusCode == null ||
|
|
|
|
response.statusCode < 200 ||
|
|
|
|
response.statusCode >= 300;
|
|
|
|
|
2016-12-09 00:24:07 +00:00
|
|
|
AngelHttpException failure(http.Response response, {error, StackTrace stack}) {
|
|
|
|
try {
|
|
|
|
final json = JSON.decode(response.body);
|
|
|
|
|
|
|
|
if (json is Map && json['isError'] == true) {
|
|
|
|
return new AngelHttpException.fromMap(json);
|
|
|
|
} else {
|
|
|
|
return new AngelHttpException(error,
|
|
|
|
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.',
|
|
|
|
statusCode: response.statusCode,
|
|
|
|
stackTrace: stack ?? st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class BaseAngelClient extends Angel {
|
2017-06-03 17:43:01 +00:00
|
|
|
final StreamController<AngelAuthResult> _onAuthenticated =
|
|
|
|
new StreamController<AngelAuthResult>();
|
|
|
|
final List<Service> _services = [];
|
2016-12-09 00:24:07 +00:00
|
|
|
final http.BaseClient client;
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
@override
|
|
|
|
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
|
|
|
|
|
2016-12-09 00:24:07 +00:00
|
|
|
BaseAngelClient(this.client, String basePath) : super(basePath);
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<AngelAuthResult> authenticate(
|
2017-03-07 21:54:13 +00:00
|
|
|
{String type,
|
2016-12-09 00:24:07 +00:00
|
|
|
credentials,
|
|
|
|
String authEndpoint: '/auth',
|
|
|
|
String reviveEndpoint: '/auth/token'}) async {
|
|
|
|
if (type == null) {
|
|
|
|
final url = '$basePath$reviveEndpoint';
|
2016-12-13 16:34:22 +00:00
|
|
|
final response = await client.post(url, headers: {
|
|
|
|
'Accept': 'application/json',
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Authorization': 'Bearer ${credentials['token']}'
|
|
|
|
});
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
final json = JSON.decode(response.body);
|
|
|
|
|
|
|
|
if (json is! Map ||
|
|
|
|
!json.containsKey('data') ||
|
|
|
|
!json.containsKey('token')) {
|
2017-01-20 23:57:42 +00:00
|
|
|
throw new AngelHttpException.notAuthenticated(
|
2016-12-09 00:24:07 +00:00
|
|
|
message:
|
|
|
|
"Auth endpoint '$url' did not return a proper response.");
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = new AngelAuthResult.fromMap(json);
|
|
|
|
_onAuthenticated.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
final url = '$basePath$authEndpoint/$type';
|
|
|
|
http.Response response;
|
|
|
|
|
|
|
|
if (credentials != null) {
|
|
|
|
response = await client.post(url,
|
|
|
|
body: JSON.encode(credentials), headers: _writeHeaders);
|
|
|
|
} else {
|
|
|
|
response = await client.post(url, headers: _writeHeaders);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
final json = JSON.decode(response.body);
|
|
|
|
|
|
|
|
if (json is! Map ||
|
|
|
|
!json.containsKey('data') ||
|
|
|
|
!json.containsKey('token')) {
|
2017-01-25 23:25:31 +00:00
|
|
|
throw new AngelHttpException.notAuthenticated(
|
2016-12-09 00:24:07 +00:00
|
|
|
message:
|
|
|
|
"Auth endpoint '$url' did not return a proper response.");
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = new AngelAuthResult.fromMap(json);
|
|
|
|
_onAuthenticated.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-10 14:50:05 +00:00
|
|
|
Future close() async {
|
|
|
|
client.close();
|
2017-06-03 17:43:01 +00:00
|
|
|
_onAuthenticated.close();
|
|
|
|
Future.wait(_services.map((s) => s.close())).then((_) {
|
|
|
|
_services.clear();
|
|
|
|
});
|
2016-12-10 14:50:05 +00:00
|
|
|
}
|
|
|
|
|
2017-03-29 01:52:19 +00:00
|
|
|
Future logout() async {
|
|
|
|
authToken = null;
|
|
|
|
}
|
|
|
|
|
2017-01-25 23:25:31 +00:00
|
|
|
/// Sends a non-streaming [Request] and returns a non-streaming [Response].
|
|
|
|
Future<http.Response> sendUnstreamed(
|
|
|
|
String method, url, Map<String, String> headers,
|
|
|
|
[body, Encoding encoding]) async {
|
|
|
|
if (url is String) url = Uri.parse(url);
|
|
|
|
var request = new http.Request(method, url);
|
|
|
|
|
|
|
|
if (headers != null) request.headers.addAll(headers);
|
|
|
|
|
|
|
|
if (authToken?.isNotEmpty == true)
|
|
|
|
request.headers['Authorization'] = 'Bearer $authToken';
|
|
|
|
|
|
|
|
if (encoding != null) request.encoding = encoding;
|
|
|
|
if (body != null) {
|
|
|
|
if (body is String) {
|
|
|
|
request.body = body;
|
|
|
|
} else if (body is List) {
|
|
|
|
request.bodyBytes = DelegatingList.typed(body);
|
|
|
|
} else if (body is Map) {
|
|
|
|
request.bodyFields = DelegatingMap.typed(body);
|
|
|
|
} else {
|
|
|
|
throw new ArgumentError('Invalid request body "$body".');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return http.Response.fromStream(await client.send(request));
|
|
|
|
}
|
|
|
|
|
2016-12-09 00:24:07 +00:00
|
|
|
@override
|
2017-06-03 17:43:01 +00:00
|
|
|
Service<T> service<T>(String path,
|
|
|
|
{Type type, AngelDeserializer deserializer}) {
|
2016-12-13 16:34:22 +00:00
|
|
|
String uri = path.toString().replaceAll(straySlashes, "");
|
2017-06-03 17:43:01 +00:00
|
|
|
var s = new BaseAngelService<T>(client, this, '$basePath/$uri',
|
2016-12-13 16:35:35 +00:00
|
|
|
deserializer: deserializer);
|
2017-06-03 17:43:01 +00:00
|
|
|
_services.add(s);
|
|
|
|
return s;
|
2016-12-09 00:24:07 +00:00
|
|
|
}
|
2016-12-10 17:15:54 +00:00
|
|
|
|
|
|
|
String _join(url) {
|
|
|
|
final head = basePath.replaceAll(new RegExp(r'/+$'), '');
|
2016-12-10 17:28:24 +00:00
|
|
|
final tail = url.replaceAll(straySlashes, '');
|
2016-12-10 17:15:54 +00:00
|
|
|
return '$head/$tail';
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<http.Response> delete(String url,
|
|
|
|
{Map<String, String> headers}) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
return sendUnstreamed('DELETE', _join(url), headers);
|
2016-12-10 17:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<http.Response> get(String url, {Map<String, String> headers}) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
return sendUnstreamed('GET', _join(url), headers);
|
2016-12-10 17:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<http.Response> head(String url, {Map<String, String> headers}) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
return sendUnstreamed('HEAD', _join(url), headers);
|
2016-12-10 17:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<http.Response> patch(String url,
|
|
|
|
{body, Map<String, String> headers}) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
return sendUnstreamed('PATCH', _join(url), headers, body);
|
2016-12-10 17:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<http.Response> post(String url,
|
|
|
|
{body, Map<String, String> headers}) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
return sendUnstreamed('POST', _join(url), headers, body);
|
2016-12-10 17:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<http.Response> put(String url,
|
|
|
|
{body, Map<String, String> headers}) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
return sendUnstreamed('PUT', _join(url), headers, body);
|
2016-12-10 17:15:54 +00:00
|
|
|
}
|
2016-12-09 00:24:07 +00:00
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
class BaseAngelService<T> extends Service<T> {
|
2016-12-09 00:24:07 +00:00
|
|
|
@override
|
2017-01-25 23:25:31 +00:00
|
|
|
final BaseAngelClient app;
|
2016-12-09 00:24:07 +00:00
|
|
|
final String basePath;
|
|
|
|
final http.BaseClient client;
|
2016-12-13 16:35:35 +00:00
|
|
|
final AngelDeserializer deserializer;
|
2016-12-09 00:24:07 +00:00
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
final StreamController<T> _onIndexed = new StreamController<T>(),
|
|
|
|
_onRead = new StreamController<T>(),
|
|
|
|
_onCreated = new StreamController<T>(),
|
|
|
|
_onModified = new StreamController<T>(),
|
|
|
|
_onUpdated = new StreamController<T>(),
|
|
|
|
_onRemoved = new StreamController<T>();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<T> get onIndexed => _onIndexed.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<T> get onRead => _onRead.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<T> get onCreated => _onCreated.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<T> get onModified => _onModified.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<T> get onUpdated => _onUpdated.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<T> get onRemoved => _onRemoved.stream;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future close() async {
|
|
|
|
_onIndexed.close();
|
|
|
|
_onRead.close();
|
|
|
|
_onCreated.close();
|
|
|
|
_onModified.close();
|
|
|
|
_onUpdated.close();
|
|
|
|
_onRemoved.close();
|
|
|
|
}
|
|
|
|
|
2016-12-13 16:35:35 +00:00
|
|
|
BaseAngelService(this.client, this.app, this.basePath, {this.deserializer});
|
|
|
|
|
|
|
|
deserialize(x) {
|
|
|
|
return deserializer != null ? deserializer(x) : x;
|
|
|
|
}
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
makeBody(x) {
|
|
|
|
return JSON.encode(x);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
|
|
|
if (app.authToken != null && app.authToken.isNotEmpty) {
|
|
|
|
request.headers['Authorization'] = 'Bearer ${app.authToken}';
|
|
|
|
}
|
|
|
|
|
|
|
|
return client.send(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2017-02-12 19:58:18 +00:00
|
|
|
Future index([Map params]) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
final response = await app.sendUnstreamed(
|
2017-02-22 22:20:30 +00:00
|
|
|
'GET', '$basePath${_buildQuery(params)}', _readHeaders);
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
final json = JSON.decode(response.body);
|
2017-06-03 17:43:01 +00:00
|
|
|
|
|
|
|
if (json is! List) {
|
|
|
|
_onIndexed.add(json);
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
|
|
|
|
var r = json.map(deserialize).toList();
|
|
|
|
_onIndexed.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future read(id, [Map params]) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
final response = await app.sendUnstreamed(
|
2016-12-09 00:24:07 +00:00
|
|
|
'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders);
|
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = deserialize(JSON.decode(response.body));
|
|
|
|
_onRead.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future create(data, [Map params]) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
final response = await app.sendUnstreamed('POST',
|
2016-12-10 14:45:22 +00:00
|
|
|
'$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = deserialize(JSON.decode(response.body));
|
|
|
|
_onCreated.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future modify(id, data, [Map params]) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
final response = await app.sendUnstreamed('PATCH',
|
2016-12-10 14:45:22 +00:00
|
|
|
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = deserialize(JSON.decode(response.body));
|
|
|
|
_onModified.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future update(id, data, [Map params]) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
final response = await app.sendUnstreamed('POST',
|
2016-12-10 14:45:22 +00:00
|
|
|
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
2016-12-09 00:24:07 +00:00
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = deserialize(JSON.decode(response.body));
|
|
|
|
_onUpdated.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future remove(id, [Map params]) async {
|
2017-01-25 23:25:31 +00:00
|
|
|
final response = await app.sendUnstreamed(
|
2016-12-09 00:24:07 +00:00
|
|
|
'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders);
|
|
|
|
|
|
|
|
try {
|
2017-03-29 01:52:19 +00:00
|
|
|
if (_invalid(response)) {
|
2016-12-09 00:24:07 +00:00
|
|
|
throw failure(response);
|
|
|
|
}
|
|
|
|
|
2017-06-03 17:43:01 +00:00
|
|
|
var r = deserialize(JSON.decode(response.body));
|
|
|
|
_onRemoved.add(r);
|
|
|
|
return r;
|
2016-12-09 00:24:07 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
throw failure(response, error: e, stack: st);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|