1use super::{builder::ClientBuilder, config::Network, hooks::Hook};
4use crate::{Error, Result, error::ErrorResponse};
5use reqwest::{Client as HttpClient, header};
6use serde::{Serialize, de::DeserializeOwned};
7use serde_json;
8use std::fmt::{Debug, Formatter, Result as FmtResult};
9use url::Url;
10
11pub struct Client {
13 pub(crate) base_url: Url,
14 http_client: HttpClient,
15 hooks: Vec<Box<dyn Hook>>,
16}
17
18impl Debug for Client {
19 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
20 f.debug_struct("Client")
21 .field("base_url", &self.base_url)
22 .field("hooks_count", &self.hooks.len())
23 .finish()
24 }
25}
26
27impl Client {
28 pub fn mainnet() -> Result<Self> {
30 ClientBuilder::new().network(Network::Mainnet).build()
31 }
32
33 pub fn testnet() -> Result<Self> {
35 ClientBuilder::new().network(Network::Testnet).build()
36 }
37
38 pub fn local() -> Result<Self> {
40 ClientBuilder::new().network(Network::Local).build()
41 }
42
43 pub(crate) fn new(base_url: Url, http_client: HttpClient, hooks: Vec<Box<dyn Hook>>) -> Self {
45 Self {
46 base_url,
47 http_client,
48 hooks,
49 }
50 }
51
52 pub async fn get<T>(&self, path: &str) -> Result<T>
54 where
55 T: DeserializeOwned,
56 {
57 let url = self.base_url.join(path)?;
58 let url_str = url.as_str().to_string();
59
60 for hook in &self.hooks {
62 hook.before_request("GET", &url_str, None);
63 }
64
65 let response = self.http_client.get(url).send().await?;
66 let status = response.status();
67
68 let response_text = response.text().await?;
69
70 for hook in &self.hooks {
72 hook.after_response("GET", &url_str, status.as_u16(), Some(&response_text));
73 }
74
75 if !status.is_success() {
76 return Err(self.handle_error_response(status.as_u16(), &response_text));
77 }
78
79 let result: T = serde_json::from_str(&response_text)?;
80 Ok(result)
81 }
82
83 pub async fn post<B, T>(&self, path: &str, body: &B) -> Result<T>
85 where
86 B: Serialize,
87 T: DeserializeOwned,
88 {
89 let url = self.base_url.join(path)?;
90 let url_str = url.as_str().to_string();
91
92 let body_json = serde_json::to_string(body)?;
93
94 for hook in &self.hooks {
96 hook.before_request("POST", &url_str, Some(&body_json));
97 }
98
99 let response = self
100 .http_client
101 .post(url)
102 .header(header::CONTENT_TYPE, "application/json")
103 .body(body_json)
104 .send()
105 .await?;
106
107 let status = response.status();
108 let response_text = response.text().await?;
109
110 for hook in &self.hooks {
112 hook.after_response("POST", &url_str, status.as_u16(), Some(&response_text));
113 }
114
115 if !status.is_success() {
116 return Err(self.handle_error_response(status.as_u16(), &response_text));
117 }
118
119 let result: T = serde_json::from_str(&response_text)?;
120 Ok(result)
121 }
122
123 fn handle_error_response(&self, status_code: u16, body: &str) -> Error {
125 if let Ok(error_response) = serde_json::from_str::<ErrorResponse>(body) {
127 Self::classify_error(
129 status_code,
130 &error_response.error_code,
131 &error_response.message,
132 )
133 } else {
134 match status_code {
136 400 => Error::invalid_parameter("request", body),
137 401 => Error::authentication(body),
138 403 => Error::authorization(body),
139 404 => Error::resource_not_found("unknown", body),
140 408 => Error::request_timeout("unknown", 0),
141 422 => Error::business_logic("validation", body),
142 429 => Error::rate_limit_exceeded(None),
143 500..=599 => Error::http_transport(body, Some(status_code)),
144 _ => Error::api(status_code, "unknown".to_string(), body.to_string()),
145 }
146 }
147 }
148
149 fn classify_error(status_code: u16, error_code: &str, message: &str) -> Error {
151 match (status_code, error_code) {
152 (400, code) if code.starts_with("validation_") => {
154 let param = code.strip_prefix("validation_").unwrap_or("unknown");
155 Error::invalid_parameter(param, message)
156 }
157
158 (401, _) => Error::authentication(message),
160
161 (403, _) => Error::authorization(message),
163
164 (404, code) if code.starts_with("resource_") => {
166 let resource = code.strip_prefix("resource_").unwrap_or("unknown");
167 Error::resource_not_found(resource, message)
168 }
169
170 (408, "request_timeout") => Error::request_timeout(message, 0),
172
173 (422, code) if code.starts_with("business_") => {
175 let operation = code.strip_prefix("business_").unwrap_or("unknown");
176 Error::business_logic(operation, message)
177 }
178
179 (429, "rate_limit_exceeded") => Error::rate_limit_exceeded(None),
181
182 (500..=599, code) if code.starts_with("system_") => {
184 Error::http_transport(message, Some(status_code))
185 }
186
187 _ => Error::api(status_code, error_code.to_string(), message.to_string()),
189 }
190 }
191
192 #[doc(hidden)]
196 pub fn test_handle_error_response(&self, status_code: u16, body: &str) -> Error {
197 self.handle_error_response(status_code, body)
198 }
199
200 #[doc(hidden)]
204 pub fn test_classify_error(status_code: u16, error_code: &str, message: &str) -> Error {
205 Self::classify_error(status_code, error_code, message)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use serde::{Deserialize, Serialize};
213
214 #[derive(Serialize, Deserialize, PartialEq, Debug)]
215 struct TestResponse {
216 id: u64,
217 message: String,
218 }
219
220 #[derive(Serialize)]
221 #[allow(dead_code)]
222 struct TestRequest {
223 data: String,
224 }
225
226 #[test]
227 fn test_client_creation_methods() {
228 let mainnet_client = Client::mainnet();
230 assert!(mainnet_client.is_ok());
231 let client = mainnet_client.unwrap();
232 assert!(client.base_url.as_str().contains("mainnet"));
233
234 let testnet_client = Client::testnet();
236 assert!(testnet_client.is_ok());
237 let client = testnet_client.unwrap();
238 assert!(client.base_url.as_str().contains("testnet"));
239
240 let local_client = Client::local();
242 assert!(local_client.is_ok());
243 let client = local_client.unwrap();
244 assert!(client.base_url.as_str().contains("127.0.0.1"));
245 }
246
247 #[test]
248 fn test_client_debug_implementation() {
249 let client = Client::mainnet().expect("Failed to create mainnet client");
250 let debug_str = format!("{:?}", client);
251
252 assert!(debug_str.contains("Client"));
253 assert!(debug_str.contains("base_url"));
254 assert!(debug_str.contains("hooks_count"));
255 assert!(debug_str.contains("0")); }
257
258 #[test]
259 fn test_error_classification_validation_errors() {
260 let error =
262 Client::test_classify_error(400, "validation_address", "Invalid address format");
263 assert!(
264 matches!(error, Error::InvalidParameter { parameter, .. } if parameter == "address")
265 );
266
267 let error = Client::test_classify_error(400, "validation_amount", "Invalid amount");
268 assert!(
269 matches!(error, Error::InvalidParameter { parameter, .. } if parameter == "amount")
270 );
271
272 let error =
273 Client::test_classify_error(400, "validation_unknown", "Unknown validation error");
274 assert!(
275 matches!(error, Error::InvalidParameter { parameter, .. } if parameter == "unknown")
276 );
277 }
278
279 #[test]
280 fn test_error_classification_authentication_errors() {
281 let error =
282 Client::test_classify_error(401, "invalid_signature", "Signature verification failed");
283 assert!(matches!(error, Error::Authentication { .. }));
284
285 let error = Client::test_classify_error(401, "expired_token", "Token has expired");
286 assert!(matches!(error, Error::Authentication { .. }));
287 }
288
289 #[test]
290 fn test_error_classification_authorization_errors() {
291 let error = Client::test_classify_error(403, "insufficient_permissions", "Access denied");
292 assert!(matches!(error, Error::Authorization { .. }));
293
294 let error =
295 Client::test_classify_error(403, "forbidden_resource", "Resource access forbidden");
296 assert!(matches!(error, Error::Authorization { .. }));
297 }
298
299 #[test]
300 fn test_error_classification_resource_not_found_errors() {
301 let error =
302 Client::test_classify_error(404, "resource_transaction", "Transaction not found");
303 assert!(
304 matches!(error, Error::ResourceNotFound { resource_type, .. } if resource_type == "transaction")
305 );
306
307 let error = Client::test_classify_error(404, "resource_account", "Account not found");
308 assert!(
309 matches!(error, Error::ResourceNotFound { resource_type, .. } if resource_type == "account")
310 );
311
312 let error = Client::test_classify_error(404, "resource_unknown", "Resource not found");
313 assert!(
314 matches!(error, Error::ResourceNotFound { resource_type, .. } if resource_type == "unknown")
315 );
316 }
317
318 #[test]
319 fn test_error_classification_timeout_errors() {
320 let error = Client::test_classify_error(408, "request_timeout", "Request timed out");
321 assert!(matches!(error, Error::RequestTimeout { .. }));
322 }
323
324 #[test]
325 fn test_error_classification_business_logic_errors() {
326 let error =
327 Client::test_classify_error(422, "business_insufficient_funds", "Insufficient balance");
328 assert!(
329 matches!(error, Error::BusinessLogic { operation, .. } if operation == "insufficient_funds")
330 );
331
332 let error = Client::test_classify_error(422, "business_token_paused", "Token is paused");
333 assert!(
334 matches!(error, Error::BusinessLogic { operation, .. } if operation == "token_paused")
335 );
336 }
337
338 #[test]
339 fn test_error_classification_rate_limit_errors() {
340 let error = Client::test_classify_error(429, "rate_limit_exceeded", "Too many requests");
341 assert!(matches!(error, Error::RateLimitExceeded { .. }));
342 }
343
344 #[test]
345 fn test_error_classification_server_errors() {
346 let error =
347 Client::test_classify_error(500, "system_database_error", "Database connection failed");
348 assert!(matches!(error, Error::HttpTransport { .. }));
349
350 let error = Client::test_classify_error(
351 503,
352 "system_service_unavailable",
353 "Service temporarily unavailable",
354 );
355 assert!(matches!(error, Error::HttpTransport { .. }));
356 }
357
358 #[test]
359 fn test_error_classification_generic_api_errors() {
360 let error = Client::test_classify_error(400, "unknown_error", "Unknown error occurred");
362 assert!(
363 matches!(error, Error::Api { status_code: 400, error_code, .. } if error_code == "unknown_error")
364 );
365
366 let error = Client::test_classify_error(418, "teapot", "I'm a teapot");
368 assert!(
369 matches!(error, Error::Api { status_code: 418, error_code, .. } if error_code == "teapot")
370 );
371 }
372
373 #[test]
374 fn test_handle_error_response_with_structured_json() {
375 let client = Client::mainnet().expect("Failed to create client");
376
377 let structured_error =
379 r#"{"error_code": "validation_address", "message": "Invalid address format"}"#;
380 let error = client.test_handle_error_response(400, structured_error);
381 assert!(
382 matches!(error, Error::InvalidParameter { parameter, .. } if parameter == "address")
383 );
384
385 let business_error = r#"{"error_code": "business_insufficient_funds", "message": "Insufficient balance for transaction"}"#;
387 let error = client.test_handle_error_response(422, business_error);
388 assert!(
389 matches!(error, Error::BusinessLogic { operation, .. } if operation == "insufficient_funds")
390 );
391 }
392
393 #[test]
394 fn test_handle_error_response_fallback_to_status_code() {
395 let client = Client::mainnet().expect("Failed to create client");
396
397 let invalid_json = "Not a JSON response";
399
400 let error = client.test_handle_error_response(400, invalid_json);
401 assert!(matches!(error, Error::InvalidParameter { .. }));
402
403 let error = client.test_handle_error_response(401, invalid_json);
404 assert!(matches!(error, Error::Authentication { .. }));
405
406 let error = client.test_handle_error_response(403, invalid_json);
407 assert!(matches!(error, Error::Authorization { .. }));
408
409 let error = client.test_handle_error_response(404, invalid_json);
410 assert!(matches!(error, Error::ResourceNotFound { .. }));
411
412 let error = client.test_handle_error_response(408, invalid_json);
413 assert!(matches!(error, Error::RequestTimeout { .. }));
414
415 let error = client.test_handle_error_response(422, invalid_json);
416 assert!(matches!(error, Error::BusinessLogic { .. }));
417
418 let error = client.test_handle_error_response(429, invalid_json);
419 assert!(matches!(error, Error::RateLimitExceeded { .. }));
420
421 let error = client.test_handle_error_response(500, invalid_json);
422 assert!(matches!(error, Error::HttpTransport { .. }));
423
424 let error = client.test_handle_error_response(418, invalid_json);
425 assert!(matches!(
426 error,
427 Error::Api {
428 status_code: 418,
429 ..
430 }
431 ));
432 }
433
434 #[test]
435 fn test_network_url_configuration() {
436 let mainnet = Client::mainnet().unwrap();
438 assert!(mainnet.base_url.as_str().contains("mainnet.1money.network"));
439
440 let testnet = Client::testnet().unwrap();
441 assert!(testnet.base_url.as_str().contains("testnet.1money.network"));
442
443 let local = Client::local().unwrap();
444 assert!(local.base_url.as_str().contains("127.0.0.1:18555"));
445 }
446
447 #[test]
448 fn test_client_new_method() {
449 use reqwest::Client as HttpClient;
450 use url::Url;
451
452 let base_url = Url::parse("https://test.example.com").expect("Valid URL");
453 let http_client = HttpClient::new();
454 let hooks: Vec<Box<dyn Hook>> = vec![];
455
456 let client = Client::new(base_url.clone(), http_client, hooks);
457
458 assert_eq!(client.base_url, base_url);
459 assert_eq!(client.hooks.len(), 0);
460 }
461}