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