onemoney_protocol/client/
builder.rs1use 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
19pub 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 pub fn new() -> Self {
40 Self {
41 network: None,
42 timeout: None,
43 http_client: None,
44 hooks: Vec::new(),
45 }
46 }
47
48 pub fn network(mut self, network: Network) -> Self {
50 self.network = Some(network);
51 self
52 }
53
54 pub fn timeout(mut self, timeout: Duration) -> Self {
56 self.timeout = Some(timeout);
57 self
58 }
59
60 pub fn http_client(mut self, client: HttpClient) -> Self {
62 self.http_client = Some(client);
63 self
64 }
65
66 pub fn hook<H: Hook + 'static>(mut self, hook: H) -> Self {
68 self.hooks.push(Box::new(hook));
69 self
70 }
71
72 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 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 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 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 let builder = ClientBuilder::new()
238 .network(Network::Mainnet)
239 .network(Network::Testnet) .timeout(Duration::from_secs(10))
241 .timeout(Duration::from_secs(20)); 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 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 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 let client2 = ClientBuilder::new()
272 .network(Network::Mainnet)
273 .timeout(Duration::from_secs(u64::MAX / 1000)) .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}