1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use directories::ProjectDirs;
7
8use crate::cache::CacheMetadata;
9use crate::models::{DocumentType, Format};
10
11#[derive(Debug, Clone)]
13pub struct CachedDocument {
14 pub doc_type: DocumentType,
15 pub metadata: Option<CacheMetadata>,
16}
17
18pub struct CacheManager {
20 cache_dir: PathBuf,
21}
22
23impl CacheManager {
24 pub fn new() -> Result<Self> {
26 let cache_dir = Self::default_cache_dir()?;
27 fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
28 Ok(Self { cache_dir })
29 }
30
31 pub fn with_dir(cache_dir: PathBuf) -> Result<Self> {
33 fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
34 Ok(Self { cache_dir })
35 }
36
37 pub fn default_cache_dir() -> Result<PathBuf> {
39 if let Some(proj_dirs) = ProjectDirs::from("", "", "rfc") {
40 Ok(proj_dirs.cache_dir().to_path_buf())
41 } else {
42 let home = std::env::var("HOME").context("HOME not set")?;
44 Ok(PathBuf::from(home).join(".cache").join("rfc"))
45 }
46 }
47
48 pub fn get_document(&self, doc: &DocumentType, format: Format) -> Option<String> {
50 let path = self.document_path(doc, format);
51 fs::read_to_string(path).ok()
52 }
53
54 pub fn store_document(&self, doc: &DocumentType, format: Format, content: &str) -> Result<()> {
56 let path = self.document_path(doc, format);
57
58 if let Some(parent) = path.parent() {
60 fs::create_dir_all(parent).context("Failed to create document cache directory")?;
61 }
62
63 fs::write(&path, content).context("Failed to write document to cache")?;
64 Ok(())
65 }
66
67 pub fn clear_cache(&self) -> Result<()> {
69 if self.cache_dir.exists() {
70 fs::remove_dir_all(&self.cache_dir).context("Failed to clear cache")?;
71 fs::create_dir_all(&self.cache_dir).context("Failed to recreate cache directory")?;
72 }
73 Ok(())
74 }
75
76 pub fn remove(&self, doc: &DocumentType) -> Result<bool> {
80 let html_path = self.document_path(doc, Format::Html);
81 let text_path = self.document_path(doc, Format::Text);
82 let meta_path = self.metadata_path(doc);
83
84 let mut removed = false;
85
86 if html_path.exists() {
87 fs::remove_file(&html_path).context("Failed to remove cached HTML file")?;
88 removed = true;
89 }
90
91 if text_path.exists() {
92 fs::remove_file(&text_path).context("Failed to remove cached text file")?;
93 removed = true;
94 }
95
96 if meta_path.exists() {
97 fs::remove_file(&meta_path).context("Failed to remove cached metadata file")?;
98 }
99
100 Ok(removed)
101 }
102
103 pub fn list_cached(&self) -> Vec<DocumentType> {
105 let docs_dir = self.cache_dir.join("documents");
106 if !docs_dir.exists() {
107 return Vec::new();
108 }
109
110 let mut seen = HashSet::new();
111 let mut documents = Vec::new();
112
113 if let Ok(entries) = fs::read_dir(&docs_dir) {
114 for entry in entries.flatten() {
115 let path = entry.path();
116 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
117 let doc_type = DocumentType::from_canonical_name(stem);
118 if seen.insert(doc_type.clone()) {
119 documents.push(doc_type);
120 }
121 }
122 }
123 }
124
125 documents
126 }
127
128 pub fn cache_dir(&self) -> &Path {
130 &self.cache_dir
131 }
132
133 fn document_path(&self, doc: &DocumentType, format: Format) -> PathBuf {
135 self.cache_dir
136 .join("documents")
137 .join(format!("{}.{}", doc.name(), format.extension()))
138 }
139
140 fn metadata_path(&self, doc: &DocumentType) -> PathBuf {
142 self.cache_dir
143 .join("documents")
144 .join(format!("{}.meta", doc.name()))
145 }
146
147 pub fn get_metadata(&self, doc: &DocumentType) -> Option<CacheMetadata> {
149 let path = self.metadata_path(doc);
150 let content = fs::read_to_string(path).ok()?;
151 serde_json::from_str(&content).ok()
152 }
153
154 pub fn store_metadata(&self, doc: &DocumentType, meta: &CacheMetadata) -> Result<()> {
156 let path = self.metadata_path(doc);
157 if let Some(parent) = path.parent() {
158 fs::create_dir_all(parent).context("Failed to create metadata directory")?;
159 }
160 let content = serde_json::to_string_pretty(meta).context("Failed to serialize metadata")?;
161 fs::write(path, content).context("Failed to write metadata file")?;
162 Ok(())
163 }
164
165 pub fn list_cached_with_metadata(&self) -> Vec<CachedDocument> {
167 self.list_cached()
168 .into_iter()
169 .map(|doc_type| {
170 let metadata = self.get_metadata(&doc_type);
171 CachedDocument { doc_type, metadata }
172 })
173 .collect()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use chrono::Utc;
181 use tempfile::TempDir;
182
183 fn test_cache() -> (CacheManager, TempDir) {
184 let temp_dir = TempDir::new().unwrap();
185 let cache = CacheManager::with_dir(temp_dir.path().to_path_buf()).unwrap();
186 (cache, temp_dir)
187 }
188
189 #[test]
190 fn test_store_and_retrieve() {
191 let (cache, _temp) = test_cache();
192 let doc = DocumentType::Rfc(9000);
193 let content = "<html>Test content</html>";
194
195 cache.store_document(&doc, Format::Html, content).unwrap();
196
197 let retrieved = cache.get_document(&doc, Format::Html);
198 assert_eq!(retrieved, Some(content.to_string()));
199 }
200
201 #[test]
202 fn test_list_cached() {
203 let (cache, _temp) = test_cache();
204
205 cache
206 .store_document(&DocumentType::Rfc(9000), Format::Html, "test")
207 .unwrap();
208 cache
209 .store_document(&DocumentType::Rfc(8200), Format::Text, "test")
210 .unwrap();
211
212 let cached = cache.list_cached();
213 assert_eq!(cached.len(), 2);
214 }
215
216 #[test]
217 fn test_clear_cache() {
218 let (cache, _temp) = test_cache();
219 let doc = DocumentType::Rfc(9000);
220
221 cache.store_document(&doc, Format::Html, "test").unwrap();
222 assert!(cache.get_document(&doc, Format::Html).is_some());
223
224 cache.clear_cache().unwrap();
225 assert!(cache.get_document(&doc, Format::Html).is_none());
226 }
227
228 #[test]
229 fn test_remove_document() {
230 let (cache, _temp) = test_cache();
231 let doc = DocumentType::Rfc(9000);
232
233 assert!(!cache.remove(&doc).unwrap());
235
236 cache
238 .store_document(&doc, Format::Html, "html content")
239 .unwrap();
240 cache
241 .store_document(&doc, Format::Text, "text content")
242 .unwrap();
243
244 let meta = CacheMetadata {
245 title: "Test Title".to_string(),
246 cached_at: Utc::now(),
247 };
248 cache.store_metadata(&doc, &meta).unwrap();
249
250 assert!(cache.remove(&doc).unwrap());
251
252 assert!(cache.get_document(&doc, Format::Html).is_none());
254 assert!(cache.get_document(&doc, Format::Text).is_none());
255 assert!(cache.get_metadata(&doc).is_none());
256
257 assert!(!cache.remove(&doc).unwrap());
259 }
260
261 #[test]
262 fn test_remove_partial_formats() {
263 let (cache, _temp) = test_cache();
264 let doc = DocumentType::Rfc(8000);
265
266 cache
268 .store_document(&doc, Format::Html, "html only")
269 .unwrap();
270
271 assert!(cache.remove(&doc).unwrap());
273 assert!(cache.get_document(&doc, Format::Html).is_none());
274 }
275
276 #[test]
277 fn test_list_cached_with_drafts() {
278 let (cache, _temp) = test_cache();
279
280 let draft = DocumentType::Draft("draft-ietf-quic-transport-34".to_string());
281 cache.store_document(&draft, Format::Text, "test").unwrap();
282
283 let cached = cache.list_cached();
284 assert_eq!(cached.len(), 1);
285 assert!(cached.contains(&draft));
286 }
287
288 #[test]
289 fn test_store_and_retrieve_metadata() {
290 let (cache, _temp) = test_cache();
291 let doc = DocumentType::Rfc(9000);
292 let meta = CacheMetadata {
293 title: "QUIC: A UDP-Based Multiplexed and Secure Transport".to_string(),
294 cached_at: Utc::now(),
295 };
296
297 cache.store_metadata(&doc, &meta).unwrap();
298
299 let retrieved = cache.get_metadata(&doc);
300 assert!(retrieved.is_some());
301 let retrieved = retrieved.unwrap();
302 assert_eq!(retrieved.title, meta.title);
303 }
304
305 #[test]
306 fn test_list_cached_with_metadata() {
307 let (cache, _temp) = test_cache();
308
309 let doc1 = DocumentType::Rfc(9000);
310 let doc2 = DocumentType::Rfc(8200);
311
312 cache.store_document(&doc1, Format::Text, "test").unwrap();
313 cache.store_document(&doc2, Format::Text, "test").unwrap();
314
315 let meta1 = CacheMetadata {
316 title: "QUIC Transport".to_string(),
317 cached_at: Utc::now(),
318 };
319 cache.store_metadata(&doc1, &meta1).unwrap();
320
321 let cached = cache.list_cached_with_metadata();
322 assert_eq!(cached.len(), 2);
323
324 let cached_doc1 = cached.iter().find(|cd| cd.doc_type == doc1).unwrap();
326 assert!(cached_doc1.metadata.is_some());
327
328 let cached_doc2 = cached.iter().find(|cd| cd.doc_type == doc2).unwrap();
330 assert!(cached_doc2.metadata.is_none());
331 }
332
333 #[test]
334 fn test_metadata_missing() {
335 let (cache, _temp) = test_cache();
336 let doc = DocumentType::Rfc(9000);
337
338 cache.store_document(&doc, Format::Text, "test").unwrap();
339
340 assert!(cache.get_metadata(&doc).is_none());
342 }
343}