diff --git a/lib/src/core/rest_client/src/exception/rest_client_exception.dart b/lib/src/core/rest_client/src/exception/rest_client_exception.dart index 772c09e..82bdd30 100644 --- a/lib/src/core/rest_client/src/exception/rest_client_exception.dart +++ b/lib/src/core/rest_client/src/exception/rest_client_exception.dart @@ -1,12 +1,11 @@ import 'package:meta/meta.dart'; import 'package:sizzle_starter/src/core/rest_client/rest_client.dart'; -// coverage:ignore-start /// {@template rest_client_exception} -/// Base class for all rest client exceptions +/// Base class for all [RestClient] exceptions /// {@endtemplate} @immutable -abstract base class RestClientException implements Exception { +sealed class RestClientException implements Exception { /// {@macro network_exception} const RestClientException({ required this.message, @@ -64,9 +63,10 @@ final class ClientException extends RestClientException { /// /// This class exists to make handling of structured errors easier. /// Basically, in data providers that use [RestClientBase], you can catch -/// this exception and convert it to a system-wide error. For example, -/// if backend returns an error with code 123 that means that the action -/// is not allowed, you can convert this exception to a NotAllowedException +/// this exception and convert it to a system-wide error. +/// +/// For example, if backend returns an error with code "not_allowed" that means that the action +/// is not allowed and you can convert this exception to a NotAllowedException /// and rethrow. This way, the rest of the application does not need to know /// about the structure of the error and should only handle system-wide /// exceptions. @@ -142,4 +142,3 @@ final class InternalServerException extends RestClientException { 'cause: $cause' ')'; } -// coverage:ignore-end diff --git a/lib/src/core/rest_client/src/http/rest_client_http.dart b/lib/src/core/rest_client/src/http/rest_client_http.dart index 90e5909..fb5522f 100644 --- a/lib/src/core/rest_client/src/http/rest_client_http.dart +++ b/lib/src/core/rest_client/src/http/rest_client_http.dart @@ -78,12 +78,10 @@ final class RestClientHttp extends RestClientBase { request.headers.addAll(headers); } - final response = await _client.send(request).then( - http.Response.fromStream, - ); + final response = await _client.send(request).then(http.Response.fromStream); final result = await decodeResponse( - response.bodyBytes, + BytesResponseBody(response.bodyBytes), statusCode: response.statusCode, ); diff --git a/lib/src/core/rest_client/src/rest_client.dart b/lib/src/core/rest_client/src/rest_client.dart index 54e20a0..9a7cee1 100644 --- a/lib/src/core/rest_client/src/rest_client.dart +++ b/lib/src/core/rest_client/src/rest_client.dart @@ -1,7 +1,7 @@ /// {@template rest_client} /// A REST client for making HTTP requests. /// {@endtemplate} -abstract class RestClient { +abstract interface class RestClient { /// Sends a GET request to the given [path]. Future?> get( String path, { diff --git a/lib/src/core/rest_client/src/rest_client_base.dart b/lib/src/core/rest_client/src/rest_client_base.dart index 583a9c2..d64e3e1 100644 --- a/lib/src/core/rest_client/src/rest_client_base.dart +++ b/lib/src/core/rest_client/src/rest_client_base.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:isolate'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'package:sizzle_starter/src/core/rest_client/rest_client.dart'; @@ -135,26 +134,16 @@ abstract base class RestClientBase implements RestClient { @protected @visibleForTesting Future?> decodeResponse( - /* String, Map, List */ - Object? body, { + ResponseBody? body, { int? statusCode, }) async { if (body == null) return null; - assert( - body is String || body is Map || body is List, - 'Unexpected response body type: ${body.runtimeType}', - ); - try { final decodedBody = switch (body) { - final Map map => map, - final String str => await _decodeString(str), - final List bytes => await _decodeBytes(bytes), - _ => throw WrongResponseTypeException( - message: 'Unexpected response body type: ${body.runtimeType}', - statusCode: statusCode, - ), + MapResponseBody(:final Map data) => data, + StringResponseBody(:final String data) => await _decodeString(data), + BytesResponseBody(:final List data) => await _decodeBytes(data), }; if (decodedBody case {'error': final Map error}) { @@ -169,8 +158,6 @@ abstract base class RestClientBase implements RestClient { } // Simply return decoded body if it is not an error or data - // This is useful for responses that do not follow the structured response - // But generally, it is recommended to follow the structured response :) return decodedBody; } on RestClientException { rethrow; @@ -187,26 +174,67 @@ abstract base class RestClientBase implements RestClient { } /// Decodes a [String] to a [Map] - Future?> _decodeString(String str) async { - if (str.isEmpty) return null; - - if (str.length > 1000) { - return Isolate.run(() => json.decode(str) as Map); + Future?> _decodeString(String stringBody) async { + if (stringBody.isEmpty) return null; + + if (stringBody.length > 1000) { + return (await compute( + json.decode, + stringBody, + debugLabel: kDebugMode ? 'Decode String Compute' : null, + )) as Map; } - return json.decode(str) as Map; + return json.decode(stringBody) as Map; } /// Decodes a [List] to a [Map] - Future?> _decodeBytes(List bytes) async { - if (bytes.isEmpty) return null; - - if (bytes.length > 1000) { - return Isolate.run( - () => _jsonUTF8.decode(bytes)! as Map, - ); + Future?> _decodeBytes(List bytesBody) async { + if (bytesBody.isEmpty) return null; + + if (bytesBody.length > 1000) { + return (await compute( + _jsonUTF8.decode, + bytesBody, + debugLabel: kDebugMode ? 'Decode Bytes Compute' : null, + ))! as Map; } - return _jsonUTF8.decode(bytes)! as Map; + return _jsonUTF8.decode(bytesBody)! as Map; } } + +/// {@template response_body} +/// A sealed class representing the response body +/// {@endtemplate} +sealed class ResponseBody { + /// {@macro response_body} + const ResponseBody(this.data); + + /// The data of the response. + final T data; +} + +/// {@template string_response_body} +/// A [ResponseBody] for a [String] response +/// {@endtemplate} +class StringResponseBody extends ResponseBody { + /// {@macro string_response_body} + const StringResponseBody(super.data); +} + +/// {@template map_response_body} +/// A [ResponseBody] for a [Map] response +/// {@endtemplate} +class MapResponseBody extends ResponseBody> { + /// {@macro map_response_body} + const MapResponseBody(super.data); +} + +/// {@template bytes_response_body} +/// A [ResponseBody] for both [Uint8List] and [List] responses +/// {@endtemplate} +class BytesResponseBody extends ResponseBody> { + /// {@macro bytes_response_body} + const BytesResponseBody(super.data); +} diff --git a/test/src/core/rest_client/rest_client_base_test.dart b/test/src/core/rest_client/rest_client_base_test.dart index d0d0f28..4d6682a 100644 --- a/test/src/core/rest_client/rest_client_base_test.dart +++ b/test/src/core/rest_client/rest_client_base_test.dart @@ -62,28 +62,28 @@ void main() { test('decodeResponseWithEmptyBody', () { final client = NoOpRestClientBase(baseUrl: 'http://localhost:8080'); - expectLater(client.decodeResponse([]), completion(isNull)); + expectLater(client.decodeResponse(const BytesResponseBody([])), completion(isNull)); }); test('decodeResponseWithMapBody', () { final client = NoOpRestClientBase(baseUrl: 'http://localhost:8080'); final body = {'key1': 'value1', 'key2': 2, 'key3': true}; final encodedBody = jsonUtf8.encode(body); - expectLater(client.decodeResponse(encodedBody), completion(equals(body))); + expectLater(client.decodeResponse(BytesResponseBody(encodedBody)), completion(equals(body))); }); test('decodeResponseWithStringBody', () { final client = NoOpRestClientBase(baseUrl: 'http://localhost:8080'); const body = '{}'; final encodedBody = utf8.encode(body); - expectLater(client.decodeResponse(encodedBody), completion(equals({}))); + expectLater(client.decodeResponse(BytesResponseBody(encodedBody)), completion(equals({}))); }); test('decodeResponseWithEmptyStringBody', () { final client = NoOpRestClientBase(baseUrl: 'http://localhost:8080'); const body = ''; final encodedBody = utf8.encode(body); - expectLater(client.decodeResponse(encodedBody), completion(equals(null))); + expectLater(client.decodeResponse(BytesResponseBody(encodedBody)), completion(equals(null))); }); test('decodeResponseWithInvalidJsonBody', () { @@ -91,7 +91,7 @@ void main() { const body = 'invalid json'; final encodedBody = utf8.encode(body); expectLater( - client.decodeResponse(encodedBody), + client.decodeResponse(BytesResponseBody(encodedBody)), throwsA(isA()), ); }); @@ -101,7 +101,7 @@ void main() { const body = 'invalid json'; final encodedBody = utf8.encode(body); expectLater( - client.decodeResponse(encodedBody), + client.decodeResponse(BytesResponseBody(encodedBody)), throwsA(isA()), ); }); @@ -113,7 +113,7 @@ void main() { }; final encodedBody = jsonUtf8.encode(body); expectLater( - client.decodeResponse(encodedBody), + client.decodeResponse(BytesResponseBody(encodedBody)), throwsA(isA()), ); }); @@ -125,7 +125,7 @@ void main() { }; final encodedBody = jsonUtf8.encode(body); expectLater( - client.decodeResponse(encodedBody), + client.decodeResponse(BytesResponseBody(encodedBody)), completion(equals(body['data'])), ); });