by_loco/cache/
mod.rs

1//! # Cache Module
2//!
3//! This module provides a generic cache interface for various cache drivers.
4pub mod drivers;
5
6use std::{future::Future, time::Duration};
7
8use serde::{de::DeserializeOwned, Serialize};
9
10use self::drivers::CacheDriver;
11use crate::config;
12use crate::Result as LocoResult;
13use std::sync::Arc;
14
15/// Errors related to cache operations
16#[derive(thiserror::Error, Debug)]
17#[allow(clippy::module_name_repetitions)]
18pub enum CacheError {
19    #[error(transparent)]
20    Any(#[from] Box<dyn std::error::Error + Send + Sync>),
21
22    #[error("Serialization error: {0}")]
23    Serialization(String),
24
25    #[error("Deserialization error: {0}")]
26    Deserialization(String),
27
28    #[cfg(feature = "cache_redis")]
29    #[error(transparent)]
30    Redis(#[from] bb8_redis::redis::RedisError),
31
32    #[cfg(feature = "cache_redis")]
33    #[error(transparent)]
34    RedisConnectionError(#[from] bb8_redis::bb8::RunError<bb8_redis::redis::RedisError>),
35}
36
37pub type CacheResult<T> = std::result::Result<T, CacheError>;
38
39/// Create a provider
40///
41/// # Errors
42///
43/// This function will return an error if fails to build
44#[allow(clippy::unused_async)]
45pub async fn create_cache_provider(config: &config::Config) -> crate::Result<Arc<Cache>> {
46    match &config.cache {
47        #[cfg(feature = "cache_redis")]
48        config::CacheConfig::Redis(config) => {
49            let cache = crate::cache::drivers::redis::new(config).await?;
50            Ok(Arc::new(cache))
51        }
52        #[cfg(feature = "cache_inmem")]
53        config::CacheConfig::InMem(config) => {
54            let cache = crate::cache::drivers::inmem::new(config);
55            Ok(Arc::new(cache))
56        }
57        config::CacheConfig::Null => {
58            let driver = crate::cache::drivers::null::new();
59            Ok(Arc::new(Cache::new(driver)))
60        }
61    }
62}
63
64/// Represents a cache instance
65pub struct Cache {
66    /// The cache driver used for underlying operations
67    pub driver: Box<dyn CacheDriver>,
68}
69
70impl Cache {
71    /// Creates a new cache instance with the specified cache driver.
72    #[must_use]
73    pub fn new(driver: Box<dyn CacheDriver>) -> Self {
74        Self { driver }
75    }
76
77    /// Checks if a key exists in the cache.
78    ///
79    /// # Example
80    /// ```
81    /// use loco_rs::cache::{self, CacheResult};
82    /// use loco_rs::config::InMemCacheConfig;
83    ///
84    /// pub async fn contains_key() -> CacheResult<bool> {
85    ///     let config = InMemCacheConfig { max_capacity: 100 };
86    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
87    ///     cache.contains_key("key").await
88    /// }
89    /// ```
90    ///
91    /// # Errors
92    /// A [`CacheResult`] indicating whether the key exists in the cache.
93    pub async fn contains_key(&self, key: &str) -> CacheResult<bool> {
94        self.driver.contains_key(key).await
95    }
96
97    /// Retrieves a value from the cache based on the provided key and deserializes it.
98    ///
99    /// # Example
100    /// ```
101    /// use loco_rs::cache::{self, CacheResult};
102    /// use loco_rs::config::InMemCacheConfig;
103    /// use serde::Deserialize;
104    ///
105    /// #[derive(Deserialize)]
106    /// struct User {
107    ///     name: String,
108    ///     age: u32,
109    /// }
110    ///
111    /// pub async fn get_user() -> CacheResult<Option<User>> {
112    ///     let config = InMemCacheConfig { max_capacity: 100 };
113    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
114    ///     cache.get::<User>("user:1").await
115    /// }
116    /// ```
117    ///
118    /// # Example with String
119    /// ```
120    /// use loco_rs::cache::{self, CacheResult};
121    /// use loco_rs::config::InMemCacheConfig;
122    ///
123    /// pub async fn get_string() -> CacheResult<Option<String>> {
124    ///     let config = InMemCacheConfig { max_capacity: 100 };
125    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
126    ///     cache.get::<String>("key").await
127    /// }
128    /// ```
129    ///
130    /// # Errors
131    /// A [`CacheResult`] containing an `Option` representing the retrieved
132    /// and deserialized value.
133    pub async fn get<T: DeserializeOwned>(&self, key: &str) -> CacheResult<Option<T>> {
134        let result = self.driver.get(key).await?;
135        if let Some(value) = result {
136            let deserialized = serde_json::from_str::<T>(&value)
137                .map_err(|e| CacheError::Deserialization(e.to_string()))?;
138            Ok(Some(deserialized))
139        } else {
140            Ok(None)
141        }
142    }
143
144    /// Inserts a serializable value into the cache with the provided key.
145    ///
146    /// # Example
147    /// ```
148    /// use loco_rs::cache::{self, CacheResult};
149    /// use loco_rs::config::InMemCacheConfig;
150    /// use serde::Serialize;
151    ///
152    /// #[derive(Serialize)]
153    /// struct User {
154    ///     name: String,
155    ///     age: u32,
156    /// }
157    ///
158    /// pub async fn insert() -> CacheResult<()> {
159    ///     let config = InMemCacheConfig { max_capacity: 100 };
160    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
161    ///     let user = User { name: "Alice".to_string(), age: 30 };
162    ///     cache.insert("user:1", &user).await
163    /// }
164    /// ```
165    ///
166    /// # Example with String
167    /// ```
168    /// use loco_rs::cache::{self, CacheResult};
169    /// use loco_rs::config::InMemCacheConfig;
170    ///
171    /// pub async fn insert_string() -> CacheResult<()> {
172    ///     let config = InMemCacheConfig { max_capacity: 100 };
173    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
174    ///     cache.insert("key", &"value".to_string()).await
175    /// }
176    /// ```
177    ///
178    /// # Errors
179    ///
180    /// A [`CacheResult`] indicating the success of the operation.
181    pub async fn insert<T: Serialize + Sync + ?Sized>(
182        &self,
183        key: &str,
184        value: &T,
185    ) -> CacheResult<()> {
186        let serialized =
187            serde_json::to_string(value).map_err(|e| CacheError::Serialization(e.to_string()))?;
188        self.driver.insert(key, &serialized).await
189    }
190
191    /// Inserts a serializable value into the cache with the provided key and expiry duration.
192    ///
193    /// # Example
194    /// ```
195    /// use std::time::Duration;
196    /// use loco_rs::cache::{self, CacheResult};
197    /// use loco_rs::config::InMemCacheConfig;
198    /// use serde::Serialize;
199    ///
200    /// #[derive(Serialize)]
201    /// struct User {
202    ///     name: String,
203    ///     age: u32,
204    /// }
205    ///
206    /// pub async fn insert() -> CacheResult<()> {
207    ///     let config = InMemCacheConfig { max_capacity: 100 };
208    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
209    ///     let user = User { name: "Alice".to_string(), age: 30 };
210    ///     cache.insert_with_expiry("user:1", &user, Duration::from_secs(300)).await
211    /// }
212    /// ```
213    ///
214    /// # Example with String
215    /// ```
216    /// use std::time::Duration;
217    /// use loco_rs::cache::{self, CacheResult};
218    /// use loco_rs::config::InMemCacheConfig;
219    ///
220    /// pub async fn insert_string() -> CacheResult<()> {
221    ///     let config = InMemCacheConfig { max_capacity: 100 };
222    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
223    ///     cache.insert_with_expiry("key", &"value".to_string(), Duration::from_secs(300)).await
224    /// }
225    /// ```
226    ///
227    /// # Errors
228    ///
229    /// A [`CacheResult`] indicating the success of the operation.
230    pub async fn insert_with_expiry<T: Serialize + Sync + ?Sized>(
231        &self,
232        key: &str,
233        value: &T,
234        duration: Duration,
235    ) -> CacheResult<()> {
236        let serialized =
237            serde_json::to_string(value).map_err(|e| CacheError::Serialization(e.to_string()))?;
238        self.driver
239            .insert_with_expiry(key, &serialized, duration)
240            .await
241    }
242
243    /// Retrieves and deserializes the value associated with the given key from the cache,
244    /// or inserts it if it does not exist, using the provided closure to
245    /// generate the value.
246    ///
247    /// # Example
248    /// ```
249    /// use loco_rs::{app::AppContext};
250    /// use loco_rs::tests_cfg::app::*;
251    /// use serde::{Serialize, Deserialize};
252    ///
253    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]
254    /// struct User {
255    ///     name: String,
256    ///     age: u32,
257    /// }
258    ///
259    /// pub async fn get_or_insert(){
260    ///    let app_ctx = get_app_context().await;
261    ///    let user = app_ctx.cache.get_or_insert::<User, _>("user:1", async {
262    ///            Ok(User { name: "Alice".to_string(), age: 30 })
263    ///     }).await.unwrap();
264    ///    assert_eq!(user.name, "Alice");
265    /// }
266    /// ```
267    ///
268    /// # Example with String
269    /// ```
270    /// use loco_rs::{app::AppContext};
271    /// use loco_rs::tests_cfg::app::*;
272    ///
273    /// pub async fn get_or_insert_string(){
274    ///    let app_ctx = get_app_context().await;
275    ///    let res = app_ctx.cache.get_or_insert::<String, _>("key", async {
276    ///            Ok("value".to_string())
277    ///     }).await.unwrap();
278    ///    assert_eq!(res, "value");
279    /// }
280    /// ```
281    ///
282    /// # Errors
283    ///
284    /// A [`LocoResult`] indicating the success of the operation.
285    pub async fn get_or_insert<T, F>(&self, key: &str, f: F) -> LocoResult<T>
286    where
287        T: Serialize + DeserializeOwned + Send + Sync,
288        F: Future<Output = LocoResult<T>> + Send,
289    {
290        if let Some(value) = self.get::<T>(key).await? {
291            Ok(value)
292        } else {
293            let value = f.await?;
294            self.insert(key, &value).await?;
295            Ok(value)
296        }
297    }
298
299    /// Retrieves and deserializes the value associated with the given key from the cache,
300    /// or inserts it (with expiry after provided duration) if it does not
301    /// exist, using the provided closure to generate the value.
302    ///
303    /// # Example
304    /// ```
305    /// use std::time::Duration;
306    /// use loco_rs::{app::AppContext};
307    /// use loco_rs::tests_cfg::app::*;
308    /// use serde::{Serialize, Deserialize};
309    ///
310    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]
311    /// struct User {
312    ///     name: String,
313    ///     age: u32,
314    /// }
315    ///
316    /// pub async fn get_or_insert(){
317    ///    let app_ctx = get_app_context().await;
318    ///    let user = app_ctx.cache.get_or_insert_with_expiry::<User, _>("user:1", Duration::from_secs(300), async {
319    ///            Ok(User { name: "Alice".to_string(), age: 30 })
320    ///     }).await.unwrap();
321    ///    assert_eq!(user.name, "Alice");
322    /// }
323    /// ```
324    ///
325    /// # Example with String
326    /// ```
327    /// use std::time::Duration;
328    /// use loco_rs::{app::AppContext};
329    /// use loco_rs::tests_cfg::app::*;
330    ///
331    /// pub async fn get_or_insert_string(){
332    ///    let app_ctx = get_app_context().await;
333    ///    let res = app_ctx.cache.get_or_insert_with_expiry::<String, _>("key", Duration::from_secs(300), async {
334    ///            Ok("value".to_string())
335    ///     }).await.unwrap();
336    ///    assert_eq!(res, "value");
337    /// }
338    /// ```
339    ///
340    /// # Errors
341    ///
342    /// A [`LocoResult`] indicating the success of the operation.
343    pub async fn get_or_insert_with_expiry<T, F>(
344        &self,
345        key: &str,
346        duration: Duration,
347        f: F,
348    ) -> LocoResult<T>
349    where
350        T: Serialize + DeserializeOwned + Send + Sync,
351        F: Future<Output = LocoResult<T>> + Send,
352    {
353        if let Some(value) = self.get::<T>(key).await? {
354            Ok(value)
355        } else {
356            let value = f.await?;
357            self.insert_with_expiry(key, &value, duration).await?;
358            Ok(value)
359        }
360    }
361
362    /// Removes a key-value pair from the cache.
363    ///
364    /// # Example
365    /// ```
366    /// use loco_rs::cache::{self, CacheResult};
367    /// use loco_rs::config::InMemCacheConfig;
368    ///
369    /// pub async fn remove() -> CacheResult<()> {
370    ///     let config = InMemCacheConfig { max_capacity: 100 };
371    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
372    ///     cache.remove("key").await
373    /// }
374    /// ```
375    ///
376    /// # Errors
377    ///
378    /// A [`CacheResult`] indicating the success of the operation.
379    pub async fn remove(&self, key: &str) -> CacheResult<()> {
380        self.driver.remove(key).await
381    }
382
383    /// Clears all key-value pairs from the cache.
384    ///
385    /// # Example
386    /// ```
387    /// use loco_rs::cache::{self, CacheResult};
388    /// use loco_rs::config::InMemCacheConfig;
389    ///
390    /// pub async fn clear() -> CacheResult<()> {
391    ///     let config = InMemCacheConfig { max_capacity: 100 };
392    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
393    ///     cache.clear().await
394    /// }
395    /// ```
396    ///
397    /// # Errors
398    ///
399    /// A [`CacheResult`] indicating the success of the operation.
400    pub async fn clear(&self) -> CacheResult<()> {
401        self.driver.clear().await
402    }
403}
404
405#[cfg(test)]
406mod tests {
407
408    use crate::tests_cfg;
409    use serde::{Deserialize, Serialize};
410
411    #[tokio::test]
412    async fn can_get_or_insert() {
413        let app_ctx = tests_cfg::app::get_app_context().await;
414        let get_key = "loco";
415
416        assert_eq!(app_ctx.cache.get::<String>(get_key).await.unwrap(), None);
417
418        let result = app_ctx
419            .cache
420            .get_or_insert::<String, _>(get_key, async { Ok("loco-cache-value".to_string()) })
421            .await
422            .unwrap();
423
424        assert_eq!(result, "loco-cache-value".to_string());
425        assert_eq!(
426            app_ctx.cache.get::<String>(get_key).await.unwrap(),
427            Some("loco-cache-value".to_string())
428        );
429    }
430
431    #[derive(Debug, Serialize, Deserialize, PartialEq)]
432    struct TestUser {
433        name: String,
434        age: u32,
435    }
436
437    #[tokio::test]
438    async fn can_serialize_deserialize() {
439        let app_ctx = tests_cfg::app::get_app_context().await;
440        let key = "user:test";
441
442        // Test user data
443        let user = TestUser {
444            name: "Test User".to_string(),
445            age: 42,
446        };
447
448        // Insert serialized user
449        app_ctx.cache.insert(key, &user).await.unwrap();
450
451        // Retrieve and deserialize user
452        let retrieved: Option<TestUser> = app_ctx.cache.get(key).await.unwrap();
453        assert!(retrieved.is_some());
454        assert_eq!(retrieved.unwrap(), user);
455    }
456
457    #[tokio::test]
458    async fn can_get_or_insert_generic() {
459        let app_ctx = tests_cfg::app::get_app_context().await;
460        let key = "user:get_or_insert";
461
462        // The key should not exist initially
463        let no_user: Option<TestUser> = app_ctx.cache.get(key).await.unwrap();
464        assert!(no_user.is_none());
465
466        // Get or insert should create the user
467        let user = app_ctx
468            .cache
469            .get_or_insert::<TestUser, _>(key, async {
470                Ok(TestUser {
471                    name: "Alice".to_string(),
472                    age: 30,
473                })
474            })
475            .await
476            .unwrap();
477
478        assert_eq!(user.name, "Alice");
479        assert_eq!(user.age, 30);
480
481        // Verify the user was stored in the cache
482        let retrieved: TestUser = app_ctx
483            .cache
484            .get_or_insert::<TestUser, _>(key, async {
485                // This should not be called
486                Ok(TestUser {
487                    name: "Bob".to_string(),
488                    age: 25,
489                })
490            })
491            .await
492            .unwrap();
493
494        // Should retrieve Alice, not Bob
495        assert_eq!(retrieved.name, "Alice");
496        assert_eq!(retrieved.age, 30);
497    }
498}