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        // Priority: base_url > network > default (mainnet)
79        let base_url = if let Some(url) = self.base_url {
80            url
81        } else if let Some(network) = self.network {
82            network.url().to_string()
83        } else {
84            Network::default().url().to_string()
85        };
86        let base_url = Url::parse(&base_url)?;
87
88        let http_client = if let Some(client) = self.http_client {
89            client
90        } else {
91            let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
92            reqwest::Client::builder()
93                .timeout(timeout)
94                .user_agent("onemoney-rust-sdk/0.1.0")
95                .build()?
96        };
97
98        Ok(Client::new(base_url, http_client, self.hooks))
99    }
100}
101
102impl Default for ClientBuilder {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::client::config::{LOCAL_URL, MAINNET_URL, TESTNET_URL};
112    use std::time::Duration;
113
114    #[test]
115    fn test_builder_default_configuration() {
116        let builder = ClientBuilder::new();
117
118        // Verify default state
119        assert!(builder.network.is_none());
120        assert!(builder.base_url.is_none());
121        assert!(builder.timeout.is_none());
122        assert!(builder.http_client.is_none());
123        assert!(builder.hooks.is_empty());
124
125        // Test that default can build successfully
126        let client = builder.build();
127        assert!(client.is_ok(), "Default builder should create valid client");
128    }
129
130    #[test]
131    fn test_builder_network_configuration() {
132        // Test all network types
133        let networks = [
134            (Network::Mainnet, MAINNET_URL),
135            (Network::Testnet, TESTNET_URL),
136            (Network::Local, LOCAL_URL),
137        ];
138
139        for (network, _expected_url) in networks {
140            let builder = ClientBuilder::new().network(network);
141
142            assert_eq!(builder.network, Some(network));
143
144            let client = builder.build().expect("Network configuration should work");
145            let debug_str = format!("{:?}", client);
146            assert!(debug_str.contains("base_url"));
147        }
148    }
149
150    #[test]
151    fn test_builder_timeout_configuration() {
152        let test_timeouts = [
153            Duration::from_millis(1),
154            Duration::from_secs(5),
155            Duration::from_secs(30),
156            Duration::from_secs(120),
157            Duration::from_secs(3600),
158        ];
159
160        for timeout in test_timeouts {
161            let builder = ClientBuilder::new().timeout(timeout);
162
163            assert_eq!(builder.timeout, Some(timeout));
164
165            let client = builder.build();
166            assert!(
167                client.is_ok(),
168                "Timeout configuration should work for {:?}",
169                timeout
170            );
171        }
172    }
173
174    #[test]
175    fn test_builder_custom_base_url() {
176        let test_urls = [
177            "http://localhost:8080",
178            "https://api.example.com",
179            "http://127.0.0.1:3000",
180            "https://custom.domain.com:8443",
181        ];
182
183        for url in test_urls {
184            let builder = ClientBuilder::new().base_url(url);
185
186            assert_eq!(builder.base_url, Some(url.to_string()));
187
188            let client = builder.build();
189            assert!(client.is_ok(), "Custom base URL should work for {}", url);
190        }
191    }
192
193    #[test]
194    fn test_builder_url_priority() {
195        // Custom base_url should override network setting
196        let builder = ClientBuilder::new()
197            .network(Network::Mainnet)
198            .base_url("http://custom.example.com");
199
200        let client = builder.build().expect("URL priority test should work");
201        let debug_str = format!("{:?}", client);
202        assert!(debug_str.contains("base_url"));
203    }
204
205    #[test]
206    fn test_builder_http_client_configuration() {
207        let custom_client = reqwest::Client::builder()
208            .timeout(Duration::from_secs(10))
209            .build()
210            .expect("Custom HTTP client should build");
211
212        let builder = ClientBuilder::new().http_client(custom_client);
213
214        assert!(builder.http_client.is_some());
215
216        let client = builder.build();
217        assert!(
218            client.is_ok(),
219            "Custom HTTP client configuration should work"
220        );
221    }
222
223    #[test]
224    fn test_builder_hooks_management() {
225        // Create a test hook
226        struct TestHook;
227        impl Hook for TestHook {
228            fn before_request(&self, _method: &str, _url: &str, _body: Option<&str>) {}
229            fn after_response(&self, _method: &str, _url: &str, _status: u16, _body: Option<&str>) {
230            }
231        }
232
233        let builder = ClientBuilder::new().hook(TestHook).hook(TestHook);
234
235        assert_eq!(builder.hooks.len(), 2);
236
237        let client = builder.build();
238        assert!(client.is_ok(), "Hook management should work");
239    }
240
241    #[test]
242    fn test_builder_validation_errors() {
243        // Test invalid URL
244        let result = ClientBuilder::new().base_url("invalid-url-format").build();
245
246        assert!(result.is_err(), "Invalid URL should cause build error");
247    }
248
249    #[test]
250    fn test_builder_debug_implementation() {
251        let builder = ClientBuilder::new()
252            .network(Network::Testnet)
253            .base_url("http://example.com")
254            .timeout(Duration::from_secs(30));
255
256        let debug_str = format!("{:?}", builder);
257
258        assert!(debug_str.contains("ClientBuilder"));
259        assert!(debug_str.contains("network"));
260        assert!(debug_str.contains("base_url"));
261        assert!(debug_str.contains("timeout"));
262        assert!(debug_str.contains("hooks_count"));
263        assert!(debug_str.contains("Testnet"));
264        assert!(debug_str.contains("example.com"));
265        assert!(debug_str.contains("30s"));
266    }
267
268    #[test]
269    fn test_builder_method_chaining() {
270        // Test that all methods return Self for chaining
271        let client = ClientBuilder::new()
272            .network(Network::Local)
273            .base_url("http://localhost:8080")
274            .timeout(Duration::from_secs(15))
275            .build();
276
277        assert!(client.is_ok(), "Method chaining should work correctly");
278    }
279
280    #[test]
281    fn test_builder_multiple_configurations() {
282        // Test overwriting previous configurations
283        let builder = ClientBuilder::new()
284            .network(Network::Mainnet)
285            .network(Network::Testnet)  // Should override previous
286            .timeout(Duration::from_secs(10))
287            .timeout(Duration::from_secs(20)); // Should override previous
288
289        assert_eq!(builder.network, Some(Network::Testnet));
290        assert_eq!(builder.timeout, Some(Duration::from_secs(20)));
291
292        let client = builder.build();
293        assert!(client.is_ok(), "Multiple configurations should work");
294    }
295
296    #[test]
297    fn test_builder_default_trait() {
298        let builder1 = ClientBuilder::default();
299        let builder2 = ClientBuilder::new();
300
301        // Both should have same initial state
302        assert_eq!(builder1.network, builder2.network);
303        assert_eq!(builder1.base_url, builder2.base_url);
304        assert_eq!(builder1.timeout, builder2.timeout);
305        assert_eq!(builder1.hooks.len(), builder2.hooks.len());
306    }
307
308    #[test]
309    fn test_builder_extreme_timeout_values() {
310        // Test with very small timeout
311        let client1 = ClientBuilder::new()
312            .timeout(Duration::from_nanos(1))
313            .build();
314        assert!(client1.is_ok(), "Very small timeout should be accepted");
315
316        // Test with very large timeout
317        let client2 = ClientBuilder::new()
318            .timeout(Duration::from_secs(u64::MAX / 1000)) // Avoid overflow
319            .build();
320        assert!(client2.is_ok(), "Very large timeout should be accepted");
321    }
322
323    #[test]
324    fn test_builder_edge_case_urls() {
325        let edge_case_urls = [
326            "http://localhost",
327            "https://a.b",
328            "http://127.0.0.1",
329            "https://example.com:443",
330        ];
331
332        for url in edge_case_urls {
333            let client = ClientBuilder::new().base_url(url).build();
334            assert!(client.is_ok(), "Edge case URL {} should work", url);
335        }
336    }
337}