use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use directories::ProjectDirs;
use crate::cache::CacheMetadata;
use crate::models::{DocumentType, Format};
#[derive(Debug, Clone)]
pub struct CachedDocument {
pub doc_type: DocumentType,
pub metadata: Option<CacheMetadata>,
}
pub struct CacheManager {
cache_dir: PathBuf,
}
impl CacheManager {
pub fn new() -> Result<Self> {
let cache_dir = Self::default_cache_dir()?;
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
Ok(Self { cache_dir })
}
pub fn with_dir(cache_dir: PathBuf) -> Result<Self> {
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
Ok(Self { cache_dir })
}
pub fn default_cache_dir() -> Result<PathBuf> {
if let Some(proj_dirs) = ProjectDirs::from("", "", "rfc") {
Ok(proj_dirs.cache_dir().to_path_buf())
} else {
let home = std::env::var("HOME").context("HOME not set")?;
Ok(PathBuf::from(home).join(".cache").join("rfc"))
}
}
pub fn get_document(&self, doc: &DocumentType, format: Format) -> Option<String> {
let path = self.document_path(doc, format);
fs::read_to_string(path).ok()
}
pub fn store_document(&self, doc: &DocumentType, format: Format, content: &str) -> Result<()> {
let path = self.document_path(doc, format);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context("Failed to create document cache directory")?;
}
fs::write(&path, content).context("Failed to write document to cache")?;
Ok(())
}
pub fn clear_cache(&self) -> Result<()> {
if self.cache_dir.exists() {
fs::remove_dir_all(&self.cache_dir).context("Failed to clear cache")?;
fs::create_dir_all(&self.cache_dir).context("Failed to recreate cache directory")?;
}
Ok(())
}
pub fn remove(&self, doc: &DocumentType) -> Result<bool> {
let html_path = self.document_path(doc, Format::Html);
let text_path = self.document_path(doc, Format::Text);
let meta_path = self.metadata_path(doc);
let mut removed = false;
if html_path.exists() {
fs::remove_file(&html_path).context("Failed to remove cached HTML file")?;
removed = true;
}
if text_path.exists() {
fs::remove_file(&text_path).context("Failed to remove cached text file")?;
removed = true;
}
if meta_path.exists() {
fs::remove_file(&meta_path).context("Failed to remove cached metadata file")?;
}
Ok(removed)
}
pub fn list_cached(&self) -> Vec<DocumentType> {
let docs_dir = self.cache_dir.join("documents");
if !docs_dir.exists() {
return Vec::new();
}
let mut seen = HashSet::new();
let mut documents = Vec::new();
if let Ok(entries) = fs::read_dir(&docs_dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let doc_type = DocumentType::from_canonical_name(stem);
if seen.insert(doc_type.clone()) {
documents.push(doc_type);
}
}
}
}
documents
}
pub fn cache_dir(&self) -> &Path {
&self.cache_dir
}
fn document_path(&self, doc: &DocumentType, format: Format) -> PathBuf {
self.cache_dir
.join("documents")
.join(format!("{}.{}", doc.name(), format.extension()))
}
fn metadata_path(&self, doc: &DocumentType) -> PathBuf {
self.cache_dir
.join("documents")
.join(format!("{}.meta", doc.name()))
}
pub fn get_metadata(&self, doc: &DocumentType) -> Option<CacheMetadata> {
let path = self.metadata_path(doc);
let content = fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn store_metadata(&self, doc: &DocumentType, meta: &CacheMetadata) -> Result<()> {
let path = self.metadata_path(doc);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context("Failed to create metadata directory")?;
}
let content = serde_json::to_string_pretty(meta).context("Failed to serialize metadata")?;
fs::write(path, content).context("Failed to write metadata file")?;
Ok(())
}
pub fn list_cached_with_metadata(&self) -> Vec<CachedDocument> {
self.list_cached()
.into_iter()
.map(|doc_type| {
let metadata = self.get_metadata(&doc_type);
CachedDocument { doc_type, metadata }
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use tempfile::TempDir;
fn test_cache() -> (CacheManager, TempDir) {
let temp_dir = TempDir::new().unwrap();
let cache = CacheManager::with_dir(temp_dir.path().to_path_buf()).unwrap();
(cache, temp_dir)
}
#[test]
fn test_store_and_retrieve() {
let (cache, _temp) = test_cache();
let doc = DocumentType::Rfc(9000);
let content = "<html>Test content</html>";
cache.store_document(&doc, Format::Html, content).unwrap();
let retrieved = cache.get_document(&doc, Format::Html);
assert_eq!(retrieved, Some(content.to_string()));
}
#[test]
fn test_list_cached() {
let (cache, _temp) = test_cache();
cache
.store_document(&DocumentType::Rfc(9000), Format::Html, "test")
.unwrap();
cache
.store_document(&DocumentType::Rfc(8200), Format::Text, "test")
.unwrap();
let cached = cache.list_cached();
assert_eq!(cached.len(), 2);
}
#[test]
fn test_clear_cache() {
let (cache, _temp) = test_cache();
let doc = DocumentType::Rfc(9000);
cache.store_document(&doc, Format::Html, "test").unwrap();
assert!(cache.get_document(&doc, Format::Html).is_some());
cache.clear_cache().unwrap();
assert!(cache.get_document(&doc, Format::Html).is_none());
}
#[test]
fn test_remove_document() {
let (cache, _temp) = test_cache();
let doc = DocumentType::Rfc(9000);
assert!(!cache.remove(&doc).unwrap());
cache
.store_document(&doc, Format::Html, "html content")
.unwrap();
cache
.store_document(&doc, Format::Text, "text content")
.unwrap();
let meta = CacheMetadata {
title: "Test Title".to_string(),
cached_at: Utc::now(),
};
cache.store_metadata(&doc, &meta).unwrap();
assert!(cache.remove(&doc).unwrap());
assert!(cache.get_document(&doc, Format::Html).is_none());
assert!(cache.get_document(&doc, Format::Text).is_none());
assert!(cache.get_metadata(&doc).is_none());
assert!(!cache.remove(&doc).unwrap());
}
#[test]
fn test_remove_partial_formats() {
let (cache, _temp) = test_cache();
let doc = DocumentType::Rfc(8000);
cache
.store_document(&doc, Format::Html, "html only")
.unwrap();
assert!(cache.remove(&doc).unwrap());
assert!(cache.get_document(&doc, Format::Html).is_none());
}
#[test]
fn test_list_cached_with_drafts() {
let (cache, _temp) = test_cache();
let draft = DocumentType::Draft("draft-ietf-quic-transport-34".to_string());
cache.store_document(&draft, Format::Text, "test").unwrap();
let cached = cache.list_cached();
assert_eq!(cached.len(), 1);
assert!(cached.contains(&draft));
}
#[test]
fn test_store_and_retrieve_metadata() {
let (cache, _temp) = test_cache();
let doc = DocumentType::Rfc(9000);
let meta = CacheMetadata {
title: "QUIC: A UDP-Based Multiplexed and Secure Transport".to_string(),
cached_at: Utc::now(),
};
cache.store_metadata(&doc, &meta).unwrap();
let retrieved = cache.get_metadata(&doc);
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.title, meta.title);
}
#[test]
fn test_list_cached_with_metadata() {
let (cache, _temp) = test_cache();
let doc1 = DocumentType::Rfc(9000);
let doc2 = DocumentType::Rfc(8200);
cache.store_document(&doc1, Format::Text, "test").unwrap();
cache.store_document(&doc2, Format::Text, "test").unwrap();
let meta1 = CacheMetadata {
title: "QUIC Transport".to_string(),
cached_at: Utc::now(),
};
cache.store_metadata(&doc1, &meta1).unwrap();
let cached = cache.list_cached_with_metadata();
assert_eq!(cached.len(), 2);
let cached_doc1 = cached.iter().find(|cd| cd.doc_type == doc1).unwrap();
assert!(cached_doc1.metadata.is_some());
let cached_doc2 = cached.iter().find(|cd| cd.doc_type == doc2).unwrap();
assert!(cached_doc2.metadata.is_none());
}
#[test]
fn test_metadata_missing() {
let (cache, _temp) = test_cache();
let doc = DocumentType::Rfc(9000);
cache.store_document(&doc, Format::Text, "test").unwrap();
assert!(cache.get_metadata(&doc).is_none());
}
}