Skip to main content

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