1use redis::aio::ConnectionManager;
2use redis::{AsyncCommands, ToRedisArgs};
3
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6
7use crate::{DinocoError, DinocoResult};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum DinocoRedisConfig {
11 Url { url: String },
12 Parameters { host: String, password: Option<String>, username: Option<String> },
13}
14
15#[derive(Clone)]
16pub struct DinocoCacheClient {
17 connection: ConnectionManager,
18}
19
20fn build_redis_connection_url(host: &str, username: &Option<String>, password: &Option<String>) -> String {
21 let output = if host.starts_with("redis://") || host.starts_with("rediss://") {
22 host.to_string()
23 } else {
24 format!("redis://{host}")
25 };
26
27 if username.is_none() && password.is_none() {
28 return output;
29 }
30
31 let (scheme, address) =
32 output.split_once("://").map(|(scheme, address)| (scheme, address)).unwrap_or(("redis", output.as_str()));
33
34 let credentials = match (username.as_deref(), password.as_deref()) {
35 (Some(username), Some(password)) => format!("{username}:{password}@"),
36 (Some(username), None) => format!("{username}@"),
37 (None, Some(password)) => format!(":{password}@"),
38 (None, None) => String::new(),
39 };
40
41 format!("{scheme}://{credentials}{address}")
42}
43
44impl DinocoRedisConfig {
45 pub fn from_host(host: impl Into<String>) -> Self {
46 Self::Parameters { host: host.into(), password: None, username: None }
47 }
48
49 pub fn from_url(url: impl Into<String>) -> Self {
50 Self::Url { url: url.into() }
51 }
52
53 pub fn connection_url(&self) -> String {
54 match self {
55 Self::Url { url } => url.clone(),
56 Self::Parameters { host, password, username } => build_redis_connection_url(host, username, password),
57 }
58 }
59
60 pub fn with_password(mut self, password: impl Into<String>) -> Self {
61 if let Self::Parameters { password: current_password, .. } = &mut self {
62 *current_password = Some(password.into());
63 }
64
65 self
66 }
67
68 pub fn with_username(mut self, username: impl Into<String>) -> Self {
69 if let Self::Parameters { username: current_username, .. } = &mut self {
70 *current_username = Some(username.into());
71 }
72
73 self
74 }
75}
76
77impl DinocoCacheClient {
78 pub async fn connect(config: &DinocoRedisConfig) -> DinocoResult<Self> {
79 let client = redis::Client::open(config.connection_url())?;
80 let connection = ConnectionManager::new(client).await?;
81
82 Ok(Self { connection })
83 }
84
85 pub async fn delete(&self, key: &str) -> DinocoResult<()> {
86 let mut connection = self.connection.clone();
87
88 connection.del::<_, ()>(key).await?;
89
90 Ok(())
91 }
92
93 pub async fn get<T>(&self, key: &str) -> DinocoResult<Option<T>>
94 where
95 T: DeserializeOwned,
96 {
97 let mut connection = self.connection.clone();
98 let value: Option<String> = connection.get(key).await?;
99
100 value
101 .map(|value| serde_json::from_str::<T>(&value).map_err(|error| DinocoError::ParseError(error.to_string())))
102 .transpose()
103 }
104
105 pub async fn set<T>(&self, key: &str, value: &T) -> DinocoResult<()>
106 where
107 T: Serialize,
108 {
109 let mut connection = self.connection.clone();
110 let value = serde_json::to_string(value).map_err(|error| DinocoError::ParseError(error.to_string()))?;
111
112 connection.set::<_, _, ()>(key, value).await?;
113
114 Ok(())
115 }
116
117 pub async fn set_with_ttl<T>(&self, key: &str, value: &T, ttl_seconds: u64) -> DinocoResult<()>
118 where
119 T: Serialize,
120 {
121 let mut connection = self.connection.clone();
122 let value = serde_json::to_string(value).map_err(|error| DinocoError::ParseError(error.to_string()))?;
123
124 connection.set_ex::<_, _, ()>(key, value, ttl_seconds).await?;
125
126 Ok(())
127 }
128
129 pub async fn hash_delete(&self, key: &str, field: &str) -> DinocoResult<()> {
130 let mut connection = self.connection.clone();
131
132 connection.hdel::<_, _, ()>(key, field).await?;
133
134 Ok(())
135 }
136
137 pub async fn hash_get<T>(&self, key: &str, field: &str) -> DinocoResult<Option<T>>
138 where
139 T: DeserializeOwned,
140 {
141 let mut connection = self.connection.clone();
142 let value: Option<String> = connection.hget(key, field).await?;
143
144 value
145 .map(|value| serde_json::from_str::<T>(&value).map_err(|error| DinocoError::ParseError(error.to_string())))
146 .transpose()
147 }
148
149 pub async fn hash_set<T>(&self, key: &str, field: &str, value: &T) -> DinocoResult<()>
150 where
151 T: Serialize,
152 {
153 let mut connection = self.connection.clone();
154 let value = serde_json::to_string(value).map_err(|error| DinocoError::ParseError(error.to_string()))?;
155
156 connection.hset::<_, _, _, ()>(key, field, value).await?;
157
158 Ok(())
159 }
160
161 pub async fn sorted_set_add<V>(&self, key: &str, value: V, score: i64) -> DinocoResult<()>
162 where
163 V: ToRedisArgs + Send + Sync,
164 {
165 let mut connection = self.connection.clone();
166
167 connection.zadd::<_, _, _, ()>(key, value, score).await?;
168
169 Ok(())
170 }
171
172 pub async fn sorted_set_range_by_score(
173 &self,
174 key: &str,
175 max_score: i64,
176 limit: isize,
177 ) -> DinocoResult<Vec<String>> {
178 let mut connection = self.connection.clone();
179
180 redis::cmd("ZRANGEBYSCORE")
181 .arg(key)
182 .arg("-inf")
183 .arg(max_score)
184 .arg("LIMIT")
185 .arg(0)
186 .arg(limit)
187 .query_async::<Vec<String>>(&mut connection)
188 .await
189 .map_err(DinocoError::from)
190 }
191
192 pub async fn sorted_set_remove<V>(&self, key: &str, value: V) -> DinocoResult<usize>
193 where
194 V: ToRedisArgs + Send + Sync,
195 {
196 let mut connection = self.connection.clone();
197
198 connection.zrem(key, value).await.map_err(DinocoError::from)
199 }
200
201 pub async fn sorted_set_pop_min_by_score(&self, key: &str, max_score: i64) -> DinocoResult<Option<String>> {
202 let mut connection = self.connection.clone();
203
204 redis::Script::new(
205 r#"
206 local items = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1], "LIMIT", 0, 1)
207
208 if #items == 0 then
209 return nil
210 end
211
212 if redis.call("ZREM", KEYS[1], items[1]) == 1 then
213 return items[1]
214 end
215
216 return nil
217 "#,
218 )
219 .key(key)
220 .arg(max_score)
221 .invoke_async(&mut connection)
222 .await
223 .map_err(DinocoError::from)
224 }
225}