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 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 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 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 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 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 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 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 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 let builder = ClientBuilder::new()
284 .network(Network::Mainnet)
285 .network(Network::Testnet) .timeout(Duration::from_secs(10))
287 .timeout(Duration::from_secs(20)); 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 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 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 let client2 = ClientBuilder::new()
318 .timeout(Duration::from_secs(u64::MAX / 1000)) .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}