1use redis::aio::ConnectionManager;
13use redis::{AsyncCommands, Client};
14use serde::{de::DeserializeOwned, Serialize};
15use tokio::sync::OnceCell;
16use uuid::Uuid;
17
18use crate::error::{RedisError, Result};
19use crate::RedisConfig;
20
21#[derive(Clone)]
29pub struct CacheManager {
30 client: Client,
31 cache_ttl_seconds: u64,
32 conn: OnceCell<ConnectionManager>,
34}
35
36impl CacheManager {
37 pub fn new(config: &RedisConfig) -> Result<Self> {
39 let client = Client::open(config.url.as_str())
40 .map_err(|e| RedisError::Connection(format!("Failed to create Redis client: {}", e)))?;
41
42 Ok(Self {
43 client,
44 cache_ttl_seconds: config.cache_ttl_seconds,
45 conn: OnceCell::new(),
46 })
47 }
48
49 async fn get_connection(&self) -> Result<ConnectionManager> {
56 let conn = self.conn.get_or_try_init(|| async {
57 self.client
58 .get_connection_manager()
59 .await
60 .map_err(|e| RedisError::Connection(format!("Failed to get Redis connection manager: {}", e)))
61 }).await?;
62
63 Ok(conn.clone())
64 }
65
66 pub async fn set_string(&self, key: &str, value: &str) -> Result<()> {
68 let mut conn = self.get_connection().await?;
69
70 conn.set_ex::<_, _, ()>(key, value, self.cache_ttl_seconds)
71 .await
72 .map_err(|e| RedisError::Operation(format!("Failed to set cache: {}", e)))?;
73
74 Ok(())
75 }
76
77 pub async fn get_string(&self, key: &str) -> Result<Option<String>> {
79 let mut conn = self.get_connection().await?;
80
81 let value: Option<String> = conn
82 .get(key)
83 .await
84 .map_err(|e| RedisError::Operation(format!("Failed to get cache: {}", e)))?;
85
86 Ok(value)
87 }
88
89 pub async fn set_json<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
91 let json = serde_json::to_string(value)?;
92 self.set_string(key, &json).await
93 }
94
95 pub async fn get_json<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
97 match self.get_string(key).await? {
98 Some(json) => {
99 let value = serde_json::from_str(&json)?;
100 Ok(Some(value))
101 }
102 None => Ok(None),
103 }
104 }
105
106 pub async fn delete(&self, key: &str) -> Result<()> {
108 let mut conn = self.get_connection().await?;
109
110 conn.del::<_, ()>(key)
111 .await
112 .map_err(|e| RedisError::Operation(format!("Failed to delete cache: {}", e)))?;
113
114 Ok(())
115 }
116
117 pub async fn delete_many(&self, keys: &[&str]) -> Result<()> {
119 if keys.is_empty() {
120 return Ok(());
121 }
122
123 let mut conn = self.get_connection().await?;
124
125 conn.del::<_, ()>(keys)
126 .await
127 .map_err(|e| RedisError::Operation(format!("Failed to delete caches: {}", e)))?;
128
129 Ok(())
130 }
131
132 pub async fn exists(&self, key: &str) -> Result<bool> {
134 let mut conn = self.get_connection().await?;
135
136 let exists: bool = conn
137 .exists(key)
138 .await
139 .map_err(|e| RedisError::Operation(format!("Failed to check key existence: {}", e)))?;
140
141 Ok(exists)
142 }
143
144 pub async fn set_string_with_ttl(&self, key: &str, value: &str, ttl_seconds: u64) -> Result<()> {
146 let mut conn = self.get_connection().await?;
147
148 conn.set_ex::<_, _, ()>(key, value, ttl_seconds)
149 .await
150 .map_err(|e| RedisError::Operation(format!("Failed to set cache with TTL: {}", e)))?;
151
152 Ok(())
153 }
154
155 pub fn scoped_key(&self, namespace: &str, id: Uuid, product: &str) -> String {
159 format!("cache:{}:{}:{}", product, namespace, id)
160 }
161
162 pub fn simple_key(&self, namespace: &str, product: &str) -> String {
166 format!("cache:{}:{}", product, namespace)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_scoped_key_format() {
176 let config = RedisConfig::default();
177 let manager = CacheManager::new(&config).unwrap();
178
179 let user_id = Uuid::new_v4();
180 let key = manager.scoped_key("conversations", user_id, "novaskyn");
181 assert_eq!(key, format!("cache:novaskyn:conversations:{}", user_id));
182 }
183
184 #[test]
185 fn test_simple_key_format() {
186 let config = RedisConfig::default();
187 let manager = CacheManager::new(&config).unwrap();
188
189 let key = manager.simple_key("config", "novaskyn");
190 assert_eq!(key, "cache:novaskyn:config");
191 }
192
193 #[test]
194 fn test_product_scoping() {
195 let config = RedisConfig::default();
196 let manager = CacheManager::new(&config).unwrap();
197
198 let user_id = Uuid::new_v4();
199 let key1 = manager.scoped_key("conversations", user_id, "novaskyn");
200 let key2 = manager.scoped_key("conversations", user_id, "lilitu");
201
202 assert_ne!(key1, key2);
203 assert!(key1.contains("novaskyn"));
204 assert!(key2.contains("lilitu"));
205 }
206}