use crate::SkillDefinition;
use std::collections::HashMap;
use std::path::Path;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone)]
struct CacheEntry {
skill: SkillDefinition,
loaded_at: SystemTime,
expires_at: Option<SystemTime>,
}
impl CacheEntry {
fn new(skill: SkillDefinition, ttl: Option<Duration>) -> Self {
let now = SystemTime::now();
let expires_at = ttl.map(|d| now + d);
Self {
skill,
loaded_at: now,
expires_at,
}
}
fn is_expired(&self) -> bool {
self.expires_at.is_some_and(|exp| SystemTime::now() > exp)
}
}
pub struct SkillCache {
entries: HashMap<String, CacheEntry>,
default_ttl: Option<Duration>,
max_size: usize,
}
impl SkillCache {
pub fn new(max_size: usize, default_ttl: Option<Duration>) -> Self {
Self {
entries: HashMap::new(),
default_ttl,
max_size,
}
}
pub fn default_cache() -> Self {
Self::new(100, None)
}
pub fn get(&self, key: &str) -> Option<&SkillDefinition> {
self.entries
.get(key)
.filter(|e| !e.is_expired())
.map(|e| &e.skill)
}
pub fn insert(&mut self, key: String, skill: SkillDefinition) {
if self.entries.len() >= self.max_size {
self.remove_oldest();
}
let entry = CacheEntry::new(skill, self.default_ttl);
self.entries.insert(key, entry);
}
pub fn cleanup(&mut self) -> usize {
let expired_keys: Vec<String> = self
.entries
.iter()
.filter(|(_, e)| e.is_expired())
.map(|(k, _)| k.clone())
.collect();
let count = expired_keys.len();
for key in expired_keys {
self.entries.remove(&key);
}
count
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn remove_oldest(&mut self) {
if let Some(oldest_key) = self
.entries
.iter()
.min_by_key(|(_, e)| e.loaded_at)
.map(|(k, _)| k.clone())
{
self.entries.remove(&oldest_key);
}
}
}
pub struct CachedSkillLoader {
cache: SkillCache,
loader: crate::SkillLoader,
}
impl CachedSkillLoader {
pub fn new(skills_dir: &Path, cache: SkillCache) -> Self {
Self {
cache,
loader: crate::SkillLoader::new(skills_dir),
}
}
#[allow(clippy::unused_async)]
pub async fn get_skill(&mut self, name: &str) -> Option<crate::SkillDefinition> {
if let Some(skill) = self.cache.get(name) {
return Some(skill.clone());
}
None
}
pub fn cache_skill(&mut self, skill: crate::SkillDefinition) {
self.cache.insert(skill.name.clone(), skill);
}
pub fn cache_stats(&self) -> (usize, usize) {
(self.cache.len(), self.cache.max_size)
}
pub fn loader(&mut self) -> &mut crate::SkillLoader {
&mut self.loader
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_basic() {
let mut cache = SkillCache::default_cache();
let skill = SkillDefinition::new("test", "Test skill");
cache.insert("test".to_string(), skill);
assert_eq!(cache.len(), 1);
assert!(cache.get("test").is_some());
assert!(cache.get("nonexistent").is_none());
}
#[test]
fn test_cache_ttl() {
let mut cache = SkillCache::new(100, Some(Duration::from_millis(100)));
let skill = SkillDefinition::new("test", "Test skill");
cache.insert("test".to_string(), skill);
assert!(cache.get("test").is_some());
std::thread::sleep(Duration::from_millis(150));
assert!(cache.get("test").is_none());
}
#[test]
fn test_cache_max_size() {
let mut cache = SkillCache::new(3, None);
for i in 0..5 {
let skill = SkillDefinition::new(format!("test{i}"), "Test");
cache.insert(format!("test{i}"), skill);
}
assert_eq!(cache.len(), 3);
assert!(cache.get("test0").is_none());
assert!(cache.get("test1").is_none());
assert!(cache.get("test2").is_some());
assert!(cache.get("test3").is_some());
assert!(cache.get("test4").is_some());
}
#[test]
fn test_cleanup() {
let mut cache = SkillCache::new(100, Some(Duration::from_millis(50)));
for i in 0..3 {
let skill = SkillDefinition::new(format!("test{i}"), "Test");
cache.insert(format!("test{i}"), skill);
}
std::thread::sleep(Duration::from_millis(100));
let removed = cache.cleanup();
assert_eq!(removed, 3);
assert_eq!(cache.len(), 0);
}
}