onemoney_protocol/client/
builder.rs1use 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
14pub 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 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 pub fn network(mut self, network: Network) -> Self {
48 self.network = Some(network);
49 self
50 }
51
52 pub fn base_url<S: Into<String>>(mut self, url: S) -> Self {
54 self.base_url = Some(url.into());
55 self
56 }
57
58 pub fn timeout(mut self, timeout: Duration) -> Self {
60 self.timeout = Some(timeout);
61 self
62 }
63
64 pub fn http_client(mut self, client: HttpClient) -> Self {
66 self.http_client = Some(client);
67 self
68 }
69
70 pub fn hook<H: Hook + 'static>(mut self, hook: H) -> Self {
72 self.hooks.push(Box::new(hook));
73 self
74 }
75
76 pub fn build(self) -> Result<Client> {
78 let network = self.network.unwrap_or_default();
80
81 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 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 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 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 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 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 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 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 let builder = ClientBuilder::new()
285 .network(Network::Mainnet)
286 .network(Network::Testnet) .timeout(Duration::from_secs(10))
288 .timeout(Duration::from_secs(20)); 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 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 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 let client2 = ClientBuilder::new()
319 .timeout(Duration::from_secs(u64::MAX / 1000)) .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}