Files

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);
}
}
}