use std::collections::BTreeMap;
use std::sync::Mutex;
use crate::error::{ArchToolkitError, Result};
pub trait FeedCache: Send + Sync {
fn get(&self, key: &str) -> Result<Option<String>>;
fn put(&self, key: &str, value: &str) -> Result<()>;
}
#[derive(Debug)]
pub struct InMemoryFeedCache {
capacity: usize,
entries: Mutex<BTreeMap<String, String>>,
}
impl InMemoryFeedCache {
pub fn new(capacity: usize) -> Result<Self> {
if capacity == 0 {
return Err(ArchToolkitError::InvalidInput(
"feed cache capacity must be greater than zero".to_string(),
));
}
Ok(Self {
capacity,
entries: Mutex::new(BTreeMap::new()),
})
}
}
impl FeedCache for InMemoryFeedCache {
fn get(&self, key: &str) -> Result<Option<String>> {
let entries = self
.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Ok(entries.get(key).cloned())
}
fn put(&self, key: &str, value: &str) -> Result<()> {
let mut entries = self
.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !entries.contains_key(key) && entries.len() == self.capacity {
let _ = entries.pop_first();
}
entries.insert(key.to_string(), value.to_string());
drop(entries);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{FeedCache, InMemoryFeedCache};
#[test]
fn cache_evicts_deterministically() {
let cache = InMemoryFeedCache::new(1).expect("valid cache capacity");
cache.put("arch-news:a", "first").expect("store first");
cache.put("arch-news:b", "second").expect("store second");
assert_eq!(cache.get("arch-news:a").expect("read first"), None);
assert_eq!(
cache.get("arch-news:b").expect("read second"),
Some("second".to_string())
);
}
#[test]
fn cache_rejects_zero_capacity() {
assert!(InMemoryFeedCache::new(0).is_err());
}
}