loco_rs/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
10pub use 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    /// Pings the cache to check if it is reachable.
78    ///
79    /// # Example
80    /// ```
81    /// use loco_rs::cache::{self, CacheResult};
82    /// use loco_rs::config::InMemCacheConfig;
83    ///
84    /// pub async fn ping() -> CacheResult<()> {
85    ///     let config = InMemCacheConfig { max_capacity: 100 };
86    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
87    ///     cache.ping().await
88    /// }
89    /// ```
90    ///
91    /// # Errors
92    /// A [`CacheResult`] indicating whether the cache is reachable.
93    pub async fn ping(&self) -> CacheResult<()> {
94        self.driver.ping().await
95    }
96
97    /// Checks if a key exists in the cache.
98    ///
99    /// # Example
100    /// ```
101    /// use loco_rs::cache::{self, CacheResult};
102    /// use loco_rs::config::InMemCacheConfig;
103    ///
104    /// pub async fn contains_key() -> CacheResult<bool> {
105    ///     let config = InMemCacheConfig { max_capacity: 100 };
106    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
107    ///     cache.contains_key("key").await
108    /// }
109    /// ```
110    ///
111    /// # Errors
112    /// A [`CacheResult`] indicating whether the key exists in the cache.
113    pub async fn contains_key(&self, key: &str) -> CacheResult<bool> {
114        self.driver.contains_key(key).await
115    }
116
117    /// Retrieves a value from the cache based on the provided key and deserializes it.
118    ///
119    /// # Example
120    /// ```
121    /// use loco_rs::cache::{self, CacheResult};
122    /// use loco_rs::config::InMemCacheConfig;
123    /// use serde::Deserialize;
124    ///
125    /// #[derive(Deserialize)]
126    /// struct User {
127    ///     name: String,
128    ///     age: u32,
129    /// }
130    ///
131    /// pub async fn get_user() -> CacheResult<Option<User>> {
132    ///     let config = InMemCacheConfig { max_capacity: 100 };
133    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
134    ///     cache.get::<User>("user:1").await
135    /// }
136    /// ```
137    ///
138    /// # Example with String
139    /// ```
140    /// use loco_rs::cache::{self, CacheResult};
141    /// use loco_rs::config::InMemCacheConfig;
142    ///
143    /// pub async fn get_string() -> CacheResult<Option<String>> {
144    ///     let config = InMemCacheConfig { max_capacity: 100 };
145    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
146    ///     cache.get::<String>("key").await
147    /// }
148    /// ```
149    ///
150    /// # Errors
151    /// A [`CacheResult`] containing an `Option` representing the retrieved
152    /// and deserialized value.
153    pub async fn get<T: DeserializeOwned>(&self, key: &str) -> CacheResult<Option<T>> {
154        let result = self.driver.get(key).await?;
155        if let Some(value) = result {
156            let deserialized = serde_json::from_str::<T>(&value)
157                .map_err(|e| CacheError::Deserialization(e.to_string()))?;
158            Ok(Some(deserialized))
159        } else {
160            Ok(None)
161        }
162    }
163
164    /// Inserts a serializable value into the cache with the provided key.
165    ///
166    /// # Example
167    /// ```
168    /// use loco_rs::cache::{self, CacheResult};
169    /// use loco_rs::config::InMemCacheConfig;
170    /// use serde::Serialize;
171    ///
172    /// #[derive(Serialize)]
173    /// struct User {
174    ///     name: String,
175    ///     age: u32,
176    /// }
177    ///
178    /// pub async fn insert() -> CacheResult<()> {
179    ///     let config = InMemCacheConfig { max_capacity: 100 };
180    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
181    ///     let user = User { name: "Alice".to_string(), age: 30 };
182    ///     cache.insert("user:1", &user).await
183    /// }
184    /// ```
185    ///
186    /// # Example with String
187    /// ```
188    /// use loco_rs::cache::{self, CacheResult};
189    /// use loco_rs::config::InMemCacheConfig;
190    ///
191    /// pub async fn insert_string() -> CacheResult<()> {
192    ///     let config = InMemCacheConfig { max_capacity: 100 };
193    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
194    ///     cache.insert("key", &"value".to_string()).await
195    /// }
196    /// ```
197    ///
198    /// # Errors
199    ///
200    /// A [`CacheResult`] indicating the success of the operation.
201    pub async fn insert<T: Serialize + Sync + ?Sized>(
202        &self,
203        key: &str,
204        value: &T,
205    ) -> CacheResult<()> {
206        let serialized =
207            serde_json::to_string(value).map_err(|e| CacheError::Serialization(e.to_string()))?;
208        self.driver.insert(key, &serialized).await
209    }
210
211    /// Inserts a serializable value into the cache with the provided key and expiry duration.
212    ///
213    /// # Example
214    /// ```
215    /// use std::time::Duration;
216    /// use loco_rs::cache::{self, CacheResult};
217    /// use loco_rs::config::InMemCacheConfig;
218    /// use serde::Serialize;
219    ///
220    /// #[derive(Serialize)]
221    /// struct User {
222    ///     name: String,
223    ///     age: u32,
224    /// }
225    ///
226    /// pub async fn insert() -> CacheResult<()> {
227    ///     let config = InMemCacheConfig { max_capacity: 100 };
228    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
229    ///     let user = User { name: "Alice".to_string(), age: 30 };
230    ///     cache.insert_with_expiry("user:1", &user, Duration::from_secs(300)).await
231    /// }
232    /// ```
233    ///
234    /// # Example with String
235    /// ```
236    /// use std::time::Duration;
237    /// use loco_rs::cache::{self, CacheResult};
238    /// use loco_rs::config::InMemCacheConfig;
239    ///
240    /// pub async fn insert_string() -> CacheResult<()> {
241    ///     let config = InMemCacheConfig { max_capacity: 100 };
242    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
243    ///     cache.insert_with_expiry("key", &"value".to_string(), Duration::from_secs(300)).await
244    /// }
245    /// ```
246    ///
247    /// # Errors
248    ///
249    /// A [`CacheResult`] indicating the success of the operation.
250    pub async fn insert_with_expiry<T: Serialize + Sync + ?Sized>(
251        &self,
252        key: &str,
253        value: &T,
254        duration: Duration,
255    ) -> CacheResult<()> {
256        let serialized =
257            serde_json::to_string(value).map_err(|e| CacheError::Serialization(e.to_string()))?;
258        self.driver
259            .insert_with_expiry(key, &serialized, duration)
260            .await
261    }
262
263    /// Retrieves and deserializes the value associated with the given key from the cache,
264    /// or inserts it if it does not exist, using the provided closure to
265    /// generate the value.
266    ///
267    /// # Example
268    /// ```
269    /// use loco_rs::{app::AppContext};
270    /// use loco_rs::tests_cfg::app::*;
271    /// use serde::{Serialize, Deserialize};
272    ///
273    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]
274    /// struct User {
275    ///     name: String,
276    ///     age: u32,
277    /// }
278    ///
279    /// pub async fn get_or_insert(){
280    ///    let app_ctx = get_app_context().await;
281    ///    let user = app_ctx.cache.get_or_insert::<User, _>("user:1", async {
282    ///            Ok(User { name: "Alice".to_string(), age: 30 })
283    ///     }).await.unwrap();
284    ///    assert_eq!(user.name, "Alice");
285    /// }
286    /// ```
287    ///
288    /// # Example with String
289    /// ```
290    /// use loco_rs::{app::AppContext};
291    /// use loco_rs::tests_cfg::app::*;
292    ///
293    /// pub async fn get_or_insert_string(){
294    ///    let app_ctx = get_app_context().await;
295    ///    let res = app_ctx.cache.get_or_insert::<String, _>("key", async {
296    ///            Ok("value".to_string())
297    ///     }).await.unwrap();
298    ///    assert_eq!(res, "value");
299    /// }
300    /// ```
301    ///
302    /// # Errors
303    ///
304    /// A [`LocoResult`] indicating the success of the operation.
305    pub async fn get_or_insert<T, F>(&self, key: &str, f: F) -> LocoResult<T>
306    where
307        T: Serialize + DeserializeOwned + Send + Sync,
308        F: Future<Output = LocoResult<T>> + Send,
309    {
310        if let Some(value) = self.get::<T>(key).await? {
311            Ok(value)
312        } else {
313            let value = f.await?;
314            self.insert(key, &value).await?;
315            Ok(value)
316        }
317    }
318
319    /// Retrieves and deserializes the value associated with the given key from the cache,
320    /// or inserts it (with expiry after provided duration) if it does not
321    /// exist, using the provided closure to generate the value.
322    ///
323    /// # Example
324    /// ```
325    /// use std::time::Duration;
326    /// use loco_rs::{app::AppContext};
327    /// use loco_rs::tests_cfg::app::*;
328    /// use serde::{Serialize, Deserialize};
329    ///
330    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]
331    /// struct User {
332    ///     name: String,
333    ///     age: u32,
334    /// }
335    ///
336    /// pub async fn get_or_insert(){
337    ///    let app_ctx = get_app_context().await;
338    ///    let user = app_ctx.cache.get_or_insert_with_expiry::<User, _>("user:1", Duration::from_secs(300), async {
339    ///            Ok(User { name: "Alice".to_string(), age: 30 })
340    ///     }).await.unwrap();
341    ///    assert_eq!(user.name, "Alice");
342    /// }
343    /// ```
344    ///
345    /// # Example with String
346    /// ```
347    /// use std::time::Duration;
348    /// use loco_rs::{app::AppContext};
349    /// use loco_rs::tests_cfg::app::*;
350    ///
351    /// pub async fn get_or_insert_string(){
352    ///    let app_ctx = get_app_context().await;
353    ///    let res = app_ctx.cache.get_or_insert_with_expiry::<String, _>("key", Duration::from_secs(300), async {
354    ///            Ok("value".to_string())
355    ///     }).await.unwrap();
356    ///    assert_eq!(res, "value");
357    /// }
358    /// ```
359    ///
360    /// # Errors
361    ///
362    /// A [`LocoResult`] indicating the success of the operation.
363    pub async fn get_or_insert_with_expiry<T, F>(
364        &self,
365        key: &str,
366        duration: Duration,
367        f: F,
368    ) -> LocoResult<T>
369    where
370        T: Serialize + DeserializeOwned + Send + Sync,
371        F: Future<Output = LocoResult<T>> + Send,
372    {
373        if let Some(value) = self.get::<T>(key).await? {
374            Ok(value)
375        } else {
376            let value = f.await?;
377            self.insert_with_expiry(key, &value, duration).await?;
378            Ok(value)
379        }
380    }
381
382    /// Removes a key-value pair from the cache.
383    ///
384    /// # Example
385    /// ```
386    /// use loco_rs::cache::{self, CacheResult};
387    /// use loco_rs::config::InMemCacheConfig;
388    ///
389    /// pub async fn remove() -> CacheResult<()> {
390    ///     let config = InMemCacheConfig { max_capacity: 100 };
391    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
392    ///     cache.remove("key").await
393    /// }
394    /// ```
395    ///
396    /// # Errors
397    ///
398    /// A [`CacheResult`] indicating the success of the operation.
399    pub async fn remove(&self, key: &str) -> CacheResult<()> {
400        self.driver.remove(key).await
401    }
402
403    /// Clears all key-value pairs from the cache.
404    ///
405    /// # Example
406    /// ```
407    /// use loco_rs::cache::{self, CacheResult};
408    /// use loco_rs::config::InMemCacheConfig;
409    ///
410    /// pub async fn clear() -> CacheResult<()> {
411    ///     let config = InMemCacheConfig { max_capacity: 100 };
412    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);
413    ///     cache.clear().await
414    /// }
415    /// ```
416    ///
417    /// # Errors
418    ///
419    /// A [`CacheResult`] indicating the success of the operation.
420    pub async fn clear(&self) -> CacheResult<()> {
421        self.driver.clear().await
422    }
423}
424
425#[cfg(test)]
426mod tests {
427
428    use crate::tests_cfg;
429    use serde::{Deserialize, Serialize};
430
431    #[tokio::test]
432    async fn can_get_or_insert() {
433        let app_ctx = tests_cfg::app::get_app_context().await;
434        let get_key = "loco";
435
436        assert_eq!(app_ctx.cache.get::<String>(get_key).await.unwrap(), None);
437
438        let result = app_ctx
439            .cache
440            .get_or_insert::<String, _>(get_key, async { Ok("loco-cache-value".to_string()) })
441            .await
442            .unwrap();
443
444        assert_eq!(result, "loco-cache-value".to_string());
445        assert_eq!(
446            app_ctx.cache.get::<String>(get_key).await.unwrap(),
447            Some("loco-cache-value".to_string())
448        );
449    }
450
451    #[derive(Debug, Serialize, Deserialize, PartialEq)]
452    struct TestUser {
453        name: String,
454        age: u32,
455    }
456
457    #[tokio::test]
458    async fn can_serialize_deserialize() {
459        let app_ctx = tests_cfg::app::get_app_context().await;
460        let key = "user:test";
461
462        // Test user data
463        let user = TestUser {
464            name: "Test User".to_string(),
465            age: 42,
466        };
467
468        // Insert serialized user
469        app_ctx.cache.insert(key, &user).await.unwrap();
470
471        // Retrieve and deserialize user
472        let retrieved: Option<TestUser> = app_ctx.cache.get(key).await.unwrap();
473        assert!(retrieved.is_some());
474        assert_eq!(retrieved.unwrap(), user);
475    }
476
477    #[tokio::test]
478    async fn can_get_or_insert_generic() {
479        let app_ctx = tests_cfg::app::get_app_context().await;
480        let key = "user:get_or_insert";
481
482        // The key should not exist initially
483        let no_user: Option<TestUser> = app_ctx.cache.get(key).await.unwrap();
484        assert!(no_user.is_none());
485
486        // Get or insert should create the user
487        let user = app_ctx
488            .cache
489            .get_or_insert::<TestUser, _>(key, async {
490                Ok(TestUser {
491                    name: "Alice".to_string(),
492                    age: 30,
493                })
494            })
495            .await
496            .unwrap();
497
498        assert_eq!(user.name, "Alice");
499        assert_eq!(user.age, 30);
500
501        // Verify the user was stored in the cache
502        let retrieved: TestUser = app_ctx
503            .cache
504            .get_or_insert::<TestUser, _>(key, async {
505                // This should not be called
506                Ok(TestUser {
507                    name: "Bob".to_string(),
508                    age: 25,
509                })
510            })
511            .await
512            .unwrap();
513
514        // Should retrieve Alice, not Bob
515        assert_eq!(retrieved.name, "Alice");
516        assert_eq!(retrieved.age, 30);
517    }
518}