Skip to main content

actix_jwt/store/
factory.rs

1//! Store factory for creating token stores from configuration.
2//!
3//! Mirrors
4//! [`store/factory.go`](https://github.com/LdDl/echo-jwt/blob/master/store/factory.go)
5//! from the Go implementation.
6
7use crate::core::TokenStore;
8use crate::errors::JwtError;
9
10use super::memory::InMemoryRefreshTokenStore;
11
12/// Selects which storage backend to create.
13///
14/// # Examples
15///
16/// ```
17/// use actix_jwt::store::factory::StoreType;
18///
19/// let st = StoreType::Memory;
20/// assert_eq!(st, StoreType::Memory);
21/// ```
22#[derive(Debug, Clone, PartialEq)]
23pub enum StoreType {
24    /// In-memory store (no external dependencies).
25    Memory,
26    /// Redis-backed store (requires the `redis-store` feature).
27    Redis,
28}
29
30/// Configuration passed to [`Factory::create_store`].
31///
32/// # Examples
33///
34/// ```
35/// use actix_jwt::store::factory::{StoreConfig, StoreType};
36///
37/// let config = StoreConfig::default();
38/// assert_eq!(config.store_type, StoreType::Memory);
39/// ```
40pub struct StoreConfig {
41    /// Which backend to instantiate.
42    pub store_type: StoreType,
43    /// Redis-specific configuration (only used when `store_type` is
44    /// [`StoreType::Redis`] **and** the `redis-store` feature is enabled).
45    #[cfg(feature = "redis-store")]
46    pub redis: Option<super::redis::RedisConfig>,
47}
48
49impl Default for StoreConfig {
50    fn default() -> Self {
51        Self {
52            store_type: StoreType::Memory,
53            #[cfg(feature = "redis-store")]
54            redis: None,
55        }
56    }
57}
58
59/// Factory for creating [`TokenStore`] instances from a [`StoreConfig`].
60///
61/// # Examples
62///
63/// ```
64/// use actix_jwt::store::factory::{Factory, StoreConfig};
65///
66/// # #[tokio::main]
67/// # async fn main() {
68/// let factory = Factory::new();
69/// let store = factory.create_store(&StoreConfig::default()).await.unwrap();
70/// assert_eq!(store.count().await.unwrap(), 0);
71/// # }
72/// ```
73pub struct Factory;
74
75impl Factory {
76    /// Creates a new factory instance.
77    pub fn new() -> Self {
78        Factory
79    }
80
81    /// Creates a [`TokenStore`] based on the provided configuration.
82    ///
83    /// # Errors
84    ///
85    /// * [`JwtError::Internal`] - when `StoreType::Redis` is requested but
86    ///   the `redis-store` feature is not enabled, or when the Redis
87    ///   connection fails.
88    pub async fn create_store(
89        &self,
90        config: &StoreConfig,
91    ) -> Result<Box<dyn TokenStore>, JwtError> {
92        match config.store_type {
93            StoreType::Memory => Ok(Box::new(InMemoryRefreshTokenStore::new())),
94            StoreType::Redis => {
95                #[cfg(feature = "redis-store")]
96                {
97                    let redis_config = config.redis.clone().unwrap_or_default();
98                    let store = super::redis::RedisRefreshTokenStore::new(&redis_config).await?;
99                    Ok(Box::new(store))
100                }
101                #[cfg(not(feature = "redis-store"))]
102                {
103                    Err(JwtError::Internal(
104                        "Redis store feature not enabled. Enable the 'redis-store' feature in Cargo.toml".into(),
105                    ))
106                }
107            }
108        }
109    }
110}
111
112impl Default for Factory {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118/// Creates an in-memory token store.
119///
120/// Shorthand for `Box::new(InMemoryRefreshTokenStore::new())`.
121///
122/// # Examples
123///
124/// ```
125/// # #[tokio::main]
126/// # async fn main() {
127/// let store = actix_jwt::store::new_memory_store();
128/// assert_eq!(store.count().await.unwrap(), 0);
129/// # }
130/// ```
131pub fn new_memory_store() -> Box<dyn TokenStore> {
132    Box::new(InMemoryRefreshTokenStore::new())
133}
134
135/// Same as [`new_memory_store`] but mirrors
136/// [`MustNewMemoryStore`](https://github.com/LdDl/echo-jwt/blob/master/store/factory.go)
137/// from the Go implementation, which panics on error.  Since the in-memory store is infallible this
138/// function never panics, but the name is kept for API parity.
139///
140/// # Examples
141///
142/// ```
143/// # #[tokio::main]
144/// # async fn main() {
145/// let store = actix_jwt::store::factory::must_new_memory_store();
146/// assert_eq!(store.count().await.unwrap(), 0);
147/// # }
148/// ```
149pub fn must_new_memory_store() -> Box<dyn TokenStore> {
150    new_memory_store()
151}
152
153/// Creates a token store from the given configuration using the default
154/// [`Factory`].
155///
156/// # Examples
157///
158/// ```
159/// use actix_jwt::store::factory::{new_store, StoreConfig};
160///
161/// # #[tokio::main]
162/// # async fn main() {
163/// let store = new_store(&StoreConfig::default()).await.unwrap();
164/// assert_eq!(store.count().await.unwrap(), 0);
165/// # }
166/// ```
167pub async fn new_store(config: &StoreConfig) -> Result<Box<dyn TokenStore>, JwtError> {
168    Factory::new().create_store(config).await
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[tokio::test]
176    async fn test_factory_create_store_memory() {
177        let factory = Factory::new();
178        let config = StoreConfig {
179            store_type: StoreType::Memory,
180            #[cfg(feature = "redis-store")]
181            redis: None,
182        };
183
184        let store = factory.create_store(&config).await;
185        assert!(
186            store.is_ok(),
187            "Factory should create memory store successfully"
188        );
189
190        // Verify the store works
191        let store = store.unwrap();
192        let count = store.count().await.unwrap();
193        assert_eq!(count, 0, "New memory store should be empty");
194    }
195
196    #[tokio::test]
197    async fn test_factory_default_config() {
198        let factory = Factory::new();
199        let config = StoreConfig::default();
200
201        assert_eq!(
202            config.store_type,
203            StoreType::Memory,
204            "Default config should use Memory type"
205        );
206
207        let store = factory.create_store(&config).await;
208        assert!(
209            store.is_ok(),
210            "Factory should create store from default config"
211        );
212    }
213
214    #[tokio::test]
215    async fn test_new_store_memory() {
216        let config = StoreConfig {
217            store_type: StoreType::Memory,
218            #[cfg(feature = "redis-store")]
219            redis: None,
220        };
221
222        let store = new_store(&config).await;
223        assert!(
224            store.is_ok(),
225            "new_store() should create memory store successfully"
226        );
227
228        let store = store.unwrap();
229        let count = store.count().await.unwrap();
230        assert_eq!(count, 0);
231    }
232
233    #[tokio::test]
234    async fn test_new_memory_store() {
235        let store = new_memory_store();
236        let count = store.count().await.unwrap();
237        assert_eq!(count, 0, "new_memory_store() should return an empty store");
238    }
239
240    #[tokio::test]
241    async fn test_default_store() {
242        let store = crate::store::default_store();
243        let count = store.count().await.unwrap();
244        assert_eq!(count, 0, "default_store() should return an empty store");
245    }
246
247    #[tokio::test]
248    async fn test_must_new_memory_store() {
249        let store = must_new_memory_store();
250        let count = store.count().await.unwrap();
251        assert_eq!(
252            count, 0,
253            "must_new_memory_store() should return an empty store"
254        );
255    }
256
257    #[cfg(not(feature = "redis-store"))]
258    #[tokio::test]
259    async fn test_factory_create_store_redis_without_feature() {
260        let factory = Factory::new();
261        let config = StoreConfig {
262            store_type: StoreType::Redis,
263        };
264
265        let result = factory.create_store(&config).await;
266        assert!(
267            result.is_err(),
268            "Creating Redis store without feature should fail"
269        );
270
271        let err_msg = match result {
272            Err(e) => e.to_string(),
273            Ok(_) => panic!("Expected error but got Ok"),
274        };
275        assert!(
276            err_msg.contains("Redis store feature not enabled"),
277            "Error should mention feature not enabled, got: {}",
278            err_msg
279        );
280    }
281}