feat(proj): init
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:please_pay_me/data/models/json.dart';
|
||||
|
||||
class ApiException implements Exception {
|
||||
const ApiException(this.message, {this.statusCode});
|
||||
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
bool get isUnauthorized => statusCode == 401;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// Thin JSON transport over the PleasePayMe REST API.
|
||||
///
|
||||
/// Keeps auth concerns out of repositories: the token is supplied lazily so a
|
||||
/// re-login does not require rebuilding the whole object graph.
|
||||
class ApiClient {
|
||||
ApiClient({
|
||||
required String baseUrl,
|
||||
required String? Function() tokenProvider,
|
||||
http.Client? httpClient,
|
||||
this.onUnauthorized,
|
||||
this.timeout = const Duration(seconds: 15),
|
||||
}) : _baseUrl = baseUrl.replaceAll(RegExp(r'/$'), ''),
|
||||
_tokenProvider = tokenProvider,
|
||||
_http = httpClient ?? http.Client();
|
||||
|
||||
final String _baseUrl;
|
||||
final String? Function() _tokenProvider;
|
||||
final http.Client _http;
|
||||
final void Function()? onUnauthorized;
|
||||
final Duration timeout;
|
||||
|
||||
Future<Map<String, dynamic>> getJson(String path, {Map<String, String>? query}) async {
|
||||
return asMap(await _send('GET', path, query: query));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> postJson(
|
||||
String path, {
|
||||
Map<String, dynamic>? body,
|
||||
Map<String, String>? query,
|
||||
}) async {
|
||||
return asMap(await _send('POST', path, body: body, query: query));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> putJson(String path, {Map<String, dynamic>? body}) async {
|
||||
return asMap(await _send('PUT', path, body: body));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> patchJson(String path, {Map<String, dynamic>? body}) async {
|
||||
return asMap(await _send('PATCH', path, body: body));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> deleteJson(String path, {Map<String, String>? query}) async {
|
||||
return asMap(await _send('DELETE', path, query: query));
|
||||
}
|
||||
|
||||
Future<Object?> _send(
|
||||
String method,
|
||||
String path, {
|
||||
Map<String, dynamic>? body,
|
||||
Map<String, String>? query,
|
||||
}) async {
|
||||
final uri = Uri.parse('$_baseUrl$path').replace(
|
||||
queryParameters: query?.isEmpty ?? true ? null : query,
|
||||
);
|
||||
|
||||
final request = http.Request(method, uri);
|
||||
request.headers['Accept'] = 'application/json';
|
||||
final token = _tokenProvider();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
if (body != null) {
|
||||
request.headers['Content-Type'] = 'application/json';
|
||||
request.body = jsonEncode(body);
|
||||
}
|
||||
|
||||
late final http.Response response;
|
||||
try {
|
||||
final streamed = await _http.send(request).timeout(timeout);
|
||||
response = await http.Response.fromStream(streamed);
|
||||
} on Exception catch (error) {
|
||||
throw ApiException('Нет связи с сервером: $error');
|
||||
}
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
onUnauthorized?.call();
|
||||
throw const ApiException('Сессия истекла, войдите заново', statusCode: 401);
|
||||
}
|
||||
|
||||
final raw = utf8.decode(response.bodyBytes);
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
throw ApiException(_extractError(raw, response.statusCode), statusCode: response.statusCode);
|
||||
}
|
||||
|
||||
if (response.statusCode == 204 || raw.trim().isEmpty) return null;
|
||||
|
||||
try {
|
||||
return jsonDecode(raw);
|
||||
} on FormatException {
|
||||
throw ApiException('Сервер вернул не JSON (HTTP ${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
String _extractError(String raw, int statusCode) {
|
||||
try {
|
||||
final parsed = asMap(jsonDecode(raw));
|
||||
final detail = asStringOrNull(parsed['detail']) ?? asStringOrNull(parsed['title']);
|
||||
if (detail != null) return detail;
|
||||
} on FormatException {
|
||||
// Fall through to the raw payload.
|
||||
}
|
||||
return raw.trim().isEmpty ? 'Ошибка запроса (HTTP $statusCode)' : raw.trim();
|
||||
}
|
||||
|
||||
void close() => _http.close();
|
||||
}
|
||||
Reference in New Issue
Block a user