use indexmap::IndexMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq)]
pub enum SimpleCacheError {
NotFound,
Expired,
}
impl std::fmt::Display for SimpleCacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SimpleCacheError::NotFound => write!(f, "Cache entry not found"),
SimpleCacheError::Expired => write!(f, "Cache entry has expired"),
}
}
}
impl std::error::Error for SimpleCacheError {}
#[derive(Debug, Clone)]
pub struct SimpleCacheObject<U> {
created_at: Instant,
value: U,
max_age: Duration,
}
impl<U> SimpleCacheObject<U> {
fn new(value: U, max_age: Duration) -> Self {
Self {
created_at: Instant::now(),
value,
max_age,
}
}
pub fn is_expired(&self) -> bool {
self.created_at.elapsed() > self.max_age
}
pub fn value(&self) -> &U {
&self.value
}
pub fn value_mut(&mut self) -> &mut U {
&mut self.value
}
pub fn into_value(self) -> U {
self.value
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
pub fn created_at(&self) -> Instant {
self.created_at
}
}
pub trait Matcher<T> {
fn matches(&self, key: &T) -> bool;
}
#[derive(Debug, Clone)]
pub struct SimpleCacher<T, U> {
cache: IndexMap<T, SimpleCacheObject<U>>,
max_age: Duration,
max_size: Option<usize>,
}
impl<T, U> SimpleCacher<T, U>
where
T: Clone + Eq + std::hash::Hash,
{
pub fn new(max_age: Duration) -> Self {
Self {
cache: IndexMap::new(),
max_age,
max_size: None,
}
}
pub fn with_max_size(max_age: Duration, max_size: usize) -> Self {
Self {
cache: IndexMap::new(),
max_age,
max_size: Some(max_size),
}
}
pub fn get(&mut self, key: &T) -> Result<&SimpleCacheObject<U>, SimpleCacheError> {
let should_remove = match self.cache.get(key) {
Some(obj) => obj.is_expired(),
None => return Err(SimpleCacheError::NotFound),
};
if should_remove {
self.cache.shift_remove(key);
return Err(SimpleCacheError::Expired);
}
Ok(self.cache.get(key).unwrap())
}
pub fn get_mut(&mut self, key: &T) -> Result<&mut SimpleCacheObject<U>, SimpleCacheError> {
let should_remove = match self.cache.get(key) {
Some(obj) => obj.is_expired(),
None => return Err(SimpleCacheError::NotFound),
};
if should_remove {
self.cache.shift_remove(key);
return Err(SimpleCacheError::Expired);
}
Ok(self.cache.get_mut(key).unwrap())
}
pub fn get_by_matcher<M>(
&mut self,
matcher: &M,
) -> Result<&SimpleCacheObject<U>, SimpleCacheError>
where
M: Matcher<T>,
{
let mut expired_keys = Vec::new();
let mut found_key = None;
for (key, obj) in &self.cache {
if obj.is_expired() {
expired_keys.push(key.clone());
} else if found_key.is_none() && matcher.matches(key) {
found_key = Some(key.clone());
}
}
for key in expired_keys {
self.cache.shift_remove(&key);
}
if let Some(key) = found_key {
self.cache.get(&key).ok_or(SimpleCacheError::NotFound)
} else {
Err(SimpleCacheError::NotFound)
}
}
pub fn get_all_by_matcher<M>(&mut self, matcher: &M) -> Vec<(&T, &SimpleCacheObject<U>)>
where
M: Matcher<T>,
{
self.cleanup_expired();
self.cache
.iter()
.filter(|(key, obj)| !obj.is_expired() && matcher.matches(key))
.collect()
}
pub fn insert(&mut self, key: T, value: U) {
if let Some(max_size) = self.max_size {
while self.cache.len() >= max_size {
self.cache.shift_remove_index(0);
}
}
let cache_obj = SimpleCacheObject::new(value, self.max_age);
self.cache.insert(key, cache_obj);
}
pub fn insert_with_ttl(&mut self, key: T, value: U, ttl: Duration) {
if let Some(max_size) = self.max_size {
while self.cache.len() >= max_size {
self.cache.shift_remove_index(0);
}
}
let cache_obj = SimpleCacheObject::new(value, ttl);
self.cache.insert(key, cache_obj);
}
pub fn remove(&mut self, key: &T) -> Option<SimpleCacheObject<U>> {
self.cache.shift_remove(key)
}
pub fn contains_key(&self, key: &T) -> bool {
self.cache
.get(key)
.map(|obj| !obj.is_expired())
.unwrap_or(false)
}
pub fn cleanup_expired(&mut self) -> usize {
let expired_keys: Vec<T> = self
.cache
.iter()
.filter_map(|(k, v)| {
if v.is_expired() {
Some(k.clone())
} else {
None
}
})
.collect();
let count = expired_keys.len();
for key in expired_keys {
self.cache.shift_remove(&key);
}
count
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn active_len(&self) -> usize {
self.cache
.iter()
.filter(|(_, obj)| !obj.is_expired())
.count()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn stats(&self) -> CacheStats {
let total = self.cache.len();
let expired = self
.cache
.iter()
.filter(|(_, obj)| obj.is_expired())
.count();
CacheStats {
total_entries: total,
active_entries: total - expired,
expired_entries: expired,
max_size: self.max_size,
max_age: self.max_age,
}
}
pub fn iter_active(&self) -> impl Iterator<Item = (&T, &SimpleCacheObject<U>)> {
self.cache.iter().filter(|(_, obj)| !obj.is_expired())
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_entries: usize,
pub active_entries: usize,
pub expired_entries: usize,
pub max_size: Option<usize>,
pub max_age: Duration,
}
pub struct ExactMatcher<T> {
target: T,
}
impl<T> ExactMatcher<T> {
pub fn new(target: T) -> Self {
Self { target }
}
}
impl<T> Matcher<T> for ExactMatcher<T>
where
T: PartialEq,
{
fn matches(&self, key: &T) -> bool {
key == &self.target
}
}
pub struct PrefixMatcher {
prefix: String,
}
impl PrefixMatcher {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
}
impl Matcher<String> for PrefixMatcher {
fn matches(&self, key: &String) -> bool {
key.starts_with(&self.prefix)
}
}
impl Matcher<&str> for PrefixMatcher {
fn matches(&self, key: &&str) -> bool {
key.starts_with(&self.prefix)
}
}
pub struct SuffixMatcher {
suffix: String,
}
impl SuffixMatcher {
pub fn new(suffix: impl Into<String>) -> Self {
Self {
suffix: suffix.into(),
}
}
}
impl Matcher<String> for SuffixMatcher {
fn matches(&self, key: &String) -> bool {
key.ends_with(&self.suffix)
}
}
impl Matcher<&str> for SuffixMatcher {
fn matches(&self, key: &&str) -> bool {
key.ends_with(&self.suffix)
}
}
pub struct ContainsMatcher {
substring: String,
}
impl ContainsMatcher {
pub fn new(substring: impl Into<String>) -> Self {
Self {
substring: substring.into(),
}
}
}
impl Matcher<String> for ContainsMatcher {
fn matches(&self, key: &String) -> bool {
key.contains(&self.substring)
}
}
impl Matcher<&str> for ContainsMatcher {
fn matches(&self, key: &&str) -> bool {
key.contains(&self.substring)
}
}
pub struct RangeMatcher<T> {
min: T,
max: T,
inclusive: bool,
}
impl<T> RangeMatcher<T> {
pub fn new(min: T, max: T) -> Self {
Self {
min,
max,
inclusive: true,
}
}
pub fn exclusive(min: T, max: T) -> Self {
Self {
min,
max,
inclusive: false,
}
}
}
impl<T> Matcher<T> for RangeMatcher<T>
where
T: PartialOrd,
{
fn matches(&self, key: &T) -> bool {
if self.inclusive {
key >= &self.min && key <= &self.max
} else {
key > &self.min && key < &self.max
}
}
}
pub struct FnMatcher<T, F>
where
F: Fn(&T) -> bool,
{
matcher_fn: F,
_phantom: std::marker::PhantomData<T>,
}
impl<T, F> FnMatcher<T, F>
where
F: Fn(&T) -> bool,
{
pub fn new(matcher_fn: F) -> Self {
Self {
matcher_fn,
_phantom: std::marker::PhantomData,
}
}
}
impl<T, F> Matcher<T> for FnMatcher<T, F>
where
F: Fn(&T) -> bool,
{
fn matches(&self, key: &T) -> bool {
(self.matcher_fn)(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_basic_cache_operations() {
let mut cache = SimpleCacher::new(Duration::from_secs(1));
cache.insert("key1".to_string(), "value1".to_string());
assert_eq!(cache.get(&"key1".to_string()).unwrap().value(), "value1");
assert!(matches!(
cache.get(&"nonexistent".to_string()),
Err(SimpleCacheError::NotFound)
));
}
#[test]
fn test_expiration() {
let mut cache = SimpleCacher::new(Duration::from_millis(100));
cache.insert("key1".to_string(), "value1".to_string());
assert!(cache.get(&"key1".to_string()).is_ok());
thread::sleep(Duration::from_millis(150));
assert!(matches!(
cache.get(&"key1".to_string()),
Err(SimpleCacheError::Expired)
));
}
#[test]
fn test_max_size() {
let mut cache = SimpleCacher::with_max_size(Duration::from_secs(10), 2);
cache.insert(1, "value1");
cache.insert(2, "value2");
cache.insert(3, "value3");
assert!(matches!(cache.get(&1), Err(SimpleCacheError::NotFound)));
assert!(cache.get(&2).is_ok());
assert!(cache.get(&3).is_ok());
}
#[test]
fn test_prefix_matcher() {
let mut cache = SimpleCacher::new(Duration::from_secs(10));
cache.insert("prefix_key1".to_string(), "value1");
cache.insert("prefix_key2".to_string(), "value2");
cache.insert("other_key".to_string(), "value3");
let matcher = PrefixMatcher::new("prefix_");
let result = cache.get_by_matcher(&matcher);
assert!(result.is_ok());
assert!(result.unwrap().value().starts_with("value"));
}
#[test]
fn test_range_matcher() {
let mut cache = SimpleCacher::new(Duration::from_secs(10));
cache.insert(1, "value1");
cache.insert(5, "value5");
cache.insert(10, "value10");
cache.insert(15, "value15");
let matcher = RangeMatcher::new(3, 12);
let result = cache.get_by_matcher(&matcher);
assert!(result.is_ok());
let found_value = result.unwrap().value();
assert!(found_value.to_string() == "value5" || found_value.to_string() == "value10");
}
#[test]
fn test_function_matcher() {
let mut cache = SimpleCacher::new(Duration::from_secs(10));
cache.insert(2, "even");
cache.insert(3, "odd");
cache.insert(4, "even");
cache.insert(5, "odd");
let even_matcher = FnMatcher::new(|&key: &i32| key % 2 == 0);
let result = cache.get_by_matcher(&even_matcher);
assert!(result.is_ok());
assert_eq!(result.unwrap().value().to_string(), "even");
}
}