1use crate::error::SpecError;
7use crate::models::Spec;
8use ricecoder_storage::{CacheInvalidationStrategy, CacheManager};
9use std::path::Path;
10use std::sync::Arc;
11use tracing::{debug, info};
12
13pub struct SpecCache {
18 cache: Arc<CacheManager>,
19 ttl_seconds: u64,
20}
21
22impl SpecCache {
23 pub fn new(cache_dir: impl AsRef<Path>, ttl_seconds: u64) -> Result<Self, SpecError> {
34 let cache = CacheManager::new(cache_dir)
35 .map_err(|e| SpecError::InvalidFormat(format!("Failed to create cache: {}", e)))?;
36
37 Ok(Self {
38 cache: Arc::new(cache),
39 ttl_seconds,
40 })
41 }
42
43 pub fn get(&self, spec_path: &Path) -> Result<Option<Spec>, SpecError> {
53 let cache_key = self.make_cache_key(spec_path);
54
55 if let Ok(metadata) = std::fs::metadata(spec_path) {
57 if let Ok(_modified) = metadata.modified() {
58 if let Ok(Some(_)) = self.cache.get(&cache_key) {
60 }
63 }
64 }
65
66 match self.cache.get(&cache_key) {
67 Ok(Some(cached_json_str)) => {
68 match serde_json::from_str::<Spec>(&cached_json_str) {
69 Ok(spec) => {
70 debug!("Cache hit for spec: {}", spec_path.display());
71 Ok(Some(spec))
72 }
73 Err(e) => {
74 debug!("Failed to deserialize cached spec: {}", e);
75 let _ = self.cache.invalidate(&cache_key);
77 Ok(None)
78 }
79 }
80 }
81 Ok(None) => {
82 debug!("Cache miss for spec: {}", spec_path.display());
83 Ok(None)
84 }
85 Err(e) => {
86 debug!("Cache lookup error: {}", e);
87 Ok(None)
88 }
89 }
90 }
91
92 pub fn set(&self, spec_path: &Path, spec: &Spec) -> Result<(), SpecError> {
103 let cache_key = self.make_cache_key(spec_path);
104
105 let spec_json = serde_json::to_string(spec)
106 .map_err(|e| SpecError::InvalidFormat(format!("Failed to serialize spec: {}", e)))?;
107
108 let json_len = spec_json.len();
109
110 self.cache
111 .set(
112 &cache_key,
113 spec_json,
114 CacheInvalidationStrategy::Ttl(self.ttl_seconds),
115 )
116 .map_err(|e| SpecError::InvalidFormat(format!("Failed to cache spec: {}", e)))?;
117
118 debug!(
119 "Cached spec: {} ({} bytes)",
120 spec_path.display(),
121 json_len
122 );
123
124 Ok(())
125 }
126
127 pub fn invalidate(&self, spec_path: &Path) -> Result<bool, SpecError> {
137 let cache_key = self.make_cache_key(spec_path);
138
139 self.cache
140 .invalidate(&cache_key)
141 .map_err(|e| SpecError::InvalidFormat(format!("Failed to invalidate cache: {}", e)))
142 }
143
144 pub fn clear(&self) -> Result<(), SpecError> {
150 self.cache
151 .clear()
152 .map_err(|e| SpecError::InvalidFormat(format!("Failed to clear cache: {}", e)))
153 }
154
155 pub fn cleanup_expired(&self) -> Result<usize, SpecError> {
161 let cleaned = self
162 .cache
163 .cleanup_expired()
164 .map_err(|e| SpecError::InvalidFormat(format!("Failed to cleanup cache: {}", e)))?;
165
166 if cleaned > 0 {
167 info!("Cleaned up {} expired spec cache entries", cleaned);
168 }
169
170 Ok(cleaned)
171 }
172
173 fn make_cache_key(&self, spec_path: &Path) -> String {
175 let path_str = spec_path.to_string_lossy();
176 let sanitized = path_str
177 .chars()
178 .map(|c| {
179 if c.is_alphanumeric() || c == '_' || c == '-' || c == '.' {
180 c
181 } else {
182 '_'
183 }
184 })
185 .collect::<String>();
186
187 format!("spec_{}", sanitized)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::models::{SpecMetadata, SpecPhase, SpecStatus};
195 use chrono::Utc;
196 use std::path::PathBuf;
197 use tempfile::TempDir;
198
199 fn create_test_spec() -> Spec {
200 Spec {
201 id: "test-spec".to_string(),
202 name: "test".to_string(),
203 version: "1.0.0".to_string(),
204 requirements: vec![],
205 design: None,
206 tasks: vec![],
207 metadata: SpecMetadata {
208 author: None,
209 created_at: Utc::now(),
210 updated_at: Utc::now(),
211 phase: SpecPhase::Tasks,
212 status: SpecStatus::Approved,
213 },
214 inheritance: None,
215 }
216 }
217
218 #[test]
219 fn test_cache_set_and_get() -> Result<(), SpecError> {
220 let temp_dir = TempDir::new().unwrap();
221 let cache = SpecCache::new(temp_dir.path(), 3600)?;
222
223 let spec_path = PathBuf::from("test_spec.yaml");
224 let spec = create_test_spec();
225
226 cache.set(&spec_path, &spec)?;
228
229 let cached = cache.get(&spec_path)?;
231 assert!(cached.is_some());
232 assert_eq!(cached.unwrap().name, "test");
233
234 Ok(())
235 }
236
237 #[test]
238 fn test_cache_miss() -> Result<(), SpecError> {
239 let temp_dir = TempDir::new().unwrap();
240 let cache = SpecCache::new(temp_dir.path(), 3600)?;
241
242 let spec_path = PathBuf::from("nonexistent_spec.yaml");
243
244 let cached = cache.get(&spec_path)?;
246 assert!(cached.is_none());
247
248 Ok(())
249 }
250
251 #[test]
252 fn test_cache_invalidate() -> Result<(), SpecError> {
253 let temp_dir = TempDir::new().unwrap();
254 let cache = SpecCache::new(temp_dir.path(), 3600)?;
255
256 let spec_path = PathBuf::from("test_spec.yaml");
257 let spec = create_test_spec();
258
259 cache.set(&spec_path, &spec)?;
261
262 let invalidated = cache.invalidate(&spec_path)?;
264 assert!(invalidated);
265
266 let cached = cache.get(&spec_path)?;
268 assert!(cached.is_none());
269
270 Ok(())
271 }
272
273 #[test]
274 fn test_cache_clear() -> Result<(), SpecError> {
275 let temp_dir = TempDir::new().unwrap();
276 let cache = SpecCache::new(temp_dir.path(), 3600)?;
277
278 let spec_path1 = PathBuf::from("spec1.yaml");
279 let spec_path2 = PathBuf::from("spec2.yaml");
280 let spec = create_test_spec();
281
282 cache.set(&spec_path1, &spec)?;
284 cache.set(&spec_path2, &spec)?;
285
286 cache.clear()?;
288
289 assert!(cache.get(&spec_path1)?.is_none());
291 assert!(cache.get(&spec_path2)?.is_none());
292
293 Ok(())
294 }
295}