async_redis_session_v2/
lib.rs

1//! # async-redis-session
2//! ```rust
3//! use async_redis_session::RedisSessionStore;
4//! use async_session::{Session, SessionStore};
5//!
6//! # fn main() -> async_session::Result { async_std::task::block_on(async {
7//! let store = RedisSessionStore::new("redis://127.0.0.1/")?;
8//!
9//! let mut session = Session::new();
10//! session.insert("key", "value")?;
11//!
12//! let cookie_value = store.store_session(session).await?.unwrap();
13//! let session = store.load_session(cookie_value).await?.unwrap();
14//! assert_eq!(&session.get::<String>("key").unwrap(), "value");
15//! # Ok(()) }) }
16//! ```
17
18#![forbid(unsafe_code, future_incompatible)]
19#![deny(
20    missing_debug_implementations,
21    nonstandard_style,
22    missing_docs,
23    unreachable_pub,
24    missing_copy_implementations,
25    unused_qualifications
26)]
27
28use async_session::{async_trait, serde_json, Result, Session, SessionStore};
29use redis::{aio::Connection, AsyncCommands, Client, IntoConnectionInfo, RedisResult};
30
31/// # RedisSessionStore
32#[derive(Clone, Debug)]
33pub struct RedisSessionStore {
34    client: Client,
35    prefix: Option<String>,
36}
37
38impl RedisSessionStore {
39    /// creates a redis store from an existing [`redis::Client`]
40    /// ```rust
41    /// # use async_redis_session::RedisSessionStore;
42    /// let client = redis::Client::open("redis://127.0.0.1").unwrap();
43    /// let store = RedisSessionStore::from_client(client);
44    /// ```
45    pub fn from_client(client: Client) -> Self {
46        Self {
47            client,
48            prefix: None,
49        }
50    }
51
52    /// creates a redis store from a [`redis::IntoConnectionInfo`]
53    /// such as a [`String`], [`&str`](str), or [`Url`](../url/struct.Url.html)
54    /// ```rust
55    /// # use async_redis_session::RedisSessionStore;
56    /// let store = RedisSessionStore::new("redis://127.0.0.1").unwrap();
57    /// ```
58    pub fn new(connection_info: impl IntoConnectionInfo) -> RedisResult<Self> {
59        Ok(Self::from_client(Client::open(connection_info)?))
60    }
61
62    /// sets a key prefix for this session store
63    ///
64    /// ```rust
65    /// # use async_redis_session::RedisSessionStore;
66    /// let store = RedisSessionStore::new("redis://127.0.0.1").unwrap()
67    ///     .with_prefix("async-sessions/");
68    /// ```
69    /// ```rust
70    /// # use async_redis_session::RedisSessionStore;
71    /// let client = redis::Client::open("redis://127.0.0.1").unwrap();
72    /// let store = RedisSessionStore::from_client(client)
73    ///     .with_prefix("async-sessions/");
74    /// ```
75    pub fn with_prefix(mut self, prefix: impl AsRef<str>) -> Self {
76        self.prefix = Some(prefix.as_ref().to_owned());
77        self
78    }
79
80    async fn ids(&self) -> Result<Vec<String>> {
81        Ok(self.connection().await?.keys(self.prefix_key("*")).await?)
82    }
83
84    /// returns the number of sessions in this store
85    pub async fn count(&self) -> Result<usize> {
86        if self.prefix.is_none() {
87            let mut connection = self.connection().await?;
88            Ok(redis::cmd("DBSIZE").query_async(&mut connection).await?)
89        } else {
90            Ok(self.ids().await?.len())
91        }
92    }
93
94    #[cfg(test)]
95    async fn ttl_for_session(&self, session: &Session) -> Result<usize> {
96        Ok(self
97            .connection()
98            .await?
99            .ttl(self.prefix_key(session.id()))
100            .await?)
101    }
102
103    fn prefix_key(&self, key: impl AsRef<str>) -> String {
104        if let Some(ref prefix) = self.prefix {
105            format!("{}{}", prefix, key.as_ref())
106        } else {
107            key.as_ref().into()
108        }
109    }
110
111    async fn connection(&self) -> RedisResult<Connection> {
112        self.client.get_async_std_connection().await
113    }
114}
115
116#[async_trait]
117impl SessionStore for RedisSessionStore {
118    async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
119        let id = Session::id_from_cookie_value(&cookie_value)?;
120        let mut connection = self.connection().await?;
121        let record: Option<String> = connection.get(self.prefix_key(id)).await?;
122        match record {
123            Some(value) => Ok(serde_json::from_str(&value)?),
124            None => Ok(None),
125        }
126    }
127
128    async fn store_session(&self, session: Session) -> Result<Option<String>> {
129        let id = self.prefix_key(session.id());
130        let string = serde_json::to_string(&session)?;
131
132        let mut connection = self.connection().await?;
133
134        match session.expires_in() {
135            None => connection.set(id, string).await?,
136
137            Some(expiry) => connection.set_ex(id, string, expiry.as_secs()).await?,
138        };
139
140        Ok(session.into_cookie_value())
141    }
142
143    async fn destroy_session(&self, session: Session) -> Result {
144        let mut connection = self.connection().await?;
145        let key = self.prefix_key(session.id().to_string());
146        connection.del(key).await?;
147        Ok(())
148    }
149
150    async fn clear_store(&self) -> Result {
151        let mut connection = self.connection().await?;
152
153        if self.prefix.is_none() {
154            let _: () = redis::cmd("FLUSHDB").query_async(&mut connection).await?;
155        } else {
156            let ids = self.ids().await?;
157            if !ids.is_empty() {
158                connection.del(ids).await?;
159            }
160        }
161        Ok(())
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use async_std::task;
169    use std::time::Duration;
170
171    async fn test_store() -> RedisSessionStore {
172        let store = RedisSessionStore::new("redis://127.0.0.1").unwrap();
173        store.clear_store().await.unwrap();
174        store
175    }
176
177    #[async_std::test]
178    async fn creating_a_new_session_with_no_expiry() -> Result {
179        let store = test_store().await;
180        let mut session = Session::new();
181        session.insert("key", "value")?;
182        let cloned = session.clone();
183        let cookie_value = store.store_session(session).await?.unwrap();
184
185        let loaded_session = store.load_session(cookie_value).await?.unwrap();
186        assert_eq!(cloned.id(), loaded_session.id());
187        assert_eq!("value", &loaded_session.get::<String>("key").unwrap());
188
189        assert!(!loaded_session.is_expired());
190        Ok(())
191    }
192
193    #[async_std::test]
194    async fn updating_a_session() -> Result {
195        let store = test_store().await;
196        let mut session = Session::new();
197
198        session.insert("key", "value")?;
199        let cookie_value = store.store_session(session).await?.unwrap();
200
201        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
202        session.insert("key", "other value")?;
203        assert_eq!(None, store.store_session(session).await?);
204
205        let session = store.load_session(cookie_value.clone()).await?.unwrap();
206        assert_eq!(&session.get::<String>("key").unwrap(), "other value");
207
208        assert_eq!(1, store.count().await.unwrap());
209        Ok(())
210    }
211
212    #[async_std::test]
213    async fn updating_a_session_extending_expiry() -> Result {
214        let store = test_store().await;
215        let mut session = Session::new();
216        session.expire_in(Duration::from_secs(5));
217        let original_expires = session.expiry().unwrap().clone();
218        let cookie_value = store.store_session(session).await?.unwrap();
219
220        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
221        let ttl = store.ttl_for_session(&session).await?;
222        assert!(ttl > 3 && ttl < 5);
223
224        assert_eq!(session.expiry().unwrap(), &original_expires);
225        session.expire_in(Duration::from_secs(10));
226        let new_expires = session.expiry().unwrap().clone();
227        store.store_session(session).await?;
228
229        let session = store.load_session(cookie_value.clone()).await?.unwrap();
230        let ttl = store.ttl_for_session(&session).await?;
231        assert!(ttl > 8 && ttl < 10);
232        assert_eq!(session.expiry().unwrap(), &new_expires);
233
234        assert_eq!(1, store.count().await.unwrap());
235
236        task::sleep(Duration::from_secs(10)).await;
237        assert_eq!(0, store.count().await.unwrap());
238
239        Ok(())
240    }
241
242    #[async_std::test]
243    async fn creating_a_new_session_with_expiry() -> Result {
244        let store = test_store().await;
245        let mut session = Session::new();
246        session.expire_in(Duration::from_secs(3));
247        session.insert("key", "value")?;
248        let cloned = session.clone();
249
250        let cookie_value = store.store_session(session).await?.unwrap();
251
252        assert!(store.ttl_for_session(&cloned).await? > 1);
253
254        let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
255        assert_eq!(cloned.id(), loaded_session.id());
256        assert_eq!("value", &loaded_session.get::<String>("key").unwrap());
257
258        assert!(!loaded_session.is_expired());
259
260        task::sleep(Duration::from_secs(2)).await;
261        assert_eq!(None, store.load_session(cookie_value).await?);
262
263        Ok(())
264    }
265
266    #[async_std::test]
267    async fn destroying_a_single_session() -> Result {
268        let store = test_store().await;
269        for _ in 0..3i8 {
270            store.store_session(Session::new()).await?;
271        }
272
273        let cookie = store.store_session(Session::new()).await?.unwrap();
274        assert_eq!(4, store.count().await?);
275        let session = store.load_session(cookie.clone()).await?.unwrap();
276        store.destroy_session(session.clone()).await.unwrap();
277        assert_eq!(None, store.load_session(cookie).await?);
278        assert_eq!(3, store.count().await?);
279
280        // attempting to destroy the session again is not an error
281        assert!(store.destroy_session(session).await.is_ok());
282        Ok(())
283    }
284
285    #[async_std::test]
286    async fn clearing_the_whole_store() -> Result {
287        let store = test_store().await;
288        for _ in 0..3i8 {
289            store.store_session(Session::new()).await?;
290        }
291
292        assert_eq!(3, store.count().await?);
293        store.clear_store().await.unwrap();
294        assert_eq!(0, store.count().await?);
295
296        Ok(())
297    }
298
299    #[async_std::test]
300    async fn prefixes() -> Result {
301        test_store().await; // clear the db
302
303        let store = RedisSessionStore::new("redis://127.0.0.1")?.with_prefix("sessions/");
304        store.clear_store().await?;
305
306        for _ in 0..3i8 {
307            store.store_session(Session::new()).await?;
308        }
309
310        let mut session = Session::new();
311
312        session.insert("key", "value")?;
313        let cookie_value = store.store_session(session).await?.unwrap();
314
315        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
316        session.insert("key", "other value")?;
317        assert_eq!(None, store.store_session(session).await?);
318
319        let session = store.load_session(cookie_value.clone()).await?.unwrap();
320        assert_eq!(&session.get::<String>("key").unwrap(), "other value");
321
322        assert_eq!(4, store.count().await.unwrap());
323
324        let other_store =
325            RedisSessionStore::new("redis://127.0.0.1")?.with_prefix("other-namespace/");
326
327        assert_eq!(0, other_store.count().await.unwrap());
328        for _ in 0..3i8 {
329            other_store.store_session(Session::new()).await?;
330        }
331
332        other_store.clear_store().await?;
333
334        assert_eq!(0, other_store.count().await?);
335        assert_eq!(4, store.count().await?);
336
337        Ok(())
338    }
339}