1use std::fmt;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::time::Duration;
6
7use crate::progress::Progress;
8
9pub type ProgressCallback = Arc<dyn Fn(&Progress) + Send + Sync>;
10pub type RetryDelayFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
11pub type RetryDelayProvider = Arc<dyn Fn(Duration) -> RetryDelayFuture + Send + Sync>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct RetryPolicy {
18 pub max_retries: u32,
20 pub base_backoff: Duration,
22}
23
24impl Default for RetryPolicy {
25 fn default() -> Self {
26 Self {
27 max_retries: 3,
28 base_backoff: Duration::from_millis(100),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum FetchPhase {
41 #[default]
46 Connecting,
47
48 Downloading,
53
54 Verifying,
59
60 Committing,
65
66 Completed,
70}
71
72impl std::fmt::Display for FetchPhase {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 FetchPhase::Connecting => write!(f, "Connecting"),
76 FetchPhase::Downloading => write!(f, "Downloading"),
77 FetchPhase::Verifying => write!(f, "Verifying"),
78 FetchPhase::Committing => write!(f, "Committing"),
79 FetchPhase::Completed => write!(f, "Completed"),
80 }
81 }
82}
83
84#[derive(Clone)]
98pub struct FetchOptions {
99 pub checksum: Option<[u8; 32]>,
102
103 pub retry_policy: RetryPolicy,
105
106 pub expected_bytes: Option<u64>,
108
109 pub resume_offset: Option<u64>,
111
112 pub headers: Arc<[(String, String)]>,
118
119 pub on_progress: Option<ProgressCallback>,
130
131 pub retry_delay_provider: Option<RetryDelayProvider>,
135}
136
137impl fmt::Debug for FetchOptions {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.debug_struct("FetchOptions")
140 .field("checksum", &self.checksum)
141 .field("retry_policy", &self.retry_policy)
142 .field("expected_bytes", &self.expected_bytes)
143 .field("resume_offset", &self.resume_offset)
144 .field("headers", &self.headers)
145 .field("on_progress", &"{ ... }")
146 .field("retry_delay_provider", &"{ ... }")
147 .finish()
148 }
149}
150
151impl Default for FetchOptions {
152 fn default() -> Self {
153 Self {
154 checksum: None,
155 retry_policy: RetryPolicy::default(),
156 expected_bytes: None,
157 resume_offset: None,
158 headers: Arc::new([]),
159 on_progress: None,
160 retry_delay_provider: None,
161 }
162 }
163}
164
165impl FetchOptions {
166 #[must_use]
177 pub fn checksum(mut self, checksum: Option<[u8; 32]>) -> Self {
178 self.checksum = checksum;
179 self
180 }
181
182 #[must_use]
192 pub fn max_retries(mut self, max_retries: u32) -> Self {
193 self.retry_policy.max_retries = max_retries;
194 self
195 }
196
197 #[must_use]
209 pub fn retry_backoff(mut self, retry_backoff: Duration) -> Self {
210 self.retry_policy.base_backoff = retry_backoff;
211 self
212 }
213
214 #[must_use]
215 pub fn retry_policy(mut self, retry_policy: RetryPolicy) -> Self {
217 self.retry_policy = retry_policy;
218 self
219 }
220
221 #[must_use]
222 pub fn expected_bytes(mut self, expected_bytes: Option<u64>) -> Self {
224 self.expected_bytes = expected_bytes;
225 self
226 }
227
228 #[must_use]
229 pub fn resume_offset(mut self, resume_offset: Option<u64>) -> Self {
231 self.resume_offset = resume_offset;
232 self
233 }
234
235 #[must_use]
247 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
248 let mut headers: Vec<_> = self.headers.iter().cloned().collect();
249 headers.push((key.into(), value.into()));
250 self.headers = Arc::from(headers);
251 self
252 }
253
254 #[must_use]
270 pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
271 self.headers = Arc::from(headers);
272 self
273 }
274
275 #[must_use]
297 pub fn on_progress(mut self, on_progress: Arc<dyn Fn(&Progress) + Send + Sync>) -> Self {
298 self.on_progress = Some(on_progress);
299 self
300 }
301
302 #[must_use]
303 pub fn retry_delay_provider(mut self, provider: RetryDelayProvider) -> Self {
305 self.retry_delay_provider = Some(provider);
306 self
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use std::sync::atomic::{AtomicU32, Ordering};
314
315 #[test]
316 fn test_fetch_phase_display() {
317 assert_eq!(FetchPhase::Connecting.to_string(), "Connecting");
318 assert_eq!(FetchPhase::Downloading.to_string(), "Downloading");
319 assert_eq!(FetchPhase::Verifying.to_string(), "Verifying");
320 assert_eq!(FetchPhase::Committing.to_string(), "Committing");
321 assert_eq!(FetchPhase::Completed.to_string(), "Completed");
322 }
323
324 #[test]
325 fn test_fetch_phase_default() {
326 assert_eq!(FetchPhase::default(), FetchPhase::Connecting);
327 }
328
329 #[test]
330 fn test_fetch_options_default() {
331 let options = FetchOptions::default();
332 assert!(options.checksum.is_none());
333 assert_eq!(options.retry_policy.max_retries, 3);
334 assert_eq!(
335 options.retry_policy.base_backoff,
336 Duration::from_millis(100)
337 );
338 assert_eq!(options.expected_bytes, None);
339 assert_eq!(options.resume_offset, None);
340 assert!(options.headers.is_empty());
341 assert!(options.on_progress.is_none());
342 assert!(options.retry_delay_provider.is_none());
343 }
344
345 #[test]
346 fn test_fetch_options_checksum() {
347 let hash = [1u8; 32];
348 let options = FetchOptions::default().checksum(Some(hash));
349 assert_eq!(options.checksum, Some(hash));
350
351 let options = FetchOptions::default().checksum(None);
352 assert!(options.checksum.is_none());
353 }
354
355 #[test]
356 fn test_fetch_options_max_retries() {
357 let options = FetchOptions::default().max_retries(5);
358 assert_eq!(options.retry_policy.max_retries, 5);
359
360 let options = FetchOptions::default().max_retries(0);
361 assert_eq!(options.retry_policy.max_retries, 0);
362 }
363
364 #[test]
365 fn test_fetch_options_retry_backoff() {
366 let duration = Duration::from_secs(1);
367 let options = FetchOptions::default().retry_backoff(duration);
368 assert_eq!(options.retry_policy.base_backoff, duration);
369 }
370
371 #[test]
372 fn test_fetch_options_resume_and_expected_bytes() {
373 let options = FetchOptions::default()
374 .resume_offset(Some(128))
375 .expected_bytes(Some(512));
376 assert_eq!(options.resume_offset, Some(128));
377 assert_eq!(options.expected_bytes, Some(512));
378 }
379
380 #[test]
381 fn test_fetch_options_header() {
382 let options = FetchOptions::default()
383 .header("Authorization", "Bearer token")
384 .header("User-Agent", "MyApp/1.0");
385
386 let headers: Vec<_> = options.headers.iter().cloned().collect();
387 assert_eq!(headers.len(), 2);
388 assert!(headers.contains(&("Authorization".to_string(), "Bearer token".to_string())));
389 assert!(headers.contains(&("User-Agent".to_string(), "MyApp/1.0".to_string())));
390 }
391
392 #[test]
393 fn test_fetch_options_headers() {
394 let headers = vec![
395 ("Authorization".to_string(), "Bearer token".to_string()),
396 ("User-Agent".to_string(), "MyApp/1.0".to_string()),
397 ];
398 let options = FetchOptions::default().headers(headers.clone());
399
400 let options_headers: Vec<_> = options.headers.iter().cloned().collect();
401 assert_eq!(options_headers, headers);
402 }
403
404 #[test]
405 fn test_fetch_options_headers_replace() {
406 let options = FetchOptions::default()
407 .header("Old", "value")
408 .headers(vec![("New".to_string(), "value".to_string())]);
409
410 let headers: Vec<_> = options.headers.iter().cloned().collect();
411 assert_eq!(headers.len(), 1);
412 assert!(headers.contains(&("New".to_string(), "value".to_string())));
413 assert!(!headers.iter().any(|(k, _)| k == "Old"));
414 }
415
416 #[test]
417 fn test_fetch_options_on_progress() {
418 let call_count = Arc::new(AtomicU32::new(0));
419 let call_count_clone = call_count.clone();
420
421 let options = FetchOptions::default().on_progress(Arc::new(move |_| {
422 call_count_clone.fetch_add(1, Ordering::SeqCst);
423 }));
424
425 assert!(options.on_progress.is_some());
426
427 if let Some(callback) = &options.on_progress {
428 let progress = Progress {
429 phase: FetchPhase::Downloading,
430 bytes_downloaded: 100,
431 total_bytes: Some(1000),
432 retry_count: 0,
433 performance_metrics: None,
434 };
435 callback(&progress);
436 assert_eq!(call_count.load(Ordering::SeqCst), 1);
437 }
438 }
439
440 #[test]
441 fn test_fetch_options_debug() {
442 let options = FetchOptions::default()
443 .checksum(Some([1u8; 32]))
444 .max_retries(5)
445 .header("Test", "value");
446
447 let debug_str = format!("{:?}", options);
448 assert!(debug_str.contains("FetchOptions"));
449 assert!(debug_str.contains("checksum: Some(["));
450 assert!(debug_str.contains("retry_policy"));
451 assert!(debug_str.contains("{ ... }"));
452 }
453
454 #[test]
455 fn test_fetch_options_builder_pattern() {
456 let hash = [2u8; 32];
457 let options = FetchOptions::default()
458 .checksum(Some(hash))
459 .max_retries(10)
460 .retry_backoff(Duration::from_millis(500))
461 .header("Custom", "header");
462
463 assert_eq!(options.checksum, Some(hash));
464 assert_eq!(options.retry_policy.max_retries, 10);
465 assert_eq!(
466 options.retry_policy.base_backoff,
467 Duration::from_millis(500)
468 );
469 assert_eq!(options.headers.len(), 1);
470 assert!(options.retry_delay_provider.is_none());
471
472 let options2 = FetchOptions::default()
474 .checksum(Some(hash))
475 .max_retries(10)
476 .retry_backoff(Duration::from_millis(500))
477 .headers(vec![("Another".to_string(), "header".to_string())]);
478
479 assert_eq!(options2.checksum, Some(hash));
480 assert_eq!(options2.retry_policy.max_retries, 10);
481 assert_eq!(
482 options2.retry_policy.base_backoff,
483 Duration::from_millis(500)
484 );
485 assert_eq!(options2.headers.len(), 1);
486 assert!(options2.retry_delay_provider.is_none());
487 }
488
489 #[test]
490 fn test_fetch_options_retry_delay_provider() {
491 let options =
492 FetchOptions::default().retry_delay_provider(Arc::new(|_| Box::pin(async {})));
493 assert!(options.retry_delay_provider.is_some());
494 }
495
496 #[test]
497 fn test_fetch_options_clone() {
498 let options = FetchOptions::default()
499 .checksum(Some([3u8; 32]))
500 .header("Test", "value");
501
502 let cloned = options.clone();
503 assert_eq!(cloned.checksum, options.checksum);
504 assert_eq!(cloned.retry_policy, options.retry_policy);
505 assert_eq!(cloned.headers.as_ptr(), options.headers.as_ptr()); }
507}