onemoney_protocol/client/
builder.rs

1//! Client builder for configuration and creation.
2
3use super::{
4    config::{DEFAULT_TIMEOUT, Network},
5    hooks::Hook,
6    http::Client,
7};
8use crate::Result;
9use reqwest::Client as HttpClient;
10use std::fmt::{Debug, Formatter, Result as FmtResult};
11use std::time::Duration;
12use url::Url;
13
14/// Builder for configuring and creating clients.
15pub struct ClientBuilder {
16    network: Option<Network>,
17    base_url: Option<String>,
18    timeout: Option<Duration>,
19    http_client: Option<HttpClient>,
20    hooks: Vec<Box<dyn Hook>>,
21}
22
23impl Debug for ClientBuilder {
24    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
25        f.debug_struct("ClientBuilder")
26            .field("network", &self.network)
27            .field("base_url", &self.base_url)
28            .field("timeout", &self.timeout)
29            .field("hooks_count", &self.hooks.len())
30            .finish()
31    }
32}
33
34impl ClientBuilder {
35    /// Create a new client builder.
36    pub fn new() -> Self {
37        Self {
38            network: None,
39            base_url: None,
40            timeout: None,
41            http_client: None,
42            hooks: Vec::new(),
43        }
44    }
45
46    /// Set the network environment.
47    pub fn network(mut self, network: Network) -> Self {
48        self.network = Some(network);
49        self
50    }
51
52    /// Set the base URL.
53    pub fn base_url<S: Into<String>>(mut self, url: S) -> Self {
54        self.base_url = Some(url.into());
55        self
56    }
57
58    /// Set the request timeout.
59    pub fn timeout(mut self, timeout: Duration) -> Self {
60        self.timeout = Some(timeout);
61        self
62    }
63
64    /// Set a custom HTTP client.
65    pub fn http_client(mut self, client: HttpClient) -> Self {
66        self.http_client = Some(client);
67        self
68    }
69
70    /// Add a hook for request/response middleware.
71    pub fn hook<H: Hook + 'static>(mut self, hook: H) -> Self {
72        self.hooks.push(Box::new(hook));
73        self
74    }
75
76    /// Build the client.
77    pub fn build(self) -> Result<Client> {
78        // Determine the network - priority: network > default (mainnet)
79        let network = self.network.unwrap_or_default();
80
81        // Priority: base_url > network URL
82        let base_url = if let Some(url) = self.base_url {
83            url
84        } else {
85            network.url().to_string()
86        };
87        let base_url = Url::parse(&base_url)?;
88
89        let http_client = if let Some(client) = self.http_client {
90            client
91        } else {
92            let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
93            reqwest::Client::builder()
94                .timeout(timeout)
95                .user_agent("onemoney-rust-sdk/0.3.0")
96                .build()?
97        };
98
99        Ok(Client::new(base_url, network, http_client, self.hooks))
100    }
101}
102
103impl Default for ClientBuilder {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::client::config::{LOCAL_URL, MAINNET_URL, TESTNET_URL};
113    use std::time::Duration;
114
115    #[test]
116    fn test_builder_default_configuration() {
117        let builder = ClientBuilder::new();
118
119        // Verify default state
120        assert!(builder.network.is_none());
121        assert!(builder.base_url.is_none());
122        assert!(builder.timeout.is_none());
123        assert!(builder.http_client.is_none());
124        assert!(builder.hooks.is_empty());
125
126        // Test that default can build successfully
127        let client = builder.build();
128        assert!(client.is_ok(), "Default builder should create valid client");
129    }
130
131    #[test]
132    fn test_builder_network_configuration() {
133        // Test all network types
134        let networks = [
135            (Network::Mainnet, MAINNET_URL),
136            (Network::Testnet, TESTNET_URL),
137            (Network::Local, LOCAL_URL),
138        ];
139
140        for (network, _expected_url) in networks {
141            let builder = ClientBuilder::new().network(network);
142
143            assert_eq!(builder.network, Some(network));
144
145            let client = builder.build().expect("Network configuration should work");
146            let debug_str = format!("{:?}", client);
147            assert!(debug_str.contains("base_url"));
148        }
149    }
150
151    #[test]
152    fn test_builder_timeout_configuration() {
153        let test_timeouts = [
154            Duration::from_millis(1),
155            Duration::from_secs(5),
156            Duration::from_secs(30),
157            Duration::from_secs(120),
158            Duration::from_secs(3600),
159        ];
160
161        for timeout in test_timeouts {
162            let builder = ClientBuilder::new().timeout(timeout);
163
164            assert_eq!(builder.timeout, Some(timeout));
165
166            let client = builder.build();
167            assert!(
168                client.is_ok(),
169                "Timeout configuration should work for {:?}",
170                timeout
171            );
172        }
173    }
174
175    #[test]
176    fn test_builder_custom_base_url() {
177        let test_urls = [
178            "http://localhost:8080",
179            "https://api.example.com",
180            "http://127.0.0.1:3000",
181            "https://custom.domain.com:8443",
182        ];
183
184        for url in test_urls {
185            let builder = ClientBuilder::new().base_url(url);
186
187            assert_eq!(builder.base_url, Some(url.to_string()));
188
189            let client = builder.build();
190            assert!(client.is_ok(), "Custom base URL should work for {}", url);
191        }
192    }
193
194    #[test]
195    fn test_builder_url_priority() {
196        // Custom base_url should override network setting
197        let builder = ClientBuilder::new()
198            .network(Network::Mainnet)
199            .base_url("http://custom.example.com");
200
201        let client = builder.build().expect("URL priority test should work");
202        let debug_str = format!("{:?}", client);
203        assert!(debug_str.contains("base_url"));
204    }
205
206    #[test]
207    fn test_builder_http_client_configuration() {
208        let custom_client = reqwest::Client::builder()
209            .timeout(Duration::from_secs(10))
210            .build()
211            .expect("Custom HTTP client should build");
212
213        let builder = ClientBuilder::new().http_client(custom_client);
214
215        assert!(builder.http_client.is_some());
216
217        let client = builder.build();
218        assert!(
219            client.is_ok(),
220            "Custom HTTP client configuration should work"
221        );
222    }
223
224    #[test]
225    fn test_builder_hooks_management() {
226        // Create a test hook
227        struct TestHook;
228        impl Hook for TestHook {
229            fn before_request(&self, _method: &str, _url: &str, _body: Option<&str>) {}
230            fn after_response(&self, _method: &str, _url: &str, _status: u16, _body: Option<&str>) {
231            }
232        }
233
234        let builder = ClientBuilder::new().hook(TestHook).hook(TestHook);
235
236        assert_eq!(builder.hooks.len(), 2);
237
238        let client = builder.build();
239        assert!(client.is_ok(), "Hook management should work");
240    }
241
242    #[test]
243    fn test_builder_validation_errors() {
244        // Test invalid URL
245        let result = ClientBuilder::new().base_url("invalid-url-format").build();
246
247        assert!(result.is_err(), "Invalid URL should cause build error");
248    }
249
250    #[test]
251    fn test_builder_debug_implementation() {
252        let builder = ClientBuilder::new()
253            .network(Network::Testnet)
254            .base_url("http://example.com")
255            .timeout(Duration::from_secs(30));
256
257        let debug_str = format!("{:?}", builder);
258
259        assert!(debug_str.contains("ClientBuilder"));
260        assert!(debug_str.contains("network"));
261        assert!(debug_str.contains("base_url"));
262        assert!(debug_str.contains("timeout"));
263        assert!(debug_str.contains("hooks_count"));
264        assert!(debug_str.contains("Testnet"));
265        assert!(debug_str.contains("example.com"));
266        assert!(debug_str.contains("30s"));
267    }
268
269    #[test]
270    fn test_builder_method_chaining() {
271        // Test that all methods return Self for chaining
272        let client = ClientBuilder::new()
273            .network(Network::Local)
274            .base_url("http://localhost:8080")
275            .timeout(Duration::from_secs(15))
276            .build();
277
278        assert!(client.is_ok(), "Method chaining should work correctly");
279    }
280
281    #[test]
282    fn test_builder_multiple_configurations() {
283        // Test overwriting previous configurations
284        let builder = ClientBuilder::new()
285            .network(Network::Mainnet)
286            .network(Network::Testnet)  // Should override previous
287            .timeout(Duration::from_secs(10))
288            .timeout(Duration::from_secs(20)); // Should override previous
289
290        assert_eq!(builder.network, Some(Network::Testnet));
291        assert_eq!(builder.timeout, Some(Duration::from_secs(20)));
292
293        let client = builder.build();
294        assert!(client.is_ok(), "Multiple configurations should work");
295    }
296
297    #[test]
298    fn test_builder_default_trait() {
299        let builder1 = ClientBuilder::default();
300        let builder2 = ClientBuilder::new();
301
302        // Both should have same initial state
303        assert_eq!(builder1.network, builder2.network);
304        assert_eq!(builder1.base_url, builder2.base_url);
305        assert_eq!(builder1.timeout, builder2.timeout);
306        assert_eq!(builder1.hooks.len(), builder2.hooks.len());
307    }
308
309    #[test]
310    fn test_builder_extreme_timeout_values() {
311        // Test with very small timeout
312        let client1 = ClientBuilder::new()
313            .timeout(Duration::from_nanos(1))
314            .build();
315        assert!(client1.is_ok(), "Very small timeout should be accepted");
316
317        // Test with very large timeout
318        let client2 = ClientBuilder::new()
319            .timeout(Duration::from_secs(u64::MAX / 1000)) // Avoid overflow
320            .build();
321        assert!(client2.is_ok(), "Very large timeout should be accepted");
322    }
323
324    #[test]
325    fn test_builder_edge_case_urls() {
326        let edge_case_urls = [
327            "http://localhost",
328            "https://a.b",
329            "http://127.0.0.1",
330            "https://example.com:443",
331        ];
332
333        for url in edge_case_urls {
334            let client = ClientBuilder::new().base_url(url).build();
335            assert!(client.is_ok(), "Edge case URL {} should work", url);
336        }
337    }
338}