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}