mithril_common/digesters/cache/
json_provider.rs1use crate::{
2 digesters::{
3 cache::provider::{ImmutableDigesterCacheGetError, ImmutableDigesterCacheStoreError},
4 cache::CacheProviderResult,
5 cache::ImmutableFileDigestCacheProvider,
6 ImmutableFile,
7 },
8 entities::{HexEncodedDigest, ImmutableFileName},
9};
10
11use async_trait::async_trait;
12use std::{
13 collections::BTreeMap,
14 path::{Path, PathBuf},
15};
16#[cfg(feature = "fs")]
17use tokio::{
18 fs,
19 fs::File,
20 io::{AsyncReadExt, AsyncWriteExt},
21};
22
23type InnerStructure = BTreeMap<ImmutableFileName, HexEncodedDigest>;
24
25pub struct JsonImmutableFileDigestCacheProvider {
27 filepath: PathBuf,
28}
29
30impl JsonImmutableFileDigestCacheProvider {
31 pub fn new(filepath: &Path) -> Self {
33 Self {
34 filepath: filepath.to_path_buf(),
35 }
36 }
37
38 #[cfg(test)]
39 pub async fn from(filepath: &Path, values: InnerStructure) -> Self {
41 let provider = Self::new(filepath);
42 provider.write_data(values).await.unwrap();
43 provider
44 }
45
46 async fn write_data(
47 &self,
48 values: InnerStructure,
49 ) -> Result<(), ImmutableDigesterCacheStoreError> {
50 let mut file = File::create(&self.filepath).await?;
51 file.write_all(serde_json::to_string_pretty(&values)?.as_bytes())
52 .await?;
53
54 Ok(())
55 }
56
57 async fn read_data(&self) -> Result<InnerStructure, ImmutableDigesterCacheGetError> {
58 match self.filepath.exists() {
59 true => {
60 let mut file = File::open(&self.filepath).await?;
61 let mut json_string = String::new();
62 file.read_to_string(&mut json_string).await?;
63 let values: InnerStructure = serde_json::from_str(&json_string)?;
64 Ok(values)
65 }
66 false => Ok(BTreeMap::new()),
67 }
68 }
69}
70
71#[async_trait]
72impl ImmutableFileDigestCacheProvider for JsonImmutableFileDigestCacheProvider {
73 async fn store(
74 &self,
75 digest_per_filenames: Vec<(ImmutableFileName, HexEncodedDigest)>,
76 ) -> CacheProviderResult<()> {
77 let mut data = self.read_data().await?;
78 for (filename, digest) in digest_per_filenames {
79 data.insert(filename, digest);
80 }
81 self.write_data(data).await?;
82
83 Ok(())
84 }
85
86 async fn get(
87 &self,
88 immutables: Vec<ImmutableFile>,
89 ) -> CacheProviderResult<BTreeMap<ImmutableFile, Option<HexEncodedDigest>>> {
90 let values = self.read_data().await?;
91 let mut result = BTreeMap::new();
92
93 for immutable in immutables {
94 let value = values.get(&immutable.filename).map(|f| f.to_owned());
95 result.insert(immutable, value);
96 }
97
98 Ok(result)
99 }
100
101 async fn reset(&self) -> CacheProviderResult<()> {
102 fs::remove_file(&self.filepath)
103 .await
104 .map_err(ImmutableDigesterCacheStoreError::from)?;
105
106 Ok(())
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use crate::digesters::cache::{
113 ImmutableFileDigestCacheProvider, JsonImmutableFileDigestCacheProvider,
114 };
115 use crate::digesters::ImmutableFile;
116 use crate::test_utils::TempDir;
117 use std::{collections::BTreeMap, path::PathBuf};
118
119 fn get_test_dir(subdir_name: &str) -> PathBuf {
120 TempDir::create("json_digester_cache_provider", subdir_name)
121 }
122
123 #[tokio::test]
124 async fn can_store_values() {
125 let file = get_test_dir("can_store_values").join("immutable-cache-store.json");
126 let provider = JsonImmutableFileDigestCacheProvider::new(&file);
127 let values_to_store = vec![
128 ("0.chunk".to_string(), "digest 0".to_string()),
129 ("1.chunk".to_string(), "digest 1".to_string()),
130 ];
131 let expected: BTreeMap<_, _> = BTreeMap::from([
132 (
133 ImmutableFile::dummy(PathBuf::default(), 0, "0.chunk"),
134 Some("digest 0".to_string()),
135 ),
136 (
137 ImmutableFile::dummy(PathBuf::default(), 1, "1.chunk"),
138 Some("digest 1".to_string()),
139 ),
140 ]);
141 let immutables = expected.keys().cloned().collect();
142
143 provider
144 .store(values_to_store)
145 .await
146 .expect("Cache write should not fail");
147 let result = provider
148 .get(immutables)
149 .await
150 .expect("Cache read should not fail");
151
152 assert_eq!(expected, result);
153 }
154
155 #[tokio::test]
156 async fn returns_only_asked_immutables_cache() {
157 let file =
158 get_test_dir("returns_only_asked_immutables_cache").join("immutable-cache-store.json");
159 let provider = JsonImmutableFileDigestCacheProvider::from(
160 &file,
161 BTreeMap::from([
162 ("0.chunk".to_string(), "digest 0".to_string()),
163 ("1.chunk".to_string(), "digest 1".to_string()),
164 ]),
165 )
166 .await;
167 let expected: BTreeMap<_, _> = BTreeMap::from([(
168 ImmutableFile::dummy(PathBuf::default(), 0, "0.chunk"),
169 Some("digest 0".to_string()),
170 )]);
171 let immutables = expected.keys().cloned().collect();
172
173 let result = provider
174 .get(immutables)
175 .await
176 .expect("Cache read should not fail");
177
178 assert_eq!(expected, result);
179 }
180
181 #[tokio::test]
182 async fn returns_none_for_uncached_asked_immutables() {
183 let file = get_test_dir("returns_none_for_uncached_asked_immutables")
184 .join("immutable-cache-store.json");
185 let provider = JsonImmutableFileDigestCacheProvider::from(
186 &file,
187 BTreeMap::from([("0.chunk".to_string(), "digest 0".to_string())]),
188 )
189 .await;
190 let expected: BTreeMap<_, _> =
191 BTreeMap::from([(ImmutableFile::dummy(PathBuf::default(), 2, "2.chunk"), None)]);
192 let immutables = expected.keys().cloned().collect();
193
194 let result = provider
195 .get(immutables)
196 .await
197 .expect("Cache read should not fail");
198
199 assert_eq!(expected, result);
200 }
201
202 #[tokio::test]
203 async fn store_erase_existing_values() {
204 let file = get_test_dir("store_erase_existing_values").join("immutable-cache-store.json");
205 let provider = JsonImmutableFileDigestCacheProvider::from(
206 &file,
207 BTreeMap::from([
208 ("0.chunk".to_string(), "to erase".to_string()),
209 ("1.chunk".to_string(), "keep me".to_string()),
210 ("2.chunk".to_string(), "keep me too".to_string()),
211 ]),
212 )
213 .await;
214 let values_to_store = vec![
215 ("0.chunk".to_string(), "updated".to_string()),
216 ("1.chunk".to_string(), "keep me".to_string()),
217 ];
218 let expected: BTreeMap<_, _> = BTreeMap::from([
219 (
220 ImmutableFile::dummy(PathBuf::default(), 0, "0.chunk"),
221 Some("updated".to_string()),
222 ),
223 (
224 ImmutableFile::dummy(PathBuf::default(), 1, "1.chunk"),
225 Some("keep me".to_string()),
226 ),
227 (
228 ImmutableFile::dummy(PathBuf::default(), 2, "2.chunk"),
229 Some("keep me too".to_string()),
230 ),
231 (ImmutableFile::dummy(PathBuf::default(), 3, "3.chunk"), None),
232 ]);
233 let immutables = expected.keys().cloned().collect();
234
235 provider
236 .store(values_to_store)
237 .await
238 .expect("Cache write should not fail");
239 let result = provider
240 .get(immutables)
241 .await
242 .expect("Cache read should not fail");
243
244 assert_eq!(expected, result);
245 }
246
247 #[tokio::test]
248 async fn reset_clear_existing_values() {
249 let file = get_test_dir("reset_clear_existing_values").join("immutable-cache-store.json");
250 let provider = JsonImmutableFileDigestCacheProvider::new(&file);
251 let values_to_store = vec![
252 ("0.chunk".to_string(), "digest 0".to_string()),
253 ("1.chunk".to_string(), "digest 1".to_string()),
254 ];
255 let expected: BTreeMap<_, _> = BTreeMap::from([
256 (
257 ImmutableFile::dummy(PathBuf::default(), 0, "0.chunk"),
258 Some("digest 0".to_string()),
259 ),
260 (
261 ImmutableFile::dummy(PathBuf::default(), 1, "1.chunk"),
262 Some("digest 1".to_string()),
263 ),
264 ]);
265 let immutables = expected.keys().cloned().collect();
266
267 provider
268 .store(values_to_store)
269 .await
270 .expect("Cache write should not fail");
271 provider.reset().await.expect("reset should not fails");
272
273 let result: BTreeMap<_, _> = provider
274 .get(immutables)
275 .await
276 .expect("Cache read should not fail");
277
278 assert!(result.into_iter().all(|(_, cache)| cache.is_none()));
279 }
280}