Skip to main content

onemoney_protocol/client/
http.rs

1//! HTTP client implementation.
2
3use 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
13/// OneMoney API client.
14pub 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    /// Create a new client for mainnet.
33    pub fn mainnet() -> Result<Self> {
34        ClientBuilder::new().network(Network::Mainnet).build()
35    }
36
37    /// Create a new client for testnet.
38    pub fn testnet() -> Result<Self> {
39        ClientBuilder::new().network(Network::Testnet).build()
40    }
41
42    /// Create a new client for local development.
43    pub fn local() -> Result<Self> {
44        ClientBuilder::new().network(Network::Local).build()
45    }
46
47    /// Create a new client for custom network.
48    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    /// Create a new client instance.
57    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    /// Perform a GET request.
67    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        // Execute hooks
75        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        // Execute hooks
85        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    /// Perform a POST request.
98    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        // Execute hooks
109        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        // Execute hooks
125        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    /// Handle error responses from the API.
138    fn handle_error_response(&self, status_code: u16, body: &str) -> Error {
139        // Try to parse as structured error response first (L1 compatible)
140        if let Ok(error_response) = serde_json::from_str::<ErrorResponse>(body) {
141            // Classify error based on status code and error code
142            Self::classify_error(status_code, &error_response.error_code, &error_response.message)
143        } else {
144            // Fallback based on status code
145            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    /// Classify structured errors based on L1 error patterns.
160    fn classify_error(status_code: u16, error_code: &str, message: &str) -> Error {
161        match (status_code, error_code) {
162            // 400 Bad Request - Validation Errors
163            (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 Unauthorized
169            (401, _) => Error::authentication(message),
170
171            // 403 Forbidden
172            (403, _) => Error::authorization(message),
173
174            // 404 Not Found - Resource Errors
175            (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
181            (408, "request_timeout") => Error::request_timeout(message, 0),
182
183            // 422 Unprocessable Entity - Business Logic
184            (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 Too Many Requests
190            (429, "rate_limit_exceeded") => Error::rate_limit_exceeded(None),
191
192            // 500+ Server Errors
193            (500..=599, code) if code.starts_with("system_") => Error::http_transport(message, Some(status_code)),
194
195            // Default to generic API error
196            _ => Error::api(status_code, error_code.to_string(), message.to_string()),
197        }
198    }
199
200    /// Test helper method to expose handle_error_response for comprehensive
201    /// testing.
202    ///
203    /// **This method is intended only for testing and should not be used in
204    /// production code.**
205    #[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    /// Test helper method to expose classify_error for comprehensive testing.
211    ///
212    /// **This method is intended only for testing and should not be used in
213    /// production code.**
214    #[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        // Test mainnet client creation
242        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        // Test testnet client creation
248        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        // Test local client creation
254        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")); // Default hooks count
269    }
270
271    #[test]
272    fn test_error_classification_validation_errors() {
273        // Test validation error classification
274        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        // Test unknown error code
347        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        // Test unexpected status code
351        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        // Test structured error response parsing
360        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        // Test structured business logic error
365        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        // Test fallback to status code classification when JSON parsing fails
376        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        // Test that different networks use correct base URLs
409        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}