Skip to main content

loco_rs/cache/drivers/
inmem.rs

1//! # In-Memory Cache Driver
2//!
3//! This module implements a cache driver using an in-memory cache.
4use std::time::{Duration, Instant};
5
6use async_trait::async_trait;
7use moka::{future::Cache, Expiry};
8
9use super::CacheDriver;
10use crate::cache::CacheResult;
11use crate::config::InMemCacheConfig;
12
13/// Creates a new instance of the in-memory cache driver, with a default Loco
14/// configuration.
15///
16/// # Returns
17///
18/// A [`Cache`] instance.
19#[must_use]
20pub fn new(config: &InMemCacheConfig) -> crate::cache::Cache {
21    let cache: Cache<String, (Expiration, String)> = Cache::builder()
22        .max_capacity(config.max_capacity)
23        .expire_after(InMemExpiry)
24        .build();
25    crate::cache::Cache::new(Inmem::from(cache))
26}
27
28/// Represents the in-memory cache driver.
29#[derive(Debug)]
30pub struct Inmem {
31    cache: Cache<String, (Expiration, String)>,
32}
33
34impl Inmem {
35    /// Constructs a new [`Inmem`] instance from a given cache.
36    ///
37    /// # Returns
38    ///
39    /// A boxed [`CacheDriver`] instance.
40    #[must_use]
41    pub fn from(cache: Cache<String, (Expiration, String)>) -> Box<dyn CacheDriver> {
42        Box::new(Self { cache })
43    }
44}
45
46#[async_trait]
47impl CacheDriver for Inmem {
48    /// Pings the cache to check if it is reachable.
49    ///
50    /// # Errors
51    ///
52    /// Returns always error
53    async fn ping(&self) -> CacheResult<()> {
54        Ok(())
55    }
56
57    /// Checks if a key exists in the cache.
58    ///
59    /// # Errors
60    ///
61    /// Returns a `CacheError` if there is an error during the operation.
62    async fn contains_key(&self, key: &str) -> CacheResult<bool> {
63        Ok(self.cache.contains_key(key))
64    }
65
66    /// Retrieves a value from the cache based on the provided key.
67    ///
68    /// # Errors
69    ///
70    /// Returns a `CacheError` if there is an error during the operation.
71    async fn get(&self, key: &str) -> CacheResult<Option<String>> {
72        let result = self.cache.get(key).await;
73        match result {
74            None => Ok(None),
75            Some(v) => Ok(Some(v.1)),
76        }
77    }
78
79    /// Inserts a key-value pair into the cache.
80    ///
81    /// # Errors
82    ///
83    /// Returns a `CacheError` if there is an error during the operation.
84    async fn insert(&self, key: &str, value: &str) -> CacheResult<()> {
85        self.cache
86            .insert(key.to_string(), (Expiration::Never, value.to_string()))
87            .await;
88        Ok(())
89    }
90
91    /// Inserts a key-value pair into the cache that expires after the specified
92    /// number of seconds.
93    ///
94    /// # Errors
95    ///
96    /// Returns a [`super::CacheError`] if there is an error during the
97    /// operation.
98    async fn insert_with_expiry(
99        &self,
100        key: &str,
101        value: &str,
102        duration: Duration,
103    ) -> CacheResult<()> {
104        self.cache
105            .insert(
106                key.to_string(),
107                (Expiration::AfterDuration(duration), value.to_string()),
108            )
109            .await;
110        Ok(())
111    }
112
113    /// Removes a key-value pair from the cache.
114    ///
115    /// # Errors
116    ///
117    /// Returns a `CacheError` if there is an error during the operation.
118    async fn remove(&self, key: &str) -> CacheResult<()> {
119        self.cache.remove(key).await;
120        Ok(())
121    }
122
123    /// Clears all key-value pairs from the cache.
124    ///
125    /// # Errors
126    ///
127    /// Returns a `CacheError` if there is an error during the operation.
128    async fn clear(&self) -> CacheResult<()> {
129        self.cache.invalidate_all();
130        Ok(())
131    }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub enum Expiration {
136    Never,
137    AfterDuration(Duration),
138}
139
140impl Expiration {
141    #[must_use]
142    pub fn as_duration(&self) -> Option<Duration> {
143        match self {
144            Self::Never => None,
145            Self::AfterDuration(d) => Some(*d),
146        }
147    }
148}
149
150pub struct InMemExpiry;
151
152impl Expiry<String, (Expiration, String)> for InMemExpiry {
153    fn expire_after_create(
154        &self,
155        _key: &String,
156        value: &(Expiration, String),
157        _current_time: Instant,
158    ) -> Option<Duration> {
159        value.0.as_duration()
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::config::InMemCacheConfig;
167
168    fn create_test_config() -> InMemCacheConfig {
169        InMemCacheConfig { max_capacity: 100 }
170    }
171
172    #[tokio::test]
173    async fn ping_returns_pong_when_cache_is_accessible() {
174        let config = create_test_config();
175        let mem = new(&config);
176        assert!(mem.ping().await.is_ok());
177    }
178
179    #[tokio::test]
180    async fn is_contains_key() {
181        let config = create_test_config();
182        let mem = new(&config);
183        assert!(!mem.contains_key("key").await.unwrap());
184        assert!(mem.insert("key", "loco").await.is_ok());
185        assert!(mem.contains_key("key").await.unwrap());
186    }
187
188    #[tokio::test]
189    async fn can_get_key_value() {
190        let config = create_test_config();
191        let mem = new(&config);
192        assert!(mem.insert("key", "loco").await.is_ok());
193        assert_eq!(
194            mem.get::<String>("key").await.unwrap(),
195            Some("loco".to_string())
196        );
197
198        //try getting key that not exists
199        assert_eq!(mem.get::<String>("not-found").await.unwrap(), None);
200    }
201
202    #[tokio::test]
203    async fn can_remove_key() {
204        let config = create_test_config();
205        let mem = new(&config);
206        assert!(mem.insert("key", "loco").await.is_ok());
207        assert!(mem.contains_key("key").await.unwrap());
208        mem.remove("key").await.unwrap();
209        assert!(!mem.contains_key("key").await.unwrap());
210    }
211
212    #[tokio::test]
213    async fn can_clear() {
214        let config = create_test_config();
215        let mem = new(&config);
216
217        let keys = vec!["key", "key2", "key3"];
218        for key in &keys {
219            assert!(mem.insert(key, "loco").await.is_ok());
220        }
221        for key in &keys {
222            assert!(mem.contains_key(key).await.is_ok());
223        }
224        assert!(mem.clear().await.is_ok());
225        for key in &keys {
226            assert!(!mem.contains_key(key).await.unwrap());
227        }
228    }
229}