vika_cli/
cache.rs

1use crate::error::{FileSystemError, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6const CACHE_DIR: &str = ".vika-cache";
7
8/// Generate a cache key from spec path and optional spec name
9fn cache_key(spec_path: &str, spec_name: Option<&str>) -> String {
10    if let Some(name) = spec_name {
11        format!("{}:{}", name, spec_path)
12    } else {
13        spec_path.to_string()
14    }
15}
16
17/// Generate cache file names from cache key
18fn cache_file_names(cache_key: &str) -> (String, String) {
19    use std::collections::hash_map::DefaultHasher;
20    use std::hash::{Hash, Hasher};
21    let mut hasher = DefaultHasher::new();
22    cache_key.hash(&mut hasher);
23    let hash = format!("{:x}", hasher.finish());
24    (
25        format!("spec_{}.json", hash),
26        format!("spec_{}.meta.json", hash),
27    )
28}
29
30#[derive(Debug, Serialize, Deserialize)]
31pub struct SpecMetadata {
32    pub url: String,
33    pub timestamp: u64,
34    pub etag: Option<String>,
35    pub content_hash: String,
36}
37
38pub struct CacheManager;
39
40impl CacheManager {
41    pub fn ensure_cache_dir() -> Result<PathBuf> {
42        let cache_dir = PathBuf::from(CACHE_DIR);
43        // create_dir_all succeeds if directory already exists
44        std::fs::create_dir_all(&cache_dir).map_err(|e| {
45            FileSystemError::CreateDirectoryFailed {
46                path: CACHE_DIR.to_string(),
47                source: e,
48            }
49        })?;
50        Ok(cache_dir)
51    }
52
53    pub fn get_cached_spec(url: &str) -> Result<Option<String>> {
54        Self::get_cached_spec_with_name(url, None)
55    }
56
57    pub fn get_cached_spec_with_name(url: &str, spec_name: Option<&str>) -> Result<Option<String>> {
58        let cache_dir = Self::ensure_cache_dir()?;
59        let key = cache_key(url, spec_name);
60        let (spec_file, meta_file) = cache_file_names(&key);
61        let meta_path = cache_dir.join(&meta_file);
62        let spec_path = cache_dir.join(&spec_file);
63
64        // Check if cache exists
65        if !meta_path.exists() || !spec_path.exists() {
66            return Ok(None);
67        }
68
69        // Read metadata
70        let meta_content =
71            std::fs::read_to_string(&meta_path).map_err(|e| FileSystemError::ReadFileFailed {
72                path: meta_path.display().to_string(),
73                source: e,
74            })?;
75
76        let metadata: SpecMetadata =
77            serde_json::from_str(&meta_content).map_err(|_| FileSystemError::ReadFileFailed {
78                path: meta_path.display().to_string(),
79                source: std::io::Error::new(
80                    std::io::ErrorKind::InvalidData,
81                    "Invalid metadata format",
82                ),
83            })?;
84
85        // Check if URL matches
86        if metadata.url != url {
87            return Ok(None);
88        }
89
90        // Read cached spec
91        let spec_content =
92            std::fs::read_to_string(&spec_path).map_err(|e| FileSystemError::ReadFileFailed {
93                path: spec_path.display().to_string(),
94                source: e,
95            })?;
96
97        Ok(Some(spec_content))
98    }
99
100    pub fn cache_spec(url: &str, content: &str) -> Result<()> {
101        Self::cache_spec_with_name(url, content, None)
102    }
103
104    pub fn cache_spec_with_name(url: &str, content: &str, spec_name: Option<&str>) -> Result<()> {
105        // Ensure cache directory exists before writing
106        let cache_dir = Self::ensure_cache_dir()?;
107        let key = cache_key(url, spec_name);
108        let (spec_file, meta_file) = cache_file_names(&key);
109        let meta_path = cache_dir.join(&meta_file);
110        let spec_path = cache_dir.join(&spec_file);
111
112        // Compute content hash (simple hash for now)
113        use std::collections::hash_map::DefaultHasher;
114        use std::hash::{Hash, Hasher};
115        let mut hasher = DefaultHasher::new();
116        content.hash(&mut hasher);
117        let content_hash = format!("{:x}", hasher.finish());
118
119        // Get timestamp
120        let timestamp = SystemTime::now()
121            .duration_since(UNIX_EPOCH)
122            .unwrap()
123            .as_secs();
124
125        // Create metadata
126        let metadata = SpecMetadata {
127            url: url.to_string(),
128            timestamp,
129            etag: None, // Could be enhanced to fetch ETag from response
130            content_hash,
131        };
132
133        // Write metadata
134        let meta_json = serde_json::to_string_pretty(&metadata).map_err(|e| {
135            FileSystemError::WriteFileFailed {
136                path: meta_path.display().to_string(),
137                source: std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{}", e)),
138            }
139        })?;
140
141        std::fs::write(&meta_path, meta_json).map_err(|e| FileSystemError::WriteFileFailed {
142            path: meta_path.display().to_string(),
143            source: e,
144        })?;
145
146        // Write spec content
147        std::fs::write(&spec_path, content).map_err(|e| FileSystemError::WriteFileFailed {
148            path: spec_path.display().to_string(),
149            source: e,
150        })?;
151
152        Ok(())
153    }
154
155    pub fn clear_cache() -> Result<()> {
156        let cache_dir = PathBuf::from(CACHE_DIR);
157        if cache_dir.exists() {
158            std::fs::remove_dir_all(&cache_dir).map_err(|e| {
159                FileSystemError::CreateDirectoryFailed {
160                    path: CACHE_DIR.to_string(),
161                    source: e,
162                }
163            })?;
164        }
165        Ok(())
166    }
167}