mithril_common/entities/
file_uri.rs

1use std::collections::{HashMap, HashSet};
2
3use anyhow::anyhow;
4use serde::{Deserialize, Serialize};
5
6use crate::entities::ImmutableFileNumber;
7use crate::StdResult;
8
9/// FileUri represents a file URI used to identify the file's location
10#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
11pub struct FileUri(pub String);
12
13impl From<FileUri> for String {
14    fn from(file_uri: FileUri) -> Self {
15        file_uri.0
16    }
17}
18
19impl From<&FileUri> for String {
20    fn from(file_uri: &FileUri) -> Self {
21        file_uri.0.clone()
22    }
23}
24
25/// TemplateVariable represents a variable in a template
26pub type TemplateVariable = String;
27
28/// TemplateValue represents a value in a template
29pub type TemplateValue = String;
30
31/// [TemplateUri] represents an URI pattern used to build a file's location
32#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
33pub struct TemplateUri(pub String);
34
35/// [MultiFilesUri] represents a unique location uri for multiple files
36#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
37pub enum MultiFilesUri {
38    /// URI template representing several URI
39    Template(TemplateUri),
40}
41
42impl MultiFilesUri {
43    /// Extract a template from a list of URIs
44    pub fn extract_template_from_uris(
45        file_uris: Vec<String>,
46        extractor: impl Fn(&str) -> StdResult<Option<String>>,
47    ) -> StdResult<Option<TemplateUri>> {
48        let mut templates = HashSet::new();
49        for file_uri in file_uris {
50            let template_uri = extractor(&file_uri)?;
51            template_uri.map(|template| templates.insert(template));
52        }
53
54        if templates.len() > 1 {
55            return Err(anyhow!("Multiple templates found in the file URIs"));
56        }
57
58        Ok(templates.into_iter().next().map(TemplateUri))
59    }
60
61    /// Expand the template to a list of file URIs
62    pub fn expand_to_file_uris(
63        &self,
64        variables: Vec<HashMap<TemplateVariable, TemplateValue>>,
65    ) -> StdResult<Vec<FileUri>> {
66        Ok(variables
67            .into_iter()
68            .map(|variable| self.expand_to_file_uri(variable))
69            .collect())
70    }
71
72    /// Expand the template to one file URI
73    pub fn expand_to_file_uri(
74        &self,
75        variable: HashMap<TemplateVariable, TemplateValue>,
76    ) -> FileUri {
77        match self {
78            MultiFilesUri::Template(template) => {
79                let mut file_uri = template.0.clone();
80                for (key, value) in variable {
81                    file_uri = file_uri.replace(&format!("{{{}}}", key), &value);
82                }
83
84                FileUri(file_uri)
85            }
86        }
87    }
88
89    /// Expand the template to a file URI for a specific immutable file number
90    ///
91    /// Note: the template must contain the `{immutable_file_number}` variable
92    pub fn expand_for_immutable_file_number(
93        &self,
94        immutable_file_number: ImmutableFileNumber,
95    ) -> FileUri {
96        self.expand_to_file_uri(HashMap::from([(
97            "immutable_file_number".to_string(),
98            format!("{:05}", immutable_file_number),
99        )]))
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn returns_template() {
109        let file_uris = vec![
110            "http://whatever/00001.tar.gz".to_string(),
111            "http://whatever/00002.tar.gz".to_string(),
112        ];
113        fn extractor_returning_same_uri(_file_uri: &str) -> StdResult<Option<String>> {
114            Ok(Some(
115                "http://whatever/{immutable_file_number}.tar.gz".to_string(),
116            ))
117        }
118
119        let template =
120            MultiFilesUri::extract_template_from_uris(file_uris, extractor_returning_same_uri)
121                .unwrap();
122
123        assert_eq!(
124            template,
125            Some(TemplateUri(
126                "http://whatever/{immutable_file_number}.tar.gz".to_string()
127            ))
128        );
129    }
130
131    #[test]
132    fn returns_error_with_multiple_templates() {
133        let file_uris = vec![
134            "http://whatever/00001.tar.gz".to_string(),
135            "http://00002.tar.gz/whatever".to_string(),
136        ];
137        fn extractor_returning_different_uri(file_uri: &str) -> StdResult<Option<String>> {
138            Ok(Some(file_uri.to_string()))
139        }
140
141        MultiFilesUri::extract_template_from_uris(file_uris, extractor_returning_different_uri)
142            .expect_err(
143                "Should return an error when multiple templates are found in the file URIs",
144            );
145    }
146
147    #[test]
148    fn expand_multi_file_template_to_one_file_uri() {
149        let template = MultiFilesUri::Template(TemplateUri(
150            "http://whatever/{var1}-{var2}.tar.gz".to_string(),
151        ));
152
153        assert_eq!(
154            template.expand_to_file_uri(HashMap::from([
155                ("var1".to_string(), "00001".to_string()),
156                ("var2".to_string(), "abc".to_string()),
157            ]),),
158            FileUri("http://whatever/00001-abc.tar.gz".to_string()),
159        );
160
161        assert_eq!(
162            template.expand_to_file_uri(HashMap::from([
163                ("var1".to_string(), "00001".to_string()),
164                ("var2".to_string(), "def".to_string()),
165            ]),),
166            FileUri("http://whatever/00001-def.tar.gz".to_string()),
167        );
168
169        assert_eq!(
170            template.expand_to_file_uri(HashMap::from([
171                ("var1".to_string(), "00002".to_string()),
172                ("var2".to_string(), "def".to_string()),
173            ]),),
174            FileUri("http://whatever/00002-def.tar.gz".to_string()),
175        );
176    }
177
178    #[test]
179    fn expand_multi_file_template_to_immutable_file_number() {
180        let template = MultiFilesUri::Template(TemplateUri(
181            "http://whatever/{immutable_file_number}.tar.gz".to_string(),
182        ));
183
184        assert_eq!(
185            template.expand_for_immutable_file_number(6),
186            FileUri("http://whatever/00006.tar.gz".to_string()),
187        );
188
189        assert_eq!(
190            template.expand_for_immutable_file_number(15329),
191            FileUri("http://whatever/15329.tar.gz".to_string()),
192        );
193
194        assert_eq!(
195            template.expand_for_immutable_file_number(199999),
196            FileUri("http://whatever/199999.tar.gz".to_string()),
197        );
198    }
199
200    #[test]
201    fn expand_multi_file_template_to_multiple_file_uris() {
202        let template = MultiFilesUri::Template(TemplateUri(
203            "http://whatever/{var1}-{var2}.tar.gz".to_string(),
204        ));
205        let variables = vec![
206            HashMap::from([
207                ("var1".to_string(), "00001".to_string()),
208                ("var2".to_string(), "abc".to_string()),
209            ]),
210            HashMap::from([
211                ("var1".to_string(), "00001".to_string()),
212                ("var2".to_string(), "def".to_string()),
213            ]),
214            HashMap::from([
215                ("var1".to_string(), "00002".to_string()),
216                ("var2".to_string(), "def".to_string()),
217            ]),
218        ];
219
220        let expanded_file_uris = template.expand_to_file_uris(variables).unwrap();
221        assert_eq!(
222            vec![
223                FileUri("http://whatever/00001-abc.tar.gz".to_string()),
224                FileUri("http://whatever/00001-def.tar.gz".to_string()),
225                FileUri("http://whatever/00002-def.tar.gz".to_string()),
226            ],
227            expanded_file_uris,
228        );
229    }
230}