Skip to main content

greentic_distributor_client/
source.rs

1use crate::{ComponentId, DistributorError, PackId, Version};
2
3/// Pluggable source for fetching packs and components by identifier/version.
4pub trait DistributorSource: Send + Sync {
5    fn fetch_pack(&self, pack_id: &PackId, version: &Version) -> Result<Vec<u8>, DistributorError>;
6
7    fn fetch_component(
8        &self,
9        component_id: &ComponentId,
10        version: &Version,
11    ) -> Result<Vec<u8>, DistributorError>;
12}
13
14/// Simple priority-ordered collection of sources that tries each until one succeeds.
15pub struct ChainedDistributorSource {
16    sources: Vec<Box<dyn DistributorSource>>,
17}
18
19impl ChainedDistributorSource {
20    pub fn new(sources: Vec<Box<dyn DistributorSource>>) -> Self {
21        Self { sources }
22    }
23}
24
25impl DistributorSource for ChainedDistributorSource {
26    fn fetch_pack(&self, pack_id: &PackId, version: &Version) -> Result<Vec<u8>, DistributorError> {
27        for source in &self.sources {
28            match source.fetch_pack(pack_id, version) {
29                Ok(bytes) => return Ok(bytes),
30                Err(DistributorError::NotFound) => continue,
31                Err(err) => return Err(err),
32            }
33        }
34        Err(DistributorError::NotFound)
35    }
36
37    fn fetch_component(
38        &self,
39        component_id: &ComponentId,
40        version: &Version,
41    ) -> Result<Vec<u8>, DistributorError> {
42        for source in &self.sources {
43            match source.fetch_component(component_id, version) {
44                Ok(bytes) => return Ok(bytes),
45                Err(DistributorError::NotFound) => continue,
46                Err(err) => return Err(err),
47            }
48        }
49        Err(DistributorError::NotFound)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use std::collections::HashMap;
57
58    struct MemorySource {
59        packs: HashMap<(PackId, Version), Vec<u8>>,
60        components: HashMap<(ComponentId, Version), Vec<u8>>,
61        error: Option<String>,
62    }
63
64    impl MemorySource {
65        fn new() -> Self {
66            Self {
67                packs: HashMap::new(),
68                components: HashMap::new(),
69                error: None,
70            }
71        }
72
73        fn with_error(err: impl Into<String>) -> Self {
74            Self {
75                packs: HashMap::new(),
76                components: HashMap::new(),
77                error: Some(err.into()),
78            }
79        }
80    }
81
82    impl DistributorSource for MemorySource {
83        fn fetch_pack(
84            &self,
85            pack_id: &PackId,
86            version: &Version,
87        ) -> Result<Vec<u8>, DistributorError> {
88            if let Some(err) = &self.error {
89                return Err(DistributorError::Other(err.clone()));
90            }
91            self.packs
92                .get(&(pack_id.clone(), version.clone()))
93                .cloned()
94                .ok_or(DistributorError::NotFound)
95        }
96
97        fn fetch_component(
98            &self,
99            component_id: &ComponentId,
100            version: &Version,
101        ) -> Result<Vec<u8>, DistributorError> {
102            if let Some(err) = &self.error {
103                return Err(DistributorError::Other(err.clone()));
104            }
105            self.components
106                .get(&(component_id.clone(), version.clone()))
107                .cloned()
108                .ok_or(DistributorError::NotFound)
109        }
110    }
111
112    #[test]
113    fn chained_prefers_first_success() {
114        let version = Version::parse("1.0.0").unwrap();
115        let pack_id = PackId::try_from("pack.one").unwrap();
116        let mut primary = MemorySource::new();
117        primary
118            .packs
119            .insert((pack_id.clone(), version.clone()), b"pack".to_vec());
120        let chained =
121            ChainedDistributorSource::new(vec![Box::new(primary), Box::new(MemorySource::new())]);
122
123        let bytes = chained.fetch_pack(&pack_id, &version).unwrap();
124        assert_eq!(bytes, b"pack");
125    }
126
127    #[test]
128    fn chained_skips_not_found_continues() {
129        let version = Version::parse("1.0.0").unwrap();
130        let pack_id = PackId::try_from("pack.missing").unwrap();
131        let mut fallback = MemorySource::new();
132        fallback
133            .packs
134            .insert((pack_id.clone(), version.clone()), b"found".to_vec());
135        let chained =
136            ChainedDistributorSource::new(vec![Box::new(MemorySource::new()), Box::new(fallback)]);
137
138        let bytes = chained.fetch_pack(&pack_id, &version).unwrap();
139        assert_eq!(bytes, b"found");
140    }
141
142    #[test]
143    fn chained_propagates_other_errors() {
144        let version = Version::parse("1.0.0").unwrap();
145        let pack_id = PackId::try_from("pack.error").unwrap();
146        let chained =
147            ChainedDistributorSource::new(vec![Box::new(MemorySource::with_error("boom"))]);
148
149        let err = chained.fetch_pack(&pack_id, &version).unwrap_err();
150        assert!(matches!(err, DistributorError::Other(msg) if msg == "boom"));
151    }
152}