Line data Source code
1 : import 'dart:convert';
2 : import 'dart:developer';
3 :
4 : import 'package:http/http.dart';
5 : import 'package:flutter/foundation.dart';
6 :
7 : import 'aro_result.dart';
8 : import 'mdl_api_success.dart';
9 :
10 : import '../errors/dts_http_failure.dart';
11 :
12 : import '../../app/config/constans/app_env.dart';
13 : import '../../app/config/constans/app_enums.dart';
14 : import '../../app/data/datasources/local/dts_user_pref.dart';
15 :
16 : /// A service class for handling API requests with enhanced logging and error handling.
17 : ///
18 : /// The `ApiCall` class facilitates communication with external APIs by wrapping
19 : /// HTTP requests with configurable parameters, timeout settings, and structured
20 : /// logging. It supports both GET and POST methods and can attach authorization headers.
21 : ///
22 : /// ## Example
23 : ///
24 : /// ```dart
25 : /// void main() async {
26 : /// final client = Client();
27 : /// final env = 'https://api.example.com';
28 : /// final userPrefs = UserPref();
29 : /// await userPrefs.initPrefs();
30 : ///
31 : /// final apiService = ApiCall(client, env, userPrefs);
32 : /// final result = await apiService.request(
33 : /// endpoint: '/login',
34 : /// bearer: 'your-auth-token',
35 : /// body: jsonEncode({'username': 'user', 'password': 'pass'}),
36 : /// );
37 : ///
38 : /// result.when(
39 : /// success: (data) => print('Success: ${data.data}'),
40 : /// failure: (error) => print('Error: ${error.message}'),
41 : /// );
42 : /// }
43 : /// ```
44 : ///
45 : /// ## Properties
46 : ///
47 : /// - `_client` (`Client`): The HTTP client used for making API requests.
48 : /// - `_env` (`String`): The base URL or environment for API endpoints.
49 : /// - `_userPrefs` (`UserPref`): Instance of `UserPref` for storing request logs.
50 : ///
51 : /// ### `Future<ApiWrapper<ApiFailure, ApiSuccess>> request({...})`
52 : /// Makes an API request with the provided parameters.
53 : ///
54 : /// #### Parameters:
55 : /// - `body` (`String`): The request body in JSON format.
56 : /// - `bearer` (`String`): Bearer token for authorization (optional).
57 : /// - `endpoint` (`String`): The endpoint to append to the base URL.
58 : /// - `epName` (`String`): An optional identifier for the endpoint (default: `-|`).
59 : /// - `method` (`HttpMethod`): The HTTP method, defaulting to POST.
60 : /// - `headers` (`Map<String, String>`): Additional headers (default: content type JSON).
61 : /// - `queryParameters` (`Map<String, String>`): Query parameters (default: empty).
62 : /// - `tkMovil` (`String`): Mobile token (optional).
63 : ///
64 : /// #### Returns:
65 : /// - `ApiWrapper.success`: Contains `ApiSuccess` with the status code and response data.
66 : /// - `ApiWrapper.failure`: Contains `ApiFailure` with the error code and message.
67 : ///
68 : /// #### Example:
69 : /// ```dart
70 : /// final response = await apiService.request(
71 : /// endpoint: '/user/profile',
72 : /// bearer: 'auth-token',
73 : /// method: HttpMethod.get,
74 : /// );
75 : ///
76 : /// response.when(
77 : /// success: (success) {
78 : /// print('Response data: ${success.data}');
79 : /// },
80 : /// failure: (failure) {
81 : /// print('Error: ${failure.message}');
82 : /// },
83 : /// );
84 : /// ```
85 : class ApiCall {
86 1 : ApiCall(this._client, this._env, this._userPrefs);
87 :
88 : final Client _client;
89 : final String _env;
90 : final UserPref _userPrefs;
91 :
92 1 : Future<ARoResult<ApiFailure, ApiSuccess>> request({
93 : required String endpoint,
94 : required String body,
95 : String epName = '-|',
96 : HttpMethod method = HttpMethod.get,
97 : Map<String, String> headers = const {
98 : 'Content-Type': 'application/json',
99 : 'Accept': 'application/json',
100 : 'charset': 'UTF-8',
101 : },
102 : Map<String, String> queryParameters = const {},
103 : }) async {
104 : /// Makes an HTTP request to the specified endpoint with configurable options.
105 : ///
106 : /// Handles logging, timeout, error handling, and response decoding. The result
107 : /// is returned as an `ApiWrapper`, which is a wrapper for success and failure states.
108 :
109 : const timeout = Duration(seconds: 15);
110 :
111 2 : String uri = getBaseUri(_env, endpoint);
112 1 : DateTime startTime = DateTime.now();
113 : String resApiWithChartSet = '';
114 2 : Map<String, dynamic> logs = {'startTime': startTime.toString()};
115 : StackTrace? stackTrace;
116 :
117 1 : Response petitionResponse = Response('', 200);
118 :
119 : try {
120 1 : Uri url = Uri.parse(uri);
121 2 : logs = {'url': '$url'};
122 :
123 1 : if (queryParameters.isNotEmpty) {
124 1 : url = url.replace(queryParameters: queryParameters);
125 : }
126 :
127 2 : logs = {...logs, 'queryParameters': queryParameters};
128 :
129 2 : headers = {...headers, 'Authorization': 'Bearer $apiKey'};
130 :
131 2 : logs = {...logs, 'headers': headers};
132 :
133 : switch (method) {
134 1 : case HttpMethod.get:
135 1 : petitionResponse = await _client
136 1 : .get(url, headers: headers)
137 1 : .timeout(timeout);
138 : break;
139 :
140 1 : case HttpMethod.post:
141 1 : petitionResponse = await _client
142 1 : .post(url, headers: headers, body: body)
143 1 : .timeout(timeout);
144 : break;
145 :
146 : default:
147 1 : petitionResponse = await _client
148 1 : .get(url, headers: headers)
149 1 : .timeout(timeout);
150 : break;
151 : }
152 :
153 1 : logs = {
154 : ...logs,
155 3 : 'body': (body == 'mock') ? body : jsonDecode(body),
156 1 : 'Response::':
157 : '**************************************************************',
158 2 : 'Response code:': petitionResponse.statusCode,
159 : };
160 :
161 : /// Change charSet UTF-8
162 2 : resApiWithChartSet = utf8.decode(petitionResponse.bodyBytes);
163 :
164 3 : logs = {...logs, 'Response body': jsonDecode(resApiWithChartSet)};
165 :
166 2 : if (petitionResponse.statusCode == 200) {
167 : //. Success root request
168 1 : return ARoResult.success(
169 1 : ApiSuccess(
170 1 : code: petitionResponse.statusCode,
171 : data: resApiWithChartSet,
172 : ),
173 : );
174 : } else {
175 : //. Failure root request
176 1 : ApiFailure resFailure = ApiFailure.fromJson(resApiWithChartSet);
177 1 : return ARoResult.failure(
178 1 : ApiFailure(
179 1 : code: petitionResponse.statusCode,
180 1 : message: resFailure.message,
181 : ),
182 : );
183 : }
184 : } catch (e, s) {
185 : stackTrace = s;
186 3 : logs = {...logs, 'error': e.toString()};
187 :
188 3 : if (e is ClientException || e.toString().contains('SocketException')) {
189 1 : return ARoResult.failure(
190 3 : ApiFailure(code: petitionResponse.statusCode, message: e.toString()),
191 : );
192 : } else {
193 1 : return ARoResult.failure(
194 3 : ApiFailure(code: e.hashCode, message: e.toString()),
195 : );
196 : }
197 :
198 : ///
199 : } finally {
200 1 : DateTime endTime = DateTime.now();
201 2 : String timeElapsed = endTime.difference(startTime).toString();
202 1 : logs = {
203 : ...logs,
204 3 : 'endTime': DateTime.now().toString(),
205 1 : 'TimeElapsed': timeElapsed,
206 : };
207 :
208 : /// Only for purpose QA
209 : // coverage:ignore-start
210 :
211 : List<String> oldData = _userPrefs.resHttp;
212 :
213 : if (body != 'mock') {
214 : oldData.add(
215 : '$epName~$body~$resApiWithChartSet~${startTime.toString().substring(0, 22)} -> ${endTime.toString().substring(0, 22)}~$timeElapsed~$uri~$queryParameters~${jsonEncode(headers)}',
216 : );
217 :
218 : _userPrefs.resHttp = oldData;
219 : }
220 : // coverage:ignore-end
221 :
222 : /// Delete this snippet when finish
223 : if (kDebugMode) {
224 1 : log('''
225 : $epName
226 :
227 : Endpoint ::-> $endpoint
228 : Url :: -> $uri
229 : --------------------------------------------------------------
230 1 : ${const JsonEncoder.withIndent(' ').convert(logs)}
231 : --------------------------------------------------------------
232 1 : ''', stackTrace: stackTrace);
233 : }
234 : }
235 :
236 : ///
237 : }
238 :
239 : /// Returns the base URL for the API environment based on the specified kind.
240 : ///
241 : /// This method provides the base URL for different environments (development, QA, production).
242 : /// It returns the corresponding API URL based on the provided environment kind.
243 : ///
244 : /// ***Parameters***:
245 : /// - `env` (String):
246 : /// The environment kind, which determines the base URL to return. It can be one of the following:
247 : ///
248 : /// - `'dev'` for the development environment.
249 : /// - `'qa'` for the QA environment.
250 : /// - `'prod'` for the production environment.
251 : ///
252 : /// ***Returns***:
253 : /// - [String]: The base URL corresponding to the given environment kind.
254 : /// If the kind is not recognized, an empty string is returned.
255 : ///
256 : /// ***Notes***:
257 : /// - The method uses a map to store the base URLs for different environments.
258 : /// Ensure that the URLs are up-to-date with the correct endpoints for each environment.
259 : ///
260 : /// **Example**:
261 : /// ```dart
262 : /// final uri = getBaseUri(_env);
263 : ///
264 : /// print('Base URL for dev environment: $uri');
265 : /// ```
266 1 : String getBaseUri(String env, String ep) {
267 1 : Map<String, String> map = {
268 1 : 'dev': 'https://api.themoviedb.org$ep',
269 1 : 'qa': 'https://api.themoviedb.org$ep',
270 1 : 'prod': 'https://api.themoviedb.org$ep',
271 : };
272 :
273 2 : return map[env] ?? 'https://api.themoviedb.org$ep';
274 : }
275 : }
|