batchcensor/
config.rs

1//! Models for a single configuration file.
2
3use crate::{Replace, Transcript};
4use relative_path::{RelativePath, RelativePathBuf};
5use std::slice;
6
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize)]
8pub struct ReplaceFile {
9    path: RelativePathBuf,
10    /// Transcript of the recording.
11    #[serde(default)]
12    #[serde(skip_serializing_if = "Option::is_none")]
13    transcript: Option<Transcript>,
14    /// Replacements. If empty, file is clean.
15    #[serde(default)]
16    #[serde(skip_serializing_if = "Vec::is_empty")]
17    replace: Vec<Replace>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize)]
21#[serde(untagged)]
22pub enum Files {
23    List(Vec<ReplaceFile>),
24    Map(linked_hash_map::LinkedHashMap<RelativePathBuf, Transcript>),
25    ListOfMaps(Vec<linked_hash_map::LinkedHashMap<RelativePathBuf, Transcript>>),
26}
27
28impl Files {
29    /// Check if empty.
30    pub fn is_empty(&self) -> bool {
31        match *self {
32            Files::List(ref list) => list.is_empty(),
33            Files::Map(ref map) => map.is_empty(),
34            Files::ListOfMaps(ref list) => list.iter().all(|m| m.is_empty()),
35        }
36    }
37
38    /// Iterate over all files.
39    pub fn iter(&self) -> FilesIter<'_> {
40        match *self {
41            Files::List(ref list) => FilesIter::List(list.iter()),
42            Files::Map(ref map) => FilesIter::Map(map.iter()),
43            Files::ListOfMaps(ref list) => FilesIter::ListOfMaps {
44                current: None,
45                it: list.iter(),
46            },
47        }
48    }
49
50    /// Insert the given transcript for the specified path.
51    fn insert(&mut self, path: RelativePathBuf, transcript: Transcript) {
52        match *self {
53            Files::List(ref mut list) => list.push(ReplaceFile {
54                path,
55                transcript: Some(transcript),
56                replace: vec![],
57            }),
58            Files::Map(ref mut map) => {
59                map.insert(path, transcript);
60            }
61            Files::ListOfMaps(ref mut list) => {
62                let mut map = linked_hash_map::LinkedHashMap::new();
63                map.insert(path, transcript);
64                list.push(map);
65            }
66        }
67    }
68}
69
70impl Default for Files {
71    fn default() -> Self {
72        Files::ListOfMaps(vec![])
73    }
74}
75
76impl<'a> IntoIterator for &'a Files {
77    type IntoIter = FilesIter<'a>;
78    type Item = (&'a RelativePath, Vec<&'a Replace>, Option<&'a Transcript>);
79
80    fn into_iter(self) -> Self::IntoIter {
81        self.iter()
82    }
83}
84
85/// An iterator over replacements.
86pub enum FilesIter<'a> {
87    List(slice::Iter<'a, ReplaceFile>),
88    Map(linked_hash_map::Iter<'a, RelativePathBuf, Transcript>),
89    ListOfMaps {
90        current: Option<linked_hash_map::Iter<'a, RelativePathBuf, Transcript>>,
91        it: slice::Iter<'a, linked_hash_map::LinkedHashMap<RelativePathBuf, Transcript>>,
92    },
93}
94
95impl<'a> Iterator for FilesIter<'a> {
96    type Item = (&'a RelativePath, Vec<&'a Replace>, Option<&'a Transcript>);
97
98    fn next(&mut self) -> Option<Self::Item> {
99        match *self {
100            FilesIter::List(ref mut it) => {
101                let ReplaceFile {
102                    ref path,
103                    ref transcript,
104                    ref replace,
105                } = it.next()?;
106                Some((path, replace.iter().collect(), transcript.as_ref()))
107            }
108            FilesIter::Map(ref mut it) => {
109                let (ref path, ref transcript) = it.next()?;
110                Some((path, vec![], Some(transcript)))
111            }
112            FilesIter::ListOfMaps {
113                ref mut current,
114                ref mut it,
115            } => loop {
116                if let Some((ref path, ref transcript)) = current.as_mut().and_then(|it| it.next())
117                {
118                    return Some((path, vec![], Some(transcript)));
119                }
120
121                *current = match it.next() {
122                    Some(n) => Some(n.iter()),
123                    None => return None,
124                }
125            },
126        }
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize)]
131pub struct ReplaceDir {
132    pub path: RelativePathBuf,
133    #[serde(default)]
134    #[serde(rename = "file_prefix")]
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub prefix: Option<String>,
137    #[serde(default)]
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub suffix: Option<String>,
140    #[serde(default)]
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub file_extension: Option<String>,
143    #[serde(default)]
144    #[serde(skip_serializing_if = "Files::is_empty")]
145    pub files: Files,
146}
147
148impl ReplaceDir {
149    /// Construct a new replacement for the given directory.
150    pub fn new(path: RelativePathBuf) -> Self {
151        ReplaceDir {
152            path,
153            prefix: None,
154            suffix: None,
155            file_extension: None,
156            files: Files::List(vec![]),
157        }
158    }
159
160    /// Insert the given file into the configuration.
161    pub fn insert_file(
162        &mut self,
163        file_extension: Option<&str>,
164        mut file: RelativePathBuf,
165        transcript: Transcript,
166    ) -> Result<(), failure::Error> {
167        let file_extension = self
168            .file_extension
169            .as_ref()
170            .map(|s| s.as_str())
171            .or(file_extension);
172
173        if let Some(e) = file_extension {
174            if Some(e) != file.extension() {
175                failure::bail!("extension does not match");
176            }
177
178            file = match file.file_stem() {
179                Some(stem) => file.with_file_name(stem),
180                None => file,
181            };
182        }
183
184        if let Some(prefix) = self.prefix.as_ref() {
185            let mut name = match file.file_name() {
186                Some(name) => name,
187                None => failure::bail!("expected file name"),
188            };
189
190            if !name.starts_with(prefix) {
191                failure::bail!("bad prefix in file");
192            }
193
194            name = &name[prefix.len()..];
195            file = file.with_file_name(name);
196        }
197
198        if let Some(suffix) = self.suffix.as_ref() {
199            let mut name = match file.file_name() {
200                Some(name) => name,
201                None => failure::bail!("expected file name"),
202            };
203
204            if !name.ends_with(suffix) {
205                failure::bail!("bad prefix in file");
206            }
207
208            name = &name[..(name.len() - suffix.len())];
209            file = file.with_file_name(name);
210        }
211
212        self.files.insert(file, transcript);
213        Ok(())
214    }
215
216    /// Test if the dir contains the given path.
217    pub fn contains(&self, path: &RelativePath) -> bool {
218        let stem = match path.file_stem() {
219            Some(stem) => stem,
220            None => return false,
221        };
222
223        if let Some(prefix) = self.prefix.as_ref() {
224            if !stem.starts_with(prefix) {
225                return false;
226            }
227        }
228
229        if let Some(suffix) = self.suffix.as_ref() {
230            if !stem.ends_with(suffix) {
231                return false;
232            }
233        }
234
235        if let Some(extension) = self.file_extension.as_ref() {
236            match path.extension() {
237                Some(e) if e == extension => {}
238                _ => return false,
239            }
240        }
241
242        true
243    }
244}
245
246#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
247pub struct Config {
248    #[serde(default)]
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub file_extension: Option<String>,
251    #[serde(default)]
252    #[serde(skip_serializing_if = "Vec::is_empty")]
253    pub dirs: Vec<ReplaceDir>,
254}
255
256impl Config {
257    /// Insert the given file.
258    pub fn insert_file<'a>(
259        &'a mut self,
260        file_dir: &RelativePath,
261        file: RelativePathBuf,
262        transcript: Transcript,
263    ) -> Result<(), failure::Error> {
264        let mut found = None;
265
266        for (i, dir) in self.dirs.iter().enumerate() {
267            if dir.path == file_dir && dir.contains(&file) {
268                found = Some(i);
269                break;
270            }
271        }
272
273        let i = match found {
274            Some(i) => i,
275            None => {
276                let mut dir = ReplaceDir::new(file_dir.to_owned());
277                dir.files = Files::ListOfMaps(vec![]);
278
279                let len = self.dirs.len();
280                self.dirs.push(dir);
281                len
282            }
283        };
284
285        let Config {
286            ref mut dirs,
287            ref file_extension,
288            ..
289        } = *self;
290
291        dirs[i].insert_file(
292            file_extension.as_ref().map(|s| s.as_str()),
293            file,
294            transcript,
295        )?;
296        Ok(())
297    }
298
299    /// Optimize configuration.
300    pub fn optimize(&mut self) -> Result<(), failure::Error> {
301        self.dirs.sort();
302        Ok(())
303    }
304}