use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct CachedCardMetadata {
pub name: String,
pub description: String,
}
pub struct CardCache {
metadata: HashMap<String, CachedCardMetadata>,
cache_key: Option<String>,
paths: HashMap<String, PathBuf>,
}
impl CardCache {
pub fn new() -> Self {
Self {
metadata: HashMap::new(),
cache_key: None,
paths: HashMap::new(),
}
}
fn compute_cache_key() -> Result<String, Box<dyn std::error::Error>> {
let cards_dir = crate::character::loader::get_cards_dir();
if !cards_dir.exists() {
return Ok(String::new());
}
let mut mod_times = Vec::new();
for entry in std::fs::read_dir(cards_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
if let Ok(duration) = modified.duration_since(SystemTime::UNIX_EPOCH) {
mod_times.push(duration.as_secs());
}
}
}
}
mod_times.sort();
Ok(format!("{:?}", mod_times))
}
pub fn get_all_metadata(
&mut self,
) -> Result<Vec<CachedCardMetadata>, Box<dyn std::error::Error>> {
let current_key = Self::compute_cache_key()?;
if self.cache_key.as_ref() == Some(¤t_key) && !self.metadata.is_empty() {
let mut result: Vec<_> = self.metadata.values().cloned().collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
return Ok(result);
}
self.metadata.clear();
self.paths.clear();
let cards = crate::character::loader::list_available_cards()?;
for (name, path) in cards {
if let Ok(card) = crate::character::loader::load_card(&path) {
let metadata = CachedCardMetadata {
name: card.data.name.clone(),
description: card.data.description.clone(),
};
self.paths.insert(card.data.name.clone(), path.clone());
self.metadata.insert(name, metadata);
}
}
self.cache_key = Some(current_key);
let mut result: Vec<_> = self.metadata.values().cloned().collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
pub fn path_for(&self, name: &str) -> Option<&PathBuf> {
self.paths.get(name)
}
pub fn iter_paths(&self) -> impl Iterator<Item = (&String, &PathBuf)> {
self.paths.iter()
}
pub fn cache_key(&self) -> Option<&str> {
self.cache_key.as_deref()
}
}
impl Default for CardCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_cache_is_empty() {
let cache = CardCache::new();
assert!(cache.metadata.is_empty());
assert!(cache.cache_key.is_none());
}
#[test]
fn test_compute_cache_key_empty_directory() {
let result = CardCache::compute_cache_key();
assert!(result.is_ok());
}
#[test]
fn test_cache_hit_returns_same_data() {
let mut cache = CardCache::new();
let metadata = CachedCardMetadata {
name: "TestChar".to_string(),
description: "A test character".to_string(),
};
cache
.metadata
.insert("TestChar".to_string(), metadata.clone());
cache.cache_key = Some("test_key".to_string());
assert_eq!(cache.metadata.len(), 1);
assert!(cache.metadata.contains_key("TestChar"));
}
#[test]
fn test_cache_miss_reloads_data() {
let mut cache = CardCache::new();
cache.cache_key = Some("old_key".to_string());
assert_eq!(cache.cache_key, Some("old_key".to_string()));
}
#[test]
fn test_metadata_sorted_by_name() {
let mut cache = CardCache::new();
cache.metadata.insert(
"charlie".to_string(),
CachedCardMetadata {
name: "Charlie".to_string(),
description: "C".to_string(),
},
);
cache.metadata.insert(
"alice".to_string(),
CachedCardMetadata {
name: "Alice".to_string(),
description: "A".to_string(),
},
);
cache.metadata.insert(
"bob".to_string(),
CachedCardMetadata {
name: "Bob".to_string(),
description: "B".to_string(),
},
);
cache.cache_key = Some("test".to_string());
let mut result: Vec<_> = cache.metadata.values().cloned().collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(result[0].name, "Alice");
assert_eq!(result[1].name, "Bob");
assert_eq!(result[2].name, "Charlie");
}
#[test]
fn test_default_creates_empty_cache() {
let cache = CardCache::default();
assert!(cache.metadata.is_empty());
assert!(cache.cache_key.is_none());
}
#[test]
fn test_cache_reuses_data_on_second_call() {
let mut cache = CardCache::new();
let metadata = CachedCardMetadata {
name: "TestChar".to_string(),
description: "Test".to_string(),
};
cache.metadata.insert("TestChar".to_string(), metadata);
cache.cache_key = Some("stable_key".to_string());
assert_eq!(cache.metadata.len(), 1);
assert!(cache.cache_key.is_some());
assert_eq!(cache.metadata.len(), 1);
}
}