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