Skip to main content

tailwind_rs_scanner/
cache.rs

1//! Caching implementation for content scanning
2//!
3//! This module provides caching capabilities for efficient
4//! content scanning and file watching.
5
6use crate::class_extractor::ExtractedClass;
7use crate::error::{Result, ScannerError};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::time::{Duration, SystemTime};
12
13/// Scan cache for storing processed results
14#[derive(Debug)]
15pub struct ScanCache {
16    /// Cache entries
17    entries: HashMap<PathBuf, CacheEntry>,
18    /// Cache statistics
19    stats: CacheStats,
20}
21
22/// Cache entry for a file
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct CacheEntry {
25    /// File path
26    pub path: PathBuf,
27    /// Extracted classes
28    pub classes: Vec<ExtractedClass>,
29    /// File modification time
30    pub modified: SystemTime,
31    /// Cache creation time
32    pub created: SystemTime,
33    /// Cache TTL
34    pub ttl: Duration,
35}
36
37/// Cache statistics
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct CacheStats {
40    /// Total cache entries
41    pub total_entries: usize,
42    /// Cache hits
43    pub hits: usize,
44    /// Cache misses
45    pub misses: usize,
46    /// Cache size in bytes
47    pub size_bytes: usize,
48    /// Average entry size
49    pub average_entry_size: usize,
50}
51
52impl Default for CacheStats {
53    fn default() -> Self {
54        Self {
55            total_entries: 0,
56            hits: 0,
57            misses: 0,
58            size_bytes: 0,
59            average_entry_size: 0,
60        }
61    }
62}
63
64impl ScanCache {
65    /// Create a new scan cache
66    pub fn new() -> Self {
67        Self {
68            entries: HashMap::new(),
69            stats: CacheStats::default(),
70        }
71    }
72
73    /// Get cached classes for a file
74    pub fn get_file_classes(&mut self, path: &PathBuf) -> Option<&Vec<ExtractedClass>> {
75        if let Some(entry) = self.entries.get(path) {
76            if entry.is_valid() {
77                self.stats.hits += 1;
78                Some(&entry.classes)
79            } else {
80                self.stats.misses += 1;
81                None
82            }
83        } else {
84            self.stats.misses += 1;
85            None
86        }
87    }
88
89    /// Update file classes in cache
90    pub fn update_file(&mut self, path: PathBuf, classes: Vec<ExtractedClass>) {
91        let entry = CacheEntry {
92            path: path.clone(),
93            classes,
94            modified: SystemTime::now(),
95            created: SystemTime::now(),
96            ttl: Duration::from_secs(3600), // 1 hour default TTL
97        };
98
99        self.entries.insert(path, entry);
100        self.stats.total_entries = self.entries.len();
101    }
102
103    /// Check if file is cached and valid
104    pub fn is_cached(&self, path: &PathBuf) -> bool {
105        if let Some(entry) = self.entries.get(path) {
106            entry.is_valid()
107        } else {
108            false
109        }
110    }
111
112    /// Remove file from cache
113    pub fn remove_file(&mut self, path: &PathBuf) {
114        self.entries.remove(path);
115        self.stats.total_entries = self.entries.len();
116    }
117
118    /// Clear all cache entries
119    pub fn clear(&mut self) {
120        self.entries.clear();
121        self.stats = CacheStats::default();
122    }
123
124    /// Get cache statistics
125    pub fn get_stats(&self) -> CacheStats {
126        self.stats.clone()
127    }
128
129    /// Clean expired entries
130    pub fn clean_expired(&mut self) {
131        let now = SystemTime::now();
132        self.entries.retain(|_, entry| entry.is_valid_at(now));
133        self.stats.total_entries = self.entries.len();
134    }
135}
136
137impl CacheEntry {
138    /// Check if cache entry is valid
139    pub fn is_valid(&self) -> bool {
140        self.is_valid_at(SystemTime::now())
141    }
142
143    /// Check if cache entry is valid at a specific time
144    pub fn is_valid_at(&self, now: SystemTime) -> bool {
145        if let Ok(elapsed) = now.duration_since(self.created) {
146            elapsed < self.ttl
147        } else {
148            false
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::class_extractor::ClassContext;
157
158    #[test]
159    fn test_scan_cache_creation() {
160        let cache = ScanCache::new();
161        assert_eq!(cache.entries.len(), 0);
162    }
163
164    #[test]
165    fn test_cache_entry_creation() {
166        let path = PathBuf::from("test.rs");
167        let classes = vec![ExtractedClass {
168            class_name: "p-4".to_string(),
169            context: ClassContext::new(),
170            line: 1,
171            column: 1,
172        }];
173
174        let entry = CacheEntry {
175            path: path.clone(),
176            classes,
177            modified: SystemTime::now(),
178            created: SystemTime::now(),
179            ttl: Duration::from_secs(3600),
180        };
181
182        assert_eq!(entry.path, path);
183        assert!(entry.is_valid());
184    }
185
186    #[test]
187    fn test_cache_operations() {
188        let mut cache = ScanCache::new();
189        let path = PathBuf::from("test.rs");
190        let classes = vec![ExtractedClass {
191            class_name: "p-4".to_string(),
192            context: ClassContext::new(),
193            line: 1,
194            column: 1,
195        }];
196
197        // Add to cache
198        cache.update_file(path.clone(), classes);
199        assert!(cache.is_cached(&path));
200
201        // Get from cache
202        let cached_classes = cache.get_file_classes(&path);
203        assert!(cached_classes.is_some());
204
205        // Remove from cache
206        cache.remove_file(&path);
207        assert!(!cache.is_cached(&path));
208    }
209
210    #[test]
211    fn test_cache_stats() {
212        let cache = ScanCache::new();
213        let stats = cache.get_stats();
214        assert_eq!(stats.total_entries, 0);
215        assert_eq!(stats.hits, 0);
216        assert_eq!(stats.misses, 0);
217    }
218}