1use std::time::Duration;
9
10#[derive(Debug, Clone)]
12pub struct HttpClientConfig {
13 pub connect_timeout: Duration,
15
16 pub request_timeout: Duration,
18
19 pub user_agent: String,
21
22 pub max_retries: u32,
24
25 pub retry_strategy: RetryStrategy,
27
28 pub proxy: Option<ProxyConfig>,
30
31 pub follow_redirects: bool,
33
34 pub max_redirects: u32,
36
37 pub accept_invalid_certs: bool,
39}
40
41impl Default for HttpClientConfig {
42 fn default() -> Self {
43 Self {
44 connect_timeout: Duration::from_secs(10),
45 request_timeout: Duration::from_secs(30),
46 user_agent: format!("unistore-http/{}", env!("CARGO_PKG_VERSION")),
47 max_retries: 3,
48 retry_strategy: RetryStrategy::ExponentialBackoff {
49 initial: Duration::from_millis(100),
50 max: Duration::from_secs(5),
51 },
52 proxy: None,
53 follow_redirects: true,
54 max_redirects: 10,
55 accept_invalid_certs: false,
56 }
57 }
58}
59
60impl HttpClientConfig {
61 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn quick() -> Self {
68 Self {
69 connect_timeout: Duration::from_secs(5),
70 request_timeout: Duration::from_secs(10),
71 max_retries: 1,
72 retry_strategy: RetryStrategy::None,
73 ..Self::default()
74 }
75 }
76
77 pub fn long_running() -> Self {
79 Self {
80 connect_timeout: Duration::from_secs(30),
81 request_timeout: Duration::from_secs(300), max_retries: 5,
83 ..Self::default()
84 }
85 }
86
87 pub fn no_retry() -> Self {
89 Self {
90 max_retries: 0,
91 retry_strategy: RetryStrategy::None,
92 ..Self::default()
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub enum RetryStrategy {
100 None,
102
103 Fixed(Duration),
105
106 ExponentialBackoff {
108 initial: Duration,
110 max: Duration,
112 },
113}
114
115impl RetryStrategy {
116 pub fn delay_for_attempt(&self, attempt: u32) -> Option<Duration> {
121 match self {
122 Self::None => None,
123 Self::Fixed(duration) => Some(*duration),
124 Self::ExponentialBackoff { initial, max } => {
125 let delay = initial.saturating_mul(2u32.saturating_pow(attempt.saturating_sub(1)));
126 Some(delay.min(*max))
127 }
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct ProxyConfig {
135 pub http: Option<String>,
137
138 pub https: Option<String>,
140
141 pub no_proxy: Vec<String>,
143}
144
145impl ProxyConfig {
146 pub fn http(url: impl Into<String>) -> Self {
148 Self {
149 http: Some(url.into()),
150 https: None,
151 no_proxy: Vec::new(),
152 }
153 }
154
155 pub fn https(url: impl Into<String>) -> Self {
157 Self {
158 http: None,
159 https: Some(url.into()),
160 no_proxy: Vec::new(),
161 }
162 }
163
164 pub fn all(url: impl Into<String>) -> Self {
166 let url = url.into();
167 Self {
168 http: Some(url.clone()),
169 https: Some(url),
170 no_proxy: Vec::new(),
171 }
172 }
173
174 pub fn with_no_proxy(mut self, patterns: Vec<String>) -> Self {
176 self.no_proxy = patterns;
177 self
178 }
179}
180
181#[derive(Debug, Clone, Default)]
183pub struct HttpClientConfigBuilder {
184 config: HttpClientConfig,
185}
186
187impl HttpClientConfigBuilder {
188 pub fn new() -> Self {
190 Self::default()
191 }
192
193 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
195 self.config.connect_timeout = timeout;
196 self
197 }
198
199 pub fn request_timeout(mut self, timeout: Duration) -> Self {
201 self.config.request_timeout = timeout;
202 self
203 }
204
205 pub fn timeout(mut self, timeout: Duration) -> Self {
207 self.config.connect_timeout = timeout;
208 self.config.request_timeout = timeout;
209 self
210 }
211
212 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
214 self.config.user_agent = ua.into();
215 self
216 }
217
218 pub fn max_retries(mut self, retries: u32) -> Self {
220 self.config.max_retries = retries;
221 self
222 }
223
224 pub fn retry_strategy(mut self, strategy: RetryStrategy) -> Self {
226 self.config.retry_strategy = strategy;
227 self
228 }
229
230 pub fn no_retry(mut self) -> Self {
232 self.config.max_retries = 0;
233 self.config.retry_strategy = RetryStrategy::None;
234 self
235 }
236
237 pub fn proxy_http(mut self, url: impl Into<String>) -> Self {
239 let proxy = self.config.proxy.get_or_insert_with(|| ProxyConfig {
240 http: None,
241 https: None,
242 no_proxy: Vec::new(),
243 });
244 proxy.http = Some(url.into());
245 self
246 }
247
248 pub fn proxy_https(mut self, url: impl Into<String>) -> Self {
250 let proxy = self.config.proxy.get_or_insert_with(|| ProxyConfig {
251 http: None,
252 https: None,
253 no_proxy: Vec::new(),
254 });
255 proxy.https = Some(url.into());
256 self
257 }
258
259 pub fn proxy_all(mut self, url: impl Into<String>) -> Self {
261 let url = url.into();
262 self.config.proxy = Some(ProxyConfig {
263 http: Some(url.clone()),
264 https: Some(url),
265 no_proxy: Vec::new(),
266 });
267 self
268 }
269
270 pub fn follow_redirects(mut self, follow: bool) -> Self {
272 self.config.follow_redirects = follow;
273 self
274 }
275
276 pub fn max_redirects(mut self, max: u32) -> Self {
278 self.config.max_redirects = max;
279 self
280 }
281
282 pub fn accept_invalid_certs(mut self, accept: bool) -> Self {
284 self.config.accept_invalid_certs = accept;
285 self
286 }
287
288 pub fn build(self) -> HttpClientConfig {
290 self.config
291 }
292
293 pub fn build_client(self) -> Result<super::HttpClient, super::HttpError> {
295 super::HttpClient::with_config(self.build())
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_default_config() {
305 let config = HttpClientConfig::default();
306 assert_eq!(config.connect_timeout, Duration::from_secs(10));
307 assert_eq!(config.request_timeout, Duration::from_secs(30));
308 assert_eq!(config.max_retries, 3);
309 assert!(config.follow_redirects);
310 assert!(!config.accept_invalid_certs);
311 }
312
313 #[test]
314 fn test_quick_config() {
315 let config = HttpClientConfig::quick();
316 assert_eq!(config.connect_timeout, Duration::from_secs(5));
317 assert_eq!(config.max_retries, 1);
318 }
319
320 #[test]
321 fn test_retry_strategy_exponential() {
322 let strategy = RetryStrategy::ExponentialBackoff {
323 initial: Duration::from_millis(100),
324 max: Duration::from_secs(5),
325 };
326
327 assert_eq!(
328 strategy.delay_for_attempt(1),
329 Some(Duration::from_millis(100))
330 );
331 assert_eq!(
332 strategy.delay_for_attempt(2),
333 Some(Duration::from_millis(200))
334 );
335 assert_eq!(
336 strategy.delay_for_attempt(3),
337 Some(Duration::from_millis(400))
338 );
339 assert_eq!(
340 strategy.delay_for_attempt(10),
341 Some(Duration::from_secs(5))
342 );
343 }
344
345 #[test]
346 fn test_retry_strategy_fixed() {
347 let strategy = RetryStrategy::Fixed(Duration::from_millis(500));
348 assert_eq!(
349 strategy.delay_for_attempt(1),
350 Some(Duration::from_millis(500))
351 );
352 assert_eq!(
353 strategy.delay_for_attempt(5),
354 Some(Duration::from_millis(500))
355 );
356 }
357
358 #[test]
359 fn test_retry_strategy_none() {
360 let strategy = RetryStrategy::None;
361 assert_eq!(strategy.delay_for_attempt(1), None);
362 }
363
364 #[test]
365 fn test_builder() {
366 let config = HttpClientConfigBuilder::new()
367 .connect_timeout(Duration::from_secs(5))
368 .request_timeout(Duration::from_secs(60))
369 .max_retries(5)
370 .user_agent("TestAgent/1.0")
371 .build();
372
373 assert_eq!(config.connect_timeout, Duration::from_secs(5));
374 assert_eq!(config.request_timeout, Duration::from_secs(60));
375 assert_eq!(config.max_retries, 5);
376 assert_eq!(config.user_agent, "TestAgent/1.0");
377 }
378
379 #[test]
380 fn test_proxy_config() {
381 let proxy = ProxyConfig::all("http://proxy.example.com:8080")
382 .with_no_proxy(vec!["localhost".into(), "127.0.0.1".into()]);
383
384 assert_eq!(
385 proxy.http,
386 Some("http://proxy.example.com:8080".to_string())
387 );
388 assert_eq!(
389 proxy.https,
390 Some("http://proxy.example.com:8080".to_string())
391 );
392 assert_eq!(proxy.no_proxy.len(), 2);
393 }
394}