1use std::{fmt::Display, marker::PhantomData};
2
3use redis::{Commands, FromRedisValue, RedisError, ToRedisArgs};
4use std::time::Duration;
5
6use crate::storage::{Err, Storage};
7
8#[derive(Debug, Clone)]
9pub struct RedisStorage<K, V>
10where
11 V: Into<String>,
12{
13 client: redis::Client,
14 _marker: PhantomData<(K, V)>,
15}
16
17impl<K, V> Storage<K, V> for RedisStorage<K, V>
18where
19 K: ToRedisArgs,
20 V: Into<String> + FromRedisValue,
21{
22 fn set(&self, key: K, value: V) -> Result<(), Err> {
23 match self.client.get_connection() {
24 Ok(mut conn) => conn
25 .set::<K, String, ()>(key, value.into())
26 .map_or_else(|e| Err(e.into()), |_| Ok(())),
27 Err(e) => Err(e.into()),
28 }
29 }
30
31 fn set_ex(&self, key: K, value: V, expire: Duration) -> Result<(), Err> {
32 match self.client.get_connection() {
33 Ok(mut conn) => conn
34 .set_ex::<K, String, ()>(key, value.into(), expire.as_secs() as usize)
35 .map_or_else(|e| Err(e.into()), |_| Ok(())),
36 Err(e) => Err(e.into()),
37 }
38 }
39
40 fn get(&self, key: K) -> Result<Option<V>, Err> {
41 match self.client.get_connection() {
42 Ok(mut conn) => conn.get(key).map_or_else(
43 |e| {
44 if caused_by_nil_response(&e) {
45 return Ok(None);
46 } else {
47 return Err(e.into());
48 }
49 },
50 |resp: V| Ok(Some(resp)),
51 ),
52 Err(e) => Err(e.into()),
53 }
54 }
55
56 fn del(&self, key: K) -> Result<Option<K>, Err> {
57 match self.client.get_connection() {
58 Ok(mut conn) => conn
59 .del(&key)
60 .map_or_else(|e| Err(e.into()), |_: ()| Ok(Some(key))),
61 Err(e) => Err(e.into()),
62 }
63 }
64
65 fn contains(&self, key: K) -> Result<bool, Err> {
66 match self.client.get_connection() {
67 Ok(mut conn) => conn.get(key).map_or_else(
68 |e| {
69 if caused_by_nil_response(&e) {
70 return Ok(false);
71 } else {
72 return Err(e.into());
73 }
74 },
75 |_: V| Ok(true),
76 ),
77 Err(e) => Err(e.into()),
78 }
79 }
80}
81
82pub struct RedisStorageBuilder<K, V>
83where
84 K: ToRedisArgs,
85 V: Into<String>,
86{
87 addr: Option<String>,
88 _marker: PhantomData<(K, V)>,
89}
90
91#[allow(unused)]
92impl<K, V> RedisStorageBuilder<K, V>
93where
94 K: ToRedisArgs,
95 V: Into<String>,
96{
97 pub fn new() -> Self {
98 RedisStorageBuilder::default()
99 }
100
101 pub fn config(mut self, config: RedisConfig) -> Self {
102 self.addr = Some(format!(
103 "redis://{}:{}@{}:{}",
104 config.user, config.password, config.endpoint, config.port
105 ));
106 self
107 }
108
109 pub fn addr(mut self, addr: &str) -> Self {
110 self.addr = Some(addr.to_string());
111 self
112 }
113
114 pub fn build(self) -> RedisStorage<K, V> {
115 let addr = self.addr.clone().map_or_else(
116 || panic!("Empty url, use `config` or `url` method before building storage!"),
117 |addr| addr,
118 );
119
120 let mut client = redis::Client::open(addr).unwrap();
121 let _: () = client.set("ping".to_string(), "pong".to_string()).unwrap();
122
123 RedisStorage {
124 client,
125 _marker: self._marker,
126 }
127 }
128
129 pub fn try_build(self) -> Result<RedisStorage<K, V>, Err> {
130 let addr = self.addr.clone().map_or_else(
131 || Err("Empty url, use `config` or `url` method before building storage!"),
132 |addr| Ok(addr),
133 )?;
134
135 let mut client = redis::Client::open(addr)?;
136 let _: () = client.set("ping".to_string(), "pong".to_string())?;
137
138 Ok(RedisStorage {
139 client,
140 _marker: self._marker,
141 })
142 }
143}
144
145impl<K, V> Default for RedisStorageBuilder<K, V>
146where
147 K: ToRedisArgs,
148 V: Into<String>,
149{
150 fn default() -> Self {
151 Self {
152 addr: None,
153 _marker: PhantomData,
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
159pub struct RedisConfig {
160 pub user: String,
161 pub password: String,
162 pub endpoint: String,
163 pub port: usize,
164}
165
166impl Display for RedisConfig {
167 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 fmt.write_fmt(format_args!(
169 "redis://{}:{}@{}:{}",
170 self.user, self.password, self.endpoint, self.port
171 ))
172 }
173}
174
175fn caused_by_nil_response(e: &RedisError) -> bool {
176 e.to_string().eq("Response was of incompatible type: \"Response type not string compatible.\" (response was nil)")
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use std::time::Duration;
183
184 #[test]
185 fn test_contains() {
186 let storage = build_localhost::<String, String>();
187
188 let _ = storage
189 .set("name".to_string(), "Ferris".to_string())
190 .unwrap();
191 let _ = storage.contains("name".into()).unwrap();
192 }
193
194 #[test]
195 fn test_get() {
196 let storage = build_localhost();
197
198 let (key, value) = ("name", "Ferris".to_string());
199 let _ = storage.set(key, value.clone());
200 let resp = storage.get(key).unwrap();
201 assert_eq!(resp, Some(value));
202
203 let _ = storage.del(key).unwrap();
204 let resp = storage.get(key).unwrap();
205 assert_eq!(resp, None);
206 }
207
208 #[test]
209 fn test_set_ex() {
210 let storage = build_localhost();
211 let (key, value) = ("set_ex_test", "ok!".to_string());
212 let _ = storage
213 .set_ex(key, value.clone(), Duration::from_secs(3))
214 .unwrap();
215 let resp = storage.get(key).unwrap();
216 assert_eq!(resp, Some(value));
217 std::thread::sleep(std::time::Duration::from_secs(3));
218 let resp = storage.get(key).unwrap();
219 assert_eq!(resp, None);
220 }
221
222 #[test]
223 fn test_build() {
224 let _ = build_localhost::<String, String>();
225 }
226
227 #[test]
228 fn test_try_build() {
229 match RedisStorageBuilder::<String, String>::new()
230 .addr("redis://127.0.0.1:6379")
231 .try_build()
232 {
233 Ok(_) => println!("storage has been successfully built!"),
234 Err(e) => eprintln!("got an error: {:?}", e),
235 }
236 }
237
238 fn build_localhost<K: ToRedisArgs, V: Into<String>>() -> RedisStorage<K, V> {
239 RedisStorageBuilder::<K, V>::new()
240 .addr("redis://127.0.0.1:6379")
241 .build()
242 }
243}