1use std::{cmp::min, collections::HashMap, fmt, time::Duration};
2
3use bitflags::bitflags;
4use bon::Builder;
5
6bitflags! {
7 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
16 pub struct Compression: u8 {
17 const GZIP = 1 << 0;
18 const DEFLATE = 1 << 1;
19 const BROTLI = 1 << 2;
20 const ZSTD = 1 << 3;
21 }
22}
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
25pub struct Headers {
26 inner: HashMap<String, String>,
27}
28
29impl Headers {
30 #[must_use]
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn get(&self, key: &str) -> Option<&str> {
37 self.inner.get(key).map(String::as_str)
38 }
39
40 pub fn insert<K: Into<String>, V: Into<String>>(&mut self, key: K, value: V) {
41 self.inner.insert(key.into(), value.into());
42 }
43
44 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 self.inner.is_empty()
47 }
48
49 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
50 self.inner.iter().map(|(k, v)| (k.as_str(), v.as_str()))
51 }
52}
53
54impl From<HashMap<String, String>> for Headers {
55 fn from(map: HashMap<String, String>) -> Self {
56 Self { inner: map }
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct RangeSpec {
62 pub end: Option<u64>,
63 pub start: u64,
64}
65
66impl RangeSpec {
67 #[must_use]
68 pub fn new(start: u64, end: Option<u64>) -> Self {
69 Self { end, start }
70 }
71}
72
73impl fmt::Display for RangeSpec {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self.end {
76 Some(end) => write!(f, "bytes={}-{}", self.start, end),
77 None => write!(f, "bytes={}-", self.start),
78 }
79 }
80}
81
82#[derive(Clone, Debug)]
83#[non_exhaustive]
84pub struct RetryPolicy {
85 pub base_delay: Duration,
86 pub max_delay: Duration,
87 pub max_retries: u32,
88}
89
90impl Default for RetryPolicy {
91 fn default() -> Self {
92 Self {
93 base_delay: Duration::from_millis(100),
94 max_delay: Duration::from_secs(5),
95 max_retries: 3,
96 }
97 }
98}
99
100impl RetryPolicy {
101 #[must_use]
102 pub fn new(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
103 Self {
104 base_delay,
105 max_delay,
106 max_retries,
107 }
108 }
109
110 #[must_use]
111 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
112 const BACKOFF_BASE: u32 = 2;
113
114 if attempt == 0 {
115 return Duration::ZERO;
116 }
117 let exponential_delay = self.base_delay * BACKOFF_BASE.pow(attempt.saturating_sub(1));
118 min(exponential_delay, self.max_delay)
119 }
120}
121
122#[derive(Clone, Debug, Builder)]
123#[non_exhaustive]
124pub struct NetOptions {
125 #[builder(default = Compression::all())]
131 pub compression: Compression,
132 #[builder(default = Duration::from_secs(30))]
149 pub inactivity_timeout: Duration,
150 pub total_timeout: Option<Duration>,
159 #[builder(default)]
160 pub retry_policy: RetryPolicy,
161 #[builder(default)]
164 pub is_insecure: bool,
165 #[builder(default = 8)]
169 pub pool_max_idle_per_host: usize,
170}
171
172impl Default for NetOptions {
173 fn default() -> Self {
174 Self::builder()
175 .total_timeout(Duration::from_secs(120))
176 .build()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 mod kithara {
183 pub(crate) use kithara_test_macros::test;
184 }
185
186 use super::*;
187
188 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
189 #[case::empty_headers(Headers::new(), true)]
190 #[case::headers_with_values({
191 let mut h = Headers::new();
192 h.insert("key1", "value1");
193 h.insert("key2", "value2");
194 h
195 }, false)]
196 async fn test_headers_is_empty(#[case] headers: Headers, #[case] expected_empty: bool) {
197 assert_eq!(headers.is_empty(), expected_empty);
198 }
199
200 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
201 #[case::simple_key("key1", "value1")]
202 #[case::content_type("Content-Type", "application/json")]
203 #[case::custom_header("X-Custom-Header", "custom-value")]
204 async fn test_headers_insert_and_get(#[case] key: &str, #[case] value: &str) {
205 let mut headers = Headers::new();
206 headers.insert(key, value);
207
208 assert_eq!(headers.get(key), Some(value));
209 assert_eq!(headers.get("non-existent"), None);
210 }
211
212 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
213 async fn test_headers_iter() {
214 let mut headers = Headers::new();
215 headers.insert("key1", "value1");
216 headers.insert("key2", "value2");
217 headers.insert("key3", "value3");
218
219 let mut iterated = HashMap::new();
220 for (k, v) in headers.iter() {
221 iterated.insert(k.to_string(), v.to_string());
222 }
223
224 assert_eq!(iterated.len(), 3);
225 assert_eq!(iterated.get("key1"), Some(&"value1".to_string()));
226 assert_eq!(iterated.get("key2"), Some(&"value2".to_string()));
227 assert_eq!(iterated.get("key3"), Some(&"value3".to_string()));
228 }
229
230 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
231 async fn test_headers_from_hashmap() {
232 let mut map = HashMap::new();
233 map.insert("key1".to_string(), "value1".to_string());
234 map.insert("key2".to_string(), "value2".to_string());
235
236 let headers: Headers = map.into();
237
238 assert!(!headers.is_empty());
239 assert_eq!(headers.get("key1"), Some("value1"));
240 assert_eq!(headers.get("key2"), Some("value2"));
241 }
242
243 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
244 async fn test_headers_default() {
245 let headers = Headers::default();
246 assert!(headers.is_empty());
247 }
248
249 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
250 #[case::full_range(0, Some(100), "bytes=0-100")]
251 #[case::open_ended(50, None, "bytes=50-")]
252 #[case::single_byte(10, Some(10), "bytes=10-10")]
253 #[case::zero_length(0, Some(0), "bytes=0-0")]
254 async fn test_range_spec_to_header_value(
255 #[case] start: u64,
256 #[case] end: Option<u64>,
257 #[case] expected_header: &str,
258 ) {
259 let range = RangeSpec::new(start, end);
260 assert_eq!(range.to_string(), expected_header);
261 }
262
263 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
264 #[case::equal_ranges(RangeSpec::new(0, Some(100)), RangeSpec::new(0, Some(100)), true)]
265 #[case::different_starts(RangeSpec::new(0, Some(100)), RangeSpec::new(1, Some(100)), false)]
266 #[case::different_ends(RangeSpec::new(0, Some(100)), RangeSpec::new(0, Some(99)), false)]
267 #[case::one_open_ended(RangeSpec::new(0, None), RangeSpec::new(0, None), true)]
268 #[case::mixed_ends(RangeSpec::new(0, Some(100)), RangeSpec::new(0, None), false)]
269 async fn test_range_spec_partial_eq(
270 #[case] range1: RangeSpec,
271 #[case] range2: RangeSpec,
272 #[case] expected_equal: bool,
273 ) {
274 assert_eq!(range1 == range2, expected_equal);
275 }
276
277 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
278 async fn test_range_spec_debug() {
279 let range = RangeSpec::new(10, Some(20));
280 let debug_output = format!("{:?}", range);
281 assert!(debug_output.contains("RangeSpec"));
282 assert!(debug_output.contains("start: 10"));
283 assert!(debug_output.contains("end: Some(20)"));
284 }
285
286 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
287 async fn test_range_spec_clone() {
288 let range1 = RangeSpec::new(10, Some(20));
289 let range2 = range1.clone();
290
291 assert_eq!(range1, range2);
292 assert_eq!(range1.start, range2.start);
293 assert_eq!(range1.end, range2.end);
294 }
295
296 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
297 async fn test_retry_policy_default() {
298 let policy = RetryPolicy::default();
299
300 assert_eq!(policy.max_retries, 3);
301 assert_eq!(policy.base_delay, Duration::from_millis(100));
302 assert_eq!(policy.max_delay, Duration::from_secs(5));
303 }
304
305 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
306 #[case(1, Duration::from_millis(50), Duration::from_secs(1))]
307 #[case(5, Duration::from_millis(100), Duration::from_secs(2))]
308 #[case(10, Duration::from_millis(200), Duration::from_secs(10))]
309 async fn test_retry_policy_new(
310 #[case] max_retries: u32,
311 #[case] base_delay: Duration,
312 #[case] max_delay: Duration,
313 ) {
314 let policy = RetryPolicy::new(max_retries, base_delay, max_delay);
315
316 assert_eq!(policy.max_retries, max_retries);
317 assert_eq!(policy.base_delay, base_delay);
318 assert_eq!(policy.max_delay, max_delay);
319 }
320
321 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
322 #[case(0, Duration::ZERO)]
323 #[case(1, Duration::from_millis(100))]
324 #[case(2, Duration::from_millis(200))]
325 #[case(3, Duration::from_millis(400))]
326 #[case(4, Duration::from_millis(800))]
327 #[case(5, Duration::from_millis(1600))]
328 #[case(10, Duration::from_secs(5))]
329 #[case(20, Duration::from_secs(5))]
330 async fn test_retry_policy_delay_for_attempt_default(
331 #[case] attempt: u32,
332 #[case] expected_delay: Duration,
333 ) {
334 let policy = RetryPolicy::default();
335 let delay = policy.delay_for_attempt(attempt);
336
337 assert_eq!(delay, expected_delay);
338 }
339
340 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
341 #[case(
342 1,
343 Duration::from_millis(50),
344 Duration::from_millis(200),
345 0,
346 Duration::ZERO
347 )]
348 #[case(
349 1,
350 Duration::from_millis(50),
351 Duration::from_millis(200),
352 1,
353 Duration::from_millis(50)
354 )]
355 #[case(
356 1,
357 Duration::from_millis(50),
358 Duration::from_millis(200),
359 2,
360 Duration::from_millis(100)
361 )]
362 #[case(
363 1,
364 Duration::from_millis(50),
365 Duration::from_millis(200),
366 3,
367 Duration::from_millis(200)
368 )]
369 #[case(
370 1,
371 Duration::from_millis(50),
372 Duration::from_millis(200),
373 4,
374 Duration::from_millis(200)
375 )]
376 async fn test_retry_policy_delay_for_attempt_custom(
377 #[case] max_retries: u32,
378 #[case] base_delay: Duration,
379 #[case] max_delay: Duration,
380 #[case] attempt: u32,
381 #[case] expected_delay: Duration,
382 ) {
383 let policy = RetryPolicy::new(max_retries, base_delay, max_delay);
384 let delay = policy.delay_for_attempt(attempt);
385
386 assert_eq!(delay, expected_delay);
387 }
388
389 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
390 async fn test_retry_policy_debug() {
391 let policy = RetryPolicy::default();
392 let debug_output = format!("{:?}", policy);
393
394 assert!(debug_output.contains("RetryPolicy"));
395 assert!(debug_output.contains("max_retries: 3"));
396 assert!(debug_output.contains("base_delay"));
397 assert!(debug_output.contains("max_delay"));
398 }
399
400 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
401 async fn test_retry_policy_clone() {
402 let policy1 = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(2));
403 let policy2 = policy1.clone();
404
405 assert_eq!(policy1.max_retries, policy2.max_retries);
406 assert_eq!(policy1.base_delay, policy2.base_delay);
407 assert_eq!(policy1.max_delay, policy2.max_delay);
408 }
409
410 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
411 #[case::start_equals_end(10, Some(10), "bytes=10-10")]
412 #[case::start_greater_than_end(20, Some(10), "bytes=20-10")]
413 #[case::max_values(u64::MAX, Some(u64::MAX), &format!("bytes={}-{}", u64::MAX, u64::MAX))]
414 async fn test_range_spec_edge_cases(
415 #[case] start: u64,
416 #[case] end: Option<u64>,
417 #[case] expected_header: &str,
418 ) {
419 let range = RangeSpec::new(start, end);
420 assert_eq!(range.to_string(), expected_header);
421 }
422
423 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
424 #[case::zero_max_retries(0, Duration::from_millis(100), Duration::from_secs(1))]
425 #[case::large_max_retries(100, Duration::from_millis(10), Duration::from_secs(10))]
426 #[case::zero_base_delay(3, Duration::ZERO, Duration::from_secs(1))]
427 #[case::zero_max_delay(3, Duration::from_millis(100), Duration::ZERO)]
428 async fn test_retry_policy_edge_cases(
429 #[case] max_retries: u32,
430 #[case] base_delay: Duration,
431 #[case] max_delay: Duration,
432 ) {
433 let policy = RetryPolicy::new(max_retries, base_delay, max_delay);
434
435 for attempt in 0..=5 {
436 let delay = policy.delay_for_attempt(attempt);
437
438 assert!(delay >= Duration::ZERO);
439
440 assert!(delay <= max_delay);
441
442 if base_delay == Duration::ZERO {
443 assert_eq!(delay, Duration::ZERO);
444 }
445 }
446 }
447
448 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
449 #[case(10)]
450 #[case(20)]
451 async fn test_retry_policy_large_attempts(#[case] attempt: u32) {
452 let policy = RetryPolicy::default();
453
454 let delay = policy.delay_for_attempt(attempt);
455
456 assert!(delay <= policy.max_delay);
457 assert!(delay >= Duration::ZERO);
458 }
459
460 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
461 #[case::with_spaces("X-Custom Header", "value with spaces")]
462 #[case::with_unicode("X-Emoji", "🎉")]
463 #[case::with_special_chars("X-Special", "a\tb\nc")]
464 #[case::empty_value("X-Empty", "")]
465 async fn test_headers_special_characters(#[case] key: &str, #[case] value: &str) {
466 let mut headers = Headers::new();
467 headers.insert(key, value);
468
469 assert_eq!(headers.get(key), Some(value));
470 }
471
472 #[kithara::test(tokio, timeout(Duration::from_secs(5)))]
473 async fn test_headers_case_sensitive() {
474 let mut headers = Headers::new();
475 headers.insert("Content-Type", "application/json");
476 headers.insert("content-type", "text/plain");
477
478 assert_eq!(headers.get("Content-Type"), Some("application/json"));
479 assert_eq!(headers.get("content-type"), Some("text/plain"));
480 assert_ne!(headers.get("Content-Type"), headers.get("content-type"));
481 }
482}