165 lines
4.8 KiB
Dart
165 lines
4.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:ac_web/Api/ApiConfig.dart';
|
|
import 'package:ac_web/Api/ApiException.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
/// Thin, transport only wrapper around `package:http`.
|
|
///
|
|
/// It knows nothing about the concrete controllers of the backend, it only
|
|
/// performs requests, maps failures onto [ApiException]s and decodes json.
|
|
/// Everything endpoint specific lives in the services under `Api/Services`.
|
|
class ApiClient {
|
|
ApiClient({ApiConfig config = ApiConfig.development, http.Client? httpClient})
|
|
: mConfig = config,
|
|
mHttpClient = httpClient ?? http.Client(),
|
|
mOwnsHttpClient = httpClient == null;
|
|
|
|
final ApiConfig mConfig;
|
|
final http.Client mHttpClient;
|
|
final bool mOwnsHttpClient;
|
|
|
|
ApiConfig get config => mConfig;
|
|
|
|
/// Performs a GET request and returns the decoded json body.
|
|
Future<dynamic> getJson(String path, {Map<String, dynamic>? query, Map<String, String>? headers}) {
|
|
return _send('GET', path, query: query, headers: headers);
|
|
}
|
|
|
|
/// Performs a GET request and maps a json array onto a list of models.
|
|
Future<List<T>> getList<T>(
|
|
String path,
|
|
T Function(Map<String, dynamic> json) fromJson, {
|
|
Map<String, dynamic>? query,
|
|
Map<String, String>? headers,
|
|
}) async {
|
|
final uri = mConfig.buildUri(path, query);
|
|
final body = await _send('GET', path, query: query, headers: headers);
|
|
|
|
if (body == null) {
|
|
return <T>[];
|
|
}
|
|
if (body is! List) {
|
|
throw ApiParsingException('Expected a json array but got ${body.runtimeType}.', uri: uri);
|
|
}
|
|
|
|
try {
|
|
return body
|
|
.whereType<Map>()
|
|
.map((entry) => fromJson(Map<String, dynamic>.from(entry)))
|
|
.toList(growable: false);
|
|
} on ApiException {
|
|
rethrow;
|
|
} catch (error) {
|
|
throw ApiParsingException('Failed to parse list of $T: $error', uri: uri);
|
|
}
|
|
}
|
|
|
|
/// Performs a GET request and maps a json object onto a single model.
|
|
Future<T> getObject<T>(
|
|
String path,
|
|
T Function(Map<String, dynamic> json) fromJson, {
|
|
Map<String, dynamic>? query,
|
|
Map<String, String>? headers,
|
|
}) async {
|
|
final uri = mConfig.buildUri(path, query);
|
|
final body = await _send('GET', path, query: query, headers: headers);
|
|
|
|
if (body is! Map) {
|
|
throw ApiParsingException('Expected a json object but got ${body.runtimeType}.', uri: uri);
|
|
}
|
|
|
|
try {
|
|
return fromJson(Map<String, dynamic>.from(body));
|
|
} on ApiException {
|
|
rethrow;
|
|
} catch (error) {
|
|
throw ApiParsingException('Failed to parse $T: $error', uri: uri);
|
|
}
|
|
}
|
|
|
|
Future<dynamic> postJson(
|
|
String path, {
|
|
Object? body,
|
|
Map<String, dynamic>? query,
|
|
Map<String, String>? headers,
|
|
}) {
|
|
return _send('POST', path, body: body, query: query, headers: headers);
|
|
}
|
|
|
|
Future<dynamic> putJson(
|
|
String path, {
|
|
Object? body,
|
|
Map<String, dynamic>? query,
|
|
Map<String, String>? headers,
|
|
}) {
|
|
return _send('PUT', path, body: body, query: query, headers: headers);
|
|
}
|
|
|
|
Future<dynamic> deleteJson(
|
|
String path, {
|
|
Object? body,
|
|
Map<String, dynamic>? query,
|
|
Map<String, String>? headers,
|
|
}) {
|
|
return _send('DELETE', path, body: body, query: query, headers: headers);
|
|
}
|
|
|
|
void dispose() {
|
|
if (mOwnsHttpClient) {
|
|
mHttpClient.close();
|
|
}
|
|
}
|
|
|
|
Future<dynamic> _send(
|
|
String method,
|
|
String path, {
|
|
Object? body,
|
|
Map<String, dynamic>? query,
|
|
Map<String, String>? headers,
|
|
}) async {
|
|
final uri = mConfig.buildUri(path, query);
|
|
|
|
final request = http.Request(method, uri);
|
|
request.headers.addAll({
|
|
'Accept': 'application/json',
|
|
...mConfig.defaultHeaders,
|
|
...?headers,
|
|
});
|
|
if (body != null) {
|
|
request.headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
request.body = jsonEncode(body);
|
|
}
|
|
|
|
http.Response response;
|
|
try {
|
|
final streamed = await mHttpClient.send(request).timeout(mConfig.timeout);
|
|
response = await http.Response.fromStream(streamed);
|
|
} on TimeoutException {
|
|
throw ApiTimeoutException('Request timed out after ${mConfig.timeout.inSeconds}s.', uri: uri);
|
|
} catch (error) {
|
|
throw ApiNetworkException('Could not reach the api: $error', uri: uri);
|
|
}
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw ApiHttpException(
|
|
statusCode: response.statusCode,
|
|
message: response.reasonPhrase ?? 'Request failed.',
|
|
body: response.body,
|
|
uri: uri,
|
|
);
|
|
}
|
|
|
|
if (response.statusCode == 204 || response.body.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return jsonDecode(utf8.decode(response.bodyBytes));
|
|
} catch (error) {
|
|
throw ApiParsingException('Response was no valid json: $error', uri: uri, body: response.body);
|
|
}
|
|
}
|
|
}
|