use crate::error::{CueEngineError, Result};
use lru::LruCache;
use parking_lot::RwLock;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct CacheKey {
path: PathBuf,
package_name: String,
}
#[derive(Clone, Debug)]
struct CacheEntry {
value: String,
timestamp: Instant,
}
#[derive(Debug)]
pub struct EvaluationCache {
cache: RwLock<LruCache<CacheKey, CacheEntry>>,
ttl: Duration,
}
impl EvaluationCache {
pub fn new(capacity: usize, ttl: Duration) -> Result<Self> {
let capacity = NonZeroUsize::new(capacity)
.ok_or_else(|| CueEngineError::cache("Cache capacity must be non-zero"))?;
Ok(Self {
cache: RwLock::new(LruCache::new(capacity)),
ttl,
})
}
pub fn get(&self, path: &Path, package_name: &str) -> Option<String> {
let key = CacheKey {
path: path.to_path_buf(),
package_name: package_name.to_string(),
};
let mut cache = self.cache.write();
if let Some(entry) = cache.get(&key) {
if entry.timestamp.elapsed() < self.ttl {
return Some(entry.value.clone());
}
cache.pop(&key);
}
None
}
pub fn insert(&self, path: &Path, package_name: &str, value: String) {
let key = CacheKey {
path: path.to_path_buf(),
package_name: package_name.to_string(),
};
let entry = CacheEntry {
value,
timestamp: Instant::now(),
};
self.cache.write().put(key, entry);
}
pub fn clear(&self) {
self.cache.write().clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.cache.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cache.read().is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_cache_new_zero_capacity_error() {
let result = EvaluationCache::new(0, Duration::from_secs(60));
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("non-zero"));
}
#[test]
fn test_cache_basic_operations() {
let cache = EvaluationCache::new(10, Duration::from_secs(60)).unwrap();
let path = Path::new("/test");
cache.insert(path, "pkg1", "result1".to_string());
assert_eq!(cache.get(path, "pkg1"), Some("result1".to_string()));
assert_eq!(cache.get(path, "pkg2"), None);
assert_eq!(cache.len(), 1);
}
#[test]
fn test_cache_is_empty() {
let cache = EvaluationCache::new(10, Duration::from_secs(60)).unwrap();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
cache.insert(Path::new("/test"), "pkg", "value".to_string());
assert!(!cache.is_empty());
assert_eq!(cache.len(), 1);
}
#[test]
fn test_cache_clear() {
let cache = EvaluationCache::new(10, Duration::from_secs(60)).unwrap();
cache.insert(Path::new("/test1"), "pkg1", "value1".to_string());
cache.insert(Path::new("/test2"), "pkg2", "value2".to_string());
assert_eq!(cache.len(), 2);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.get(Path::new("/test1"), "pkg1"), None);
}
#[test]
fn test_cache_expiration() {
let cache = EvaluationCache::new(10, Duration::from_millis(100)).unwrap();
let path = Path::new("/test");
cache.insert(path, "pkg", "result".to_string());
assert_eq!(cache.get(path, "pkg"), Some("result".to_string()));
thread::sleep(Duration::from_millis(150));
assert_eq!(cache.get(path, "pkg"), None);
}
#[test]
fn test_cache_lru_eviction() {
let cache = EvaluationCache::new(2, Duration::from_secs(60)).unwrap();
cache.insert(Path::new("/test1"), "pkg", "result1".to_string());
cache.insert(Path::new("/test2"), "pkg", "result2".to_string());
cache.insert(Path::new("/test3"), "pkg", "result3".to_string());
assert_eq!(cache.get(Path::new("/test1"), "pkg"), None);
assert_eq!(
cache.get(Path::new("/test2"), "pkg"),
Some("result2".to_string())
);
assert_eq!(
cache.get(Path::new("/test3"), "pkg"),
Some("result3".to_string())
);
}
#[test]
fn test_cache_different_paths_same_package() {
let cache = EvaluationCache::new(10, Duration::from_secs(60)).unwrap();
cache.insert(Path::new("/path1"), "pkg", "value1".to_string());
cache.insert(Path::new("/path2"), "pkg", "value2".to_string());
assert_eq!(cache.len(), 2);
assert_eq!(
cache.get(Path::new("/path1"), "pkg"),
Some("value1".to_string())
);
assert_eq!(
cache.get(Path::new("/path2"), "pkg"),
Some("value2".to_string())
);
}
#[test]
fn test_cache_same_path_different_packages() {
let cache = EvaluationCache::new(10, Duration::from_secs(60)).unwrap();
let path = Path::new("/test");
cache.insert(path, "pkg1", "value1".to_string());
cache.insert(path, "pkg2", "value2".to_string());
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(path, "pkg1"), Some("value1".to_string()));
assert_eq!(cache.get(path, "pkg2"), Some("value2".to_string()));
}
#[test]
fn test_cache_update_existing_entry() {
let cache = EvaluationCache::new(10, Duration::from_secs(60)).unwrap();
let path = Path::new("/test");
cache.insert(path, "pkg", "old_value".to_string());
cache.insert(path, "pkg", "new_value".to_string());
assert_eq!(cache.len(), 1);
assert_eq!(cache.get(path, "pkg"), Some("new_value".to_string()));
}
}