Skip to main content

pulith_fetch/config/
sources.rs

1/// Represents a download source with priority and metadata.
2#[derive(Clone, Debug, PartialEq, Eq)]
3pub struct DownloadSource {
4    /// The URL to download from
5    pub url: String,
6
7    /// Priority (lower = higher priority, 0 = highest)
8    pub priority: u32,
9
10    /// Expected checksum for this specific source
11    pub checksum: Option<[u8; 32]>,
12
13    /// Source type/category
14    pub source_type: SourceType,
15
16    /// Optional geographic region hint
17    pub region: Option<String>,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum SourceType {
22    /// Primary/official source
23    Primary,
24
25    /// Mirror/replica
26    Mirror,
27
28    /// CDN edge location
29    Cdn,
30
31    /// Fallback source
32    Fallback,
33}
34
35impl DownloadSource {
36    pub fn new(url: impl Into<String>) -> Self {
37        Self {
38            url: url.into(),
39            priority: 0,
40            checksum: None,
41            source_type: SourceType::Primary,
42            region: None,
43        }
44    }
45
46    pub fn priority(mut self, priority: u32) -> Self {
47        self.priority = priority;
48        self
49    }
50
51    pub fn checksum(mut self, checksum: [u8; 32]) -> Self {
52        self.checksum = Some(checksum);
53        self
54    }
55
56    pub fn source_type(mut self, source_type: SourceType) -> Self {
57        self.source_type = source_type;
58        self
59    }
60
61    pub fn region(mut self, region: impl Into<String>) -> Self {
62        self.region = Some(region.into());
63        self
64    }
65}
66
67/// Multi-source download configuration
68#[derive(Clone, Debug)]
69pub struct MultiSourceOptions {
70    /// List of sources to try
71    pub sources: Vec<DownloadSource>,
72
73    /// Strategy for selecting sources
74    pub strategy: SourceSelectionStrategy,
75
76    /// Whether to verify all sources have same content
77    pub verify_consistency: bool,
78
79    /// Timeout for each source attempt
80    pub per_source_timeout: Option<std::time::Duration>,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum SourceSelectionStrategy {
85    /// Try in priority order until success
86    Priority,
87
88    /// Try fastest responding source first
89    FastestFirst,
90
91    /// Try geographically closest source first
92    Geographic,
93
94    /// Try all sources in parallel, use first success
95    RaceAll,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_download_source_new() {
104        let source = DownloadSource::new("https://example.com/file");
105        assert_eq!(source.url, "https://example.com/file");
106        assert_eq!(source.priority, 0);
107        assert!(source.checksum.is_none());
108        assert_eq!(source.source_type, SourceType::Primary);
109        assert!(source.region.is_none());
110    }
111
112    #[test]
113    fn test_download_source_builder() {
114        let checksum = [1u8; 32];
115        let source = DownloadSource::new("https://example.com/file")
116            .priority(5)
117            .checksum(checksum)
118            .source_type(SourceType::Mirror)
119            .region("us-west");
120
121        assert_eq!(source.url, "https://example.com/file");
122        assert_eq!(source.priority, 5);
123        assert_eq!(source.checksum, Some(checksum));
124        assert_eq!(source.source_type, SourceType::Mirror);
125        assert_eq!(source.region, Some("us-west".to_string()));
126    }
127
128    #[test]
129    fn test_download_source_clone() {
130        let source = DownloadSource::new("https://example.com/file")
131            .priority(3)
132            .source_type(SourceType::Cdn);
133
134        let cloned = source.clone();
135        assert_eq!(cloned, source);
136    }
137
138    #[test]
139    fn test_source_type_equality() {
140        assert_eq!(SourceType::Primary, SourceType::Primary);
141        assert_ne!(SourceType::Primary, SourceType::Mirror);
142        assert_ne!(SourceType::Mirror, SourceType::Cdn);
143        assert_ne!(SourceType::Cdn, SourceType::Fallback);
144    }
145
146    #[test]
147    fn test_multi_source_options_default() {
148        // Test that MultiSourceOptions can be created
149        let options = MultiSourceOptions {
150            sources: vec![],
151            strategy: SourceSelectionStrategy::Priority,
152            verify_consistency: false,
153            per_source_timeout: None,
154        };
155
156        assert!(options.sources.is_empty());
157        assert_eq!(options.strategy, SourceSelectionStrategy::Priority);
158        assert!(!options.verify_consistency);
159        assert!(options.per_source_timeout.is_none());
160    }
161
162    #[test]
163    fn test_multi_source_options_with_sources() {
164        let source1 = DownloadSource::new("https://primary.example.com")
165            .priority(0)
166            .source_type(SourceType::Primary);
167
168        let source2 = DownloadSource::new("https://mirror.example.com")
169            .priority(1)
170            .source_type(SourceType::Mirror)
171            .region("eu");
172
173        let options = MultiSourceOptions {
174            sources: vec![source1.clone(), source2.clone()],
175            strategy: SourceSelectionStrategy::FastestFirst,
176            verify_consistency: true,
177            per_source_timeout: Some(std::time::Duration::from_secs(30)),
178        };
179
180        assert_eq!(options.sources.len(), 2);
181        assert_eq!(options.sources[0], source1);
182        assert_eq!(options.sources[1], source2);
183        assert_eq!(options.strategy, SourceSelectionStrategy::FastestFirst);
184        assert!(options.verify_consistency);
185        assert_eq!(
186            options.per_source_timeout,
187            Some(std::time::Duration::from_secs(30))
188        );
189    }
190
191    #[test]
192    fn test_source_selection_strategies() {
193        let strategies = [
194            SourceSelectionStrategy::Priority,
195            SourceSelectionStrategy::FastestFirst,
196            SourceSelectionStrategy::Geographic,
197            SourceSelectionStrategy::RaceAll,
198        ];
199
200        // Test all strategies are different
201        for (i, strategy1) in strategies.iter().enumerate() {
202            for (j, strategy2) in strategies.iter().enumerate() {
203                if i != j {
204                    assert_ne!(strategy1, strategy2);
205                }
206            }
207        }
208    }
209
210    #[test]
211    fn test_download_source_with_string_conversion() {
212        let url_string = "https://example.com/file".to_string();
213        let source = DownloadSource::new(url_string.clone());
214        assert_eq!(source.url, url_string);
215
216        let region_string = "us-east".to_string();
217        let source_with_region = source.region(region_string.clone());
218        assert_eq!(source_with_region.region, Some(region_string));
219    }
220
221    #[test]
222    fn test_download_source_debug() {
223        let source = DownloadSource::new("https://example.com/file")
224            .priority(2)
225            .source_type(SourceType::Cdn)
226            .region("us-west");
227
228        let debug_str = format!("{:?}", source);
229        assert!(debug_str.contains("DownloadSource"));
230        assert!(debug_str.contains("https://example.com/file"));
231        assert!(debug_str.contains("priority: 2"));
232        assert!(debug_str.contains("Cdn"));
233        assert!(debug_str.contains("us-west"));
234    }
235
236    #[test]
237    fn test_multi_source_options_debug() {
238        let options = MultiSourceOptions {
239            sources: vec![DownloadSource::new("https://example.com")],
240            strategy: SourceSelectionStrategy::RaceAll,
241            verify_consistency: true,
242            per_source_timeout: Some(std::time::Duration::from_secs(60)),
243        };
244
245        let debug_str = format!("{:?}", options);
246        assert!(debug_str.contains("MultiSourceOptions"));
247        assert!(debug_str.contains("RaceAll"));
248        assert!(debug_str.contains("verify_consistency: true"));
249    }
250}