Skip to main content

dinoco_engine/
cache.rs

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}