import 'dart:convert'; import 'dart:io'; import '../../../domain/models/server_config.dart'; class ApiException implements Exception { const ApiException(this.statusCode, this.message); final int statusCode; final String message; @override String toString() => 'ApiException $statusCode: $message'; } abstract class LocaliserdClient { const LocaliserdClient({required this.config, this.token}); final ServerConfig config; /// Null for unauthenticated clients (session, onboarding setup). final String? token; Uri _uri(String path) => Uri( scheme: 'http', host: config.host, port: config.port, path: path, ); Future get(String path) => _request('GET', path); Future post(String path, [Object? body]) => _request('POST', path, body); Future put(String path, [Object? body]) => _request('PUT', path, body); Future patch(String path, [Object? body]) => _request('PATCH', path, body); Future delete(String path) => _request('DELETE', path).then((_) {}); /// Like [delete] but returns the response body (for endpoints that do so). Future deleteBody(String path) => _request('DELETE', path); Future _request(String method, String path, [Object? body]) async { final http = HttpClient(); try { final request = await http.openUrl(method, _uri(path)); request.headers.contentType = ContentType.json; if (token != null) { request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $token'); } if (body != null) request.write(jsonEncode(body)); final response = await request.close(); if (response.statusCode == 204) return null; final responseBody = await response.transform(utf8.decoder).join(); if (response.statusCode >= 400) { final parsed = responseBody.isNotEmpty ? jsonDecode(responseBody) as Map? : null; throw ApiException( response.statusCode, parsed?['error']?.toString() ?? responseBody, ); } return responseBody.isEmpty ? null : jsonDecode(responseBody); } finally { http.close(); } } }