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}