kit_rs/cache/
memory.rs

1//! In-memory cache implementation for testing and fallback
2//!
3//! Provides a thread-safe in-memory cache that mimics Redis behavior.
4//! Supports TTL expiration.
5
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::sync::RwLock;
9use std::time::{Duration, Instant};
10
11use super::store::CacheStore;
12use crate::error::FrameworkError;
13
14/// In-memory cache entry with optional expiration
15#[derive(Clone)]
16struct CacheEntry {
17    value: String,
18    expires_at: Option<Instant>,
19}
20
21impl CacheEntry {
22    fn is_expired(&self) -> bool {
23        self.expires_at
24            .map(|t| Instant::now() > t)
25            .unwrap_or(false)
26    }
27}
28
29/// In-memory cache implementation
30///
31/// Thread-safe cache that stores values in memory with optional TTL.
32/// Use this as a fallback when Redis is unavailable, or in tests.
33///
34/// # Example
35///
36/// ```rust,ignore
37/// use kit::cache::InMemoryCache;
38///
39/// let cache = InMemoryCache::new();
40/// ```
41pub struct InMemoryCache {
42    store: RwLock<HashMap<String, CacheEntry>>,
43    prefix: String,
44}
45
46impl InMemoryCache {
47    /// Create a new empty in-memory cache
48    pub fn new() -> Self {
49        Self {
50            store: RwLock::new(HashMap::new()),
51            prefix: "kit_cache:".to_string(),
52        }
53    }
54
55    /// Create with a custom prefix
56    pub fn with_prefix(prefix: impl Into<String>) -> Self {
57        Self {
58            store: RwLock::new(HashMap::new()),
59            prefix: prefix.into(),
60        }
61    }
62
63    fn prefixed_key(&self, key: &str) -> String {
64        format!("{}{}", self.prefix, key)
65    }
66}
67
68impl Default for InMemoryCache {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74#[async_trait]
75impl CacheStore for InMemoryCache {
76    async fn get_raw(&self, key: &str) -> Result<Option<String>, FrameworkError> {
77        let key = self.prefixed_key(key);
78
79        let store = self.store.read().map_err(|_| {
80            FrameworkError::internal("Cache lock poisoned")
81        })?;
82
83        match store.get(&key) {
84            Some(entry) if !entry.is_expired() => Ok(Some(entry.value.clone())),
85            _ => Ok(None),
86        }
87    }
88
89    async fn put_raw(
90        &self,
91        key: &str,
92        value: &str,
93        ttl: Option<Duration>,
94    ) -> Result<(), FrameworkError> {
95        let key = self.prefixed_key(key);
96
97        let entry = CacheEntry {
98            value: value.to_string(),
99            expires_at: ttl.map(|d| Instant::now() + d),
100        };
101
102        let mut store = self.store.write().map_err(|_| {
103            FrameworkError::internal("Cache lock poisoned")
104        })?;
105
106        store.insert(key, entry);
107        Ok(())
108    }
109
110    async fn has(&self, key: &str) -> Result<bool, FrameworkError> {
111        let key = self.prefixed_key(key);
112
113        let store = self.store.read().map_err(|_| {
114            FrameworkError::internal("Cache lock poisoned")
115        })?;
116
117        Ok(store.get(&key).map(|e| !e.is_expired()).unwrap_or(false))
118    }
119
120    async fn forget(&self, key: &str) -> Result<bool, FrameworkError> {
121        let key = self.prefixed_key(key);
122
123        let mut store = self.store.write().map_err(|_| {
124            FrameworkError::internal("Cache lock poisoned")
125        })?;
126
127        Ok(store.remove(&key).is_some())
128    }
129
130    async fn flush(&self) -> Result<(), FrameworkError> {
131        let mut store = self.store.write().map_err(|_| {
132            FrameworkError::internal("Cache lock poisoned")
133        })?;
134
135        store.clear();
136        Ok(())
137    }
138
139    async fn increment(&self, key: &str, amount: i64) -> Result<i64, FrameworkError> {
140        let key = self.prefixed_key(key);
141
142        let mut store = self.store.write().map_err(|_| {
143            FrameworkError::internal("Cache lock poisoned")
144        })?;
145
146        let current: i64 = store
147            .get(&key)
148            .filter(|e| !e.is_expired())
149            .and_then(|e| e.value.parse().ok())
150            .unwrap_or(0);
151
152        let new_value = current + amount;
153
154        store.insert(
155            key,
156            CacheEntry {
157                value: new_value.to_string(),
158                expires_at: None,
159            },
160        );
161
162        Ok(new_value)
163    }
164
165    async fn decrement(&self, key: &str, amount: i64) -> Result<i64, FrameworkError> {
166        self.increment(key, -amount).await
167    }
168}