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