storage_trait/
redis_storage.rs

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}