use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
#[derive(Debug, Clone)]
struct CacheEntry {
value: String,
created_at: u64,
ttl_seconds: Option<u64>,
}
impl CacheEntry {
fn is_expired(&self) -> bool {
if let Some(ttl) = self.ttl_seconds {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
now > self.created_at + ttl
} else {
false
}
}
}
pub struct ConversionCache {
cache: HashMap<String, CacheEntry>,
max_size: usize,
ttl_seconds: Option<u64>,
}
impl ConversionCache {
pub fn new(max_size: usize) -> Self {
Self {
cache: HashMap::new(),
max_size,
ttl_seconds: None,
}
}
pub fn set_ttl(&mut self, ttl_seconds: Option<u64>) {
self.ttl_seconds = ttl_seconds;
}
fn generate_key(doc_id: &Uuid, format: &str, doc_updated_at: u64) -> String {
format!("{}-{}-{}", doc_id, format, doc_updated_at)
}
pub fn get(&mut self, doc_id: &Uuid, format: &str, doc_updated_at: u64) -> Option<String> {
let key = Self::generate_key(doc_id, format, doc_updated_at);
if let Some(entry) = self.cache.get(&key) {
if !entry.is_expired() {
return Some(entry.value.clone());
} else {
self.cache.remove(&key);
}
}
None
}
pub fn set(&mut self, doc_id: &Uuid, format: &str, doc_updated_at: u64, value: String) {
if self.cache.len() >= self.max_size {
let expired_keys: Vec<_> = self
.cache
.iter()
.filter(|(_, entry)| entry.is_expired())
.map(|(k, _)| k.clone())
.collect();
for key in expired_keys {
self.cache.remove(&key);
}
if self.cache.len() >= self.max_size {
self.cache.clear();
}
}
let key = Self::generate_key(doc_id, format, doc_updated_at);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let entry = CacheEntry {
value,
created_at: now,
ttl_seconds: self.ttl_seconds,
};
self.cache.insert(key, entry);
}
pub fn invalidate(&mut self, doc_id: &Uuid, format: &str, doc_updated_at: u64) {
let key = Self::generate_key(doc_id, format, doc_updated_at);
self.cache.remove(&key);
}
pub fn invalidate_document(&mut self, doc_id: &Uuid) {
let doc_id_str = doc_id.to_string();
self.cache.retain(|k, _| !k.starts_with(&doc_id_str));
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn size(&self) -> usize {
self.cache.len()
}
pub fn stats(&self) -> CacheStats {
let total_entries = self.cache.len();
let expired_entries = self
.cache
.values()
.filter(|entry| entry.is_expired())
.count();
CacheStats {
total_entries,
expired_entries,
valid_entries: total_entries - expired_entries,
max_size: self.max_size,
}
}
pub fn cleanup_expired(&mut self) {
let expired_keys: Vec<_> = self
.cache
.iter()
.filter(|(_, entry)| entry.is_expired())
.map(|(k, _)| k.clone())
.collect();
for key in expired_keys {
self.cache.remove(&key);
}
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_entries: usize,
pub expired_entries: usize,
pub valid_entries: usize,
pub max_size: usize,
}
impl Default for ConversionCache {
fn default() -> Self {
Self::new(100)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_cache_creation() {
let cache = ConversionCache::new(100);
assert_eq!(cache.size(), 0);
assert_eq!(cache.cache.len(), 0);
}
#[test]
fn test_cache_set_and_get() {
let mut cache = ConversionCache::new(100);
let doc_id = Uuid::new_v4();
let value = "# Hello World".to_string();
cache.set(&doc_id, "markdown", 1000, value.clone());
assert_eq!(cache.size(), 1);
let retrieved = cache.get(&doc_id, "markdown", 1000);
assert_eq!(retrieved, Some(value));
}
#[test]
fn test_cache_miss_with_different_timestamp() {
let mut cache = ConversionCache::new(100);
let doc_id = Uuid::new_v4();
let value = "# Hello World".to_string();
cache.set(&doc_id, "markdown", 1000, value);
let retrieved = cache.get(&doc_id, "markdown", 1001);
assert_eq!(retrieved, None);
}
#[test]
fn test_cache_invalidate() {
let mut cache = ConversionCache::new(100);
let doc_id = Uuid::new_v4();
let value = "# Hello World".to_string();
cache.set(&doc_id, "markdown", 1000, value);
assert_eq!(cache.size(), 1);
cache.invalidate(&doc_id, "markdown", 1000);
assert_eq!(cache.size(), 0);
}
#[test]
fn test_cache_invalidate_document() {
let mut cache = ConversionCache::new(100);
let doc_id = Uuid::new_v4();
cache.set(&doc_id, "markdown", 1000, "markdown".to_string());
cache.set(&doc_id, "html", 1000, "html".to_string());
assert_eq!(cache.size(), 2);
cache.invalidate_document(&doc_id);
assert_eq!(cache.size(), 0);
}
#[test]
fn test_cache_max_size() {
let mut cache = ConversionCache::new(3);
for i in 0..5 {
let doc_id = Uuid::new_v4();
cache.set(&doc_id, "markdown", 1000, format!("content{}", i));
}
assert!(cache.size() <= 3);
}
#[test]
fn test_cache_ttl_expiration() {
let mut cache = ConversionCache::new(100);
cache.set_ttl(Some(1));
let doc_id = Uuid::new_v4();
cache.set(&doc_id, "markdown", 1000, "content".to_string());
assert!(cache.get(&doc_id, "markdown", 1000).is_some());
thread::sleep(Duration::from_secs(2));
assert!(cache.get(&doc_id, "markdown", 1000).is_none());
}
#[test]
fn test_cache_clear() {
let mut cache = ConversionCache::new(100);
let doc_id = Uuid::new_v4();
cache.set(&doc_id, "markdown", 1000, "content".to_string());
assert!(cache.size() > 0);
cache.clear();
assert_eq!(cache.size(), 0);
}
#[test]
fn test_cache_stats() {
let mut cache = ConversionCache::new(100);
let doc_id = Uuid::new_v4();
cache.set(&doc_id, "markdown", 1000, "content".to_string());
cache.set(&doc_id, "html", 1000, "content".to_string());
let stats = cache.stats();
assert_eq!(stats.total_entries, 2);
assert_eq!(stats.valid_entries, 2);
}
#[test]
fn test_cleanup_expired() {
let mut cache = ConversionCache::new(100);
cache.set_ttl(Some(1));
let doc_id = Uuid::new_v4();
cache.set(&doc_id, "markdown", 1000, "content".to_string());
thread::sleep(Duration::from_secs(2));
let initial_size = cache.size();
cache.cleanup_expired();
assert!(cache.size() < initial_size);
}
}