clnrm_core/cache/
memory_cache.rs

1//! In-memory cache implementation for testing
2//!
3//! Provides a fast, thread-safe cache that doesn't persist to disk.
4//! Ideal for unit tests and development workflows.
5
6use super::cache_trait::{Cache, CacheStats};
7use super::hash;
8use crate::error::{CleanroomError, Result};
9use chrono::Utc;
10use std::collections::HashMap;
11use std::path::Path;
12use std::sync::{Arc, Mutex};
13use tracing::debug;
14
15/// In-memory cache for testing and development
16///
17/// London School TDD Design:
18/// - Implements Cache trait for collaboration contract
19/// - Thread-safe with Arc<Mutex<>> for concurrent access
20/// - No persistence - perfect for testing
21/// - Fast operations without disk I/O
22///
23/// # Example
24/// ```
25/// use clnrm_core::cache::{MemoryCache, Cache};
26/// use std::path::Path;
27///
28/// # fn main() -> clnrm_core::Result<()> {
29/// let cache = MemoryCache::new();
30/// let file_path = Path::new("tests/api.clnrm.toml");
31/// let content = "rendered content";
32///
33/// if cache.has_changed(file_path, content)? {
34///     // Run test
35///     cache.update(file_path, content)?;
36/// }
37/// # Ok(())
38/// # }
39/// ```
40#[derive(Debug, Clone)]
41pub struct MemoryCache {
42    /// In-memory hash storage (thread-safe)
43    hashes: Arc<Mutex<HashMap<String, String>>>,
44}
45
46impl MemoryCache {
47    /// Create a new in-memory cache
48    pub fn new() -> Self {
49        Self {
50            hashes: Arc::new(Mutex::new(HashMap::new())),
51        }
52    }
53
54    /// Get the number of entries in cache (for testing)
55    ///
56    /// Returns 0 if lock acquisition fails (defensive fallback for testing utility)
57    pub fn len(&self) -> usize {
58        self.hashes.lock().map(|h| h.len()).unwrap_or_else(|e| {
59            // This should never happen in practice, but we provide a safe fallback
60            // rather than panicking. Log at debug level for visibility in tests.
61            debug!("Failed to acquire cache lock in len(): {}", e);
62            0
63        })
64    }
65
66    /// Check if cache is empty (for testing)
67    pub fn is_empty(&self) -> bool {
68        self.len() == 0
69    }
70}
71
72impl Cache for MemoryCache {
73    fn has_changed(&self, file_path: &Path, rendered_content: &str) -> Result<bool> {
74        let file_key = file_path
75            .to_str()
76            .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
77            .to_string();
78
79        // Calculate current hash
80        let current_hash = hash::hash_content(rendered_content)?;
81
82        // Check against cached hash
83        let hashes = self.hashes.lock().map_err(|e| {
84            CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
85        })?;
86
87        match hashes.get(&file_key) {
88            Some(cached_hash) if cached_hash == &current_hash => {
89                debug!("Memory cache hit: {} (unchanged)", file_key);
90                Ok(false)
91            }
92            Some(_) => {
93                debug!("Memory cache miss: {} (changed)", file_key);
94                Ok(true)
95            }
96            None => {
97                debug!("Memory cache miss: {} (new file)", file_key);
98                Ok(true)
99            }
100        }
101    }
102
103    fn update(&self, file_path: &Path, rendered_content: &str) -> Result<()> {
104        let file_key = file_path
105            .to_str()
106            .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
107            .to_string();
108
109        let hash = hash::hash_content(rendered_content)?;
110
111        let mut hashes = self.hashes.lock().map_err(|e| {
112            CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
113        })?;
114
115        hashes.insert(file_key.clone(), hash);
116        debug!("Memory cache updated: {}", file_key);
117
118        Ok(())
119    }
120
121    fn remove(&self, file_path: &Path) -> Result<()> {
122        let file_key = file_path
123            .to_str()
124            .ok_or_else(|| CleanroomError::validation_error("Invalid file path encoding"))?
125            .to_string();
126
127        let mut hashes = self.hashes.lock().map_err(|e| {
128            CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
129        })?;
130
131        if hashes.remove(&file_key).is_some() {
132            debug!("Removed from memory cache: {}", file_key);
133        }
134
135        Ok(())
136    }
137
138    fn save(&self) -> Result<()> {
139        // No-op for memory cache
140        Ok(())
141    }
142
143    fn stats(&self) -> Result<CacheStats> {
144        let hashes = self.hashes.lock().map_err(|e| {
145            CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
146        })?;
147
148        Ok(CacheStats {
149            total_files: hashes.len(),
150            last_updated: Utc::now(),
151            cache_path: None,
152        })
153    }
154
155    fn clear(&self) -> Result<()> {
156        let mut hashes = self.hashes.lock().map_err(|e| {
157            CleanroomError::internal_error(format!("Failed to acquire cache lock: {}", e))
158        })?;
159
160        hashes.clear();
161        debug!("Memory cache cleared");
162
163        Ok(())
164    }
165}
166
167impl Default for MemoryCache {
168    fn default() -> Self {
169        Self::new()
170    }
171}