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}