Skip to main content

onemoney_protocol/client/
builder.rs

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