Skip to main content

oxidite_cache/
redis.rs

1use async_trait::async_trait;
2use redis::{Client, AsyncCommands};
3use std::time::Duration;
4use serde::{Deserialize, Serialize};
5use crate::{validate_cache_key, validate_ttl, Cache, Result};
6
7/// Redis cache backend
8pub struct RedisCache {
9    client: Client,
10    default_ttl: Option<Duration>,
11}
12
13impl RedisCache {
14    pub fn new(url: &str) -> Result<Self> {
15        let client = Client::open(url)?;
16        
17        Ok(Self {
18            client,
19            default_ttl: Some(Duration::from_secs(3600)),
20        })
21    }
22
23    pub fn with_default_ttl(url: &str, ttl: Duration) -> Result<Self> {
24        validate_ttl(Some(ttl))?;
25        let client = Client::open(url)?;
26        
27        Ok(Self {
28            client,
29            default_ttl: Some(ttl),
30        })
31    }
32}
33
34#[async_trait]
35impl Cache for RedisCache {
36    async fn get<T>(&self, key: &str) -> Result<Option<T>>
37    where
38        T: for<'de> Deserialize<'de> + Send,
39    {
40        validate_cache_key(key)?;
41        let mut conn = self.client.get_multiplexed_async_connection().await?;
42            
43        let result: Option<String> = conn.get(key).await?;
44            
45        if let Some(data) = result {
46            let value: T = serde_json::from_str(&data)?;
47            Ok(Some(value))
48        } else {
49            Ok(None)
50        }
51    }
52
53    async fn set<T>(&self, key: &str, value: &T, ttl: Option<Duration>) -> Result<()>
54    where
55        T: Serialize + Send + Sync,
56    {
57        validate_cache_key(key)?;
58        validate_ttl(ttl)?;
59        let mut conn = self.client.get_multiplexed_async_connection().await?;
60            
61        let data = serde_json::to_string(value)?;
62            
63        let ttl = ttl.or(self.default_ttl);
64        
65        if let Some(duration) = ttl {
66            let seconds = duration.as_secs().max(1);
67            let _: () = conn.set_ex(key, data, seconds).await?;
68        } else {
69            let _: () = conn.set(key, data).await?;
70        }
71        
72        Ok(())
73    }
74
75    async fn delete(&self, key: &str) -> Result<()> {
76        validate_cache_key(key)?;
77        let mut conn = self.client.get_multiplexed_async_connection().await?;
78            
79        let _: () = conn.del(key).await?;
80            
81        Ok(())
82    }
83
84    async fn exists(&self, key: &str) -> Result<bool> {
85        validate_cache_key(key)?;
86        let mut conn = self.client.get_multiplexed_async_connection().await?;
87            
88        let exists: bool = conn.exists(key).await?;
89            
90        Ok(exists)
91    }
92
93    async fn flush(&self) -> Result<()> {
94        let mut conn = self.client.get_multiplexed_async_connection().await?;
95            
96        let _: () = redis::cmd("FLUSHDB")
97            .query_async(&mut conn)
98            .await?;
99            
100        Ok(())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::RedisCache;
107    use std::time::Duration;
108
109    #[test]
110    fn rejects_zero_default_ttl() {
111        let result = RedisCache::with_default_ttl("redis://127.0.0.1/", Duration::from_secs(0));
112        assert!(result.is_err());
113    }
114}