qlty_analysis/
cache.rs

1use anyhow::Result;
2use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf};
3use tracing::{debug, error, trace};
4
5#[derive(Debug, Clone, Default)]
6pub struct HashDigest {
7    pub parts: HashMap<String, String>,
8    pub digest: Option<md5::Digest>,
9}
10
11pub trait CacheKey {
12    fn hexdigest(&self) -> String;
13}
14
15impl HashDigest {
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    pub fn add(&mut self, key: &str, value: &str) {
21        match self.digest {
22            Some(_) => panic!("Cannot add to finalized cache key"),
23            None => self.parts.insert(key.to_string(), value.to_string()),
24        };
25    }
26
27    pub fn finalize(&mut self) {
28        let mut context = md5::Context::new();
29
30        let sorted_keys = {
31            let mut keys: Vec<&String> = self.parts.keys().collect();
32            keys.sort();
33            keys
34        };
35
36        for key in sorted_keys {
37            let value = self.parts.get(key).unwrap();
38
39            context.consume(key.as_bytes());
40            context.consume(value.as_bytes());
41        }
42
43        self.digest = Some(context.compute());
44    }
45}
46
47impl CacheKey for HashDigest {
48    fn hexdigest(&self) -> String {
49        match &self.digest {
50            Some(digest) => format!("{:x}", digest),
51            None => panic!("Cannot get hexdigest of unfinalized cache key"),
52        }
53    }
54}
55
56pub trait Cache: Debug + Send + Sync + 'static {
57    fn read(&self, key: &dyn CacheKey) -> Result<Option<Vec<u8>>>;
58    fn write(&self, key: &dyn CacheKey, value: &[u8]) -> Result<()>;
59    fn path(&self, key: &dyn CacheKey) -> PathBuf;
60    fn clear(&self) -> Result<()>;
61    fn clone_box(&self) -> Box<dyn Cache>;
62}
63
64impl Clone for Box<dyn Cache> {
65    fn clone(&self) -> Box<dyn Cache> {
66        self.clone_box()
67    }
68}
69
70#[derive(Debug, Clone, Default)]
71pub struct NullCache {}
72
73impl Cache for NullCache {
74    fn read(&self, _key: &dyn CacheKey) -> Result<Option<Vec<u8>>> {
75        Ok(None)
76    }
77
78    fn write(&self, _key: &dyn CacheKey, _value: &[u8]) -> Result<()> {
79        Ok(())
80    }
81
82    fn path(&self, _key: &dyn CacheKey) -> PathBuf {
83        PathBuf::new()
84    }
85
86    fn clear(&self) -> Result<()> {
87        Ok(())
88    }
89
90    fn clone_box(&self) -> Box<dyn Cache> {
91        Box::new(self.clone())
92    }
93}
94
95impl NullCache {
96    pub fn new() -> Self {
97        Self::default()
98    }
99}
100
101#[derive(Debug, Clone)]
102pub struct FilesystemCache {
103    pub root: PathBuf,
104    pub extension: String,
105}
106
107impl FilesystemCache {
108    pub fn new(root: PathBuf, extension: &str) -> Self {
109        Self {
110            root,
111            extension: extension.to_string(),
112        }
113    }
114}
115
116impl Cache for FilesystemCache {
117    fn read(&self, key: &dyn CacheKey) -> Result<Option<Vec<u8>>> {
118        let path = self.path(key);
119
120        trace!("FilesystemCache read: {}", path.display());
121        let result = fs::read(path);
122
123        match result {
124            Ok(contents) => {
125                debug!("Cache hit: {:?}", &key.hexdigest());
126                Ok(Some(contents))
127            }
128            Err(error) => match error.kind() {
129                std::io::ErrorKind::NotFound => Ok(None),
130                _ => Err(error.into()),
131            },
132        }
133    }
134
135    fn write(&self, key: &dyn CacheKey, value: &[u8]) -> Result<()> {
136        let path = self.path(key);
137        let directory = path.parent();
138
139        if directory.is_some()
140            && !directory.unwrap().exists()
141            && std::fs::create_dir_all(directory.unwrap()).is_err()
142        {
143            error!(
144                "Failed to create directory: {}",
145                directory.unwrap().display()
146            );
147        }
148
149        trace!(
150            "FilesystemCache write: {} ({} bytes)",
151            path.display(),
152            value.len()
153        );
154        fs::write(path, value)?;
155        Ok(())
156    }
157
158    #[allow(unused)]
159    fn clear(&self) -> Result<()> {
160        fs::remove_dir_all(&self.root)?;
161        Ok(())
162    }
163
164    fn path(&self, key: &dyn CacheKey) -> PathBuf {
165        let mut path = self.root.clone();
166        path.push(key.hexdigest());
167        path.set_extension(&self.extension);
168        path
169    }
170
171    fn clone_box(&self) -> Box<dyn Cache> {
172        Box::new(self.clone())
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use super::*;
179
180    #[test]
181    fn cache_key_equal() {
182        let mut key_a = HashDigest::new();
183        key_a.add("foo", "bar");
184        key_a.finalize();
185
186        let mut key_b = HashDigest::new();
187        key_b.add("foo", "bar");
188        key_b.finalize();
189
190        assert_eq!(key_a.hexdigest(), key_b.hexdigest());
191    }
192
193    #[test]
194    fn cache_key_not_equal() {
195        let mut key_a = HashDigest::new();
196        key_a.add("foo", "bar");
197        key_a.finalize();
198
199        let mut key_b = HashDigest::new();
200        key_b.add("foo", "bar");
201        key_b.add("baz", "bop");
202        key_b.finalize();
203
204        assert_ne!(key_a.hexdigest(), key_b.hexdigest());
205    }
206
207    #[test]
208    fn filesystem_cache() {
209        let tmpdir = tempfile::tempdir().unwrap();
210        let cache = FilesystemCache::new(tmpdir.into_path(), "bytes");
211
212        let mut digest = HashDigest::new();
213        digest.add("foo", "bar");
214        digest.finalize();
215
216        cache.write(&digest, "Test 123".as_bytes()).unwrap();
217        assert_eq!("Test 123".as_bytes(), cache.read(&digest).unwrap().unwrap());
218    }
219}