nonce_auth/nonce/storage/
memory.rs

1//! In-memory storage backend implementation.
2//!
3//! This module provides a simple in-memory storage backend that uses a HashMap
4//! for nonce persistence. It's ideal for testing, development, and single-instance
5//! applications where persistence across restarts is not required.
6
7use super::{NonceEntry, NonceStorage, StorageStats};
8use crate::NonceError;
9use crate::nonce::time_utils;
10use async_trait::async_trait;
11use std::collections::HashMap;
12use std::time::Duration;
13
14/// A simple in-memory storage implementation for testing and single-instance applications.
15///
16/// This implementation uses a `HashMap` wrapped in `Arc<RwLock<>>` for
17/// thread-safe access. It doesn't persist data across restarts and doesn't
18/// implement automatic expiration (expired entries are only removed during
19/// cleanup operations).
20///
21/// # Features
22///
23/// - **Zero dependencies**: No external storage dependencies required
24/// - **Thread-safe**: Uses tokio's RwLock for concurrent access
25/// - **Fast operations**: All operations are in-memory and very fast
26/// - **Context isolation**: Supports nonce namespacing via contexts
27/// - **No persistence**: Data is lost when the application restarts
28/// - **Pre-allocated capacity**: Optional capacity hint for better performance
29/// - **Batch operations**: Support for bulk operations
30///
31/// # Use Cases
32///
33/// - Development and testing environments
34/// - Single-instance applications with short-lived nonces
35/// - Applications that don't require persistence across restarts
36/// - Proof-of-concept implementations
37///
38/// # Example
39///
40/// ```rust
41/// use nonce_auth::storage::{MemoryStorage, NonceStorage};
42/// use std::time::Duration;
43///
44/// # async fn example() -> Result<(), nonce_auth::NonceError> {
45/// let storage = MemoryStorage::new();
46///
47/// // Store a nonce
48/// storage.set("test-nonce", None, Duration::from_secs(300)).await?;
49///
50/// // Check if it exists
51/// let exists = storage.exists("test-nonce", None).await?;
52/// assert!(exists);
53///
54/// // Get the entry
55/// let entry = storage.get("test-nonce", None).await?;
56/// assert!(entry.is_some());
57/// # Ok(())
58/// # }
59/// ```
60#[derive(Debug)]
61pub struct MemoryStorage {
62    data: std::sync::Arc<tokio::sync::RwLock<HashMap<String, NonceEntry>>>,
63}
64
65impl MemoryStorage {
66    /// Creates a new in-memory storage instance.
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// use nonce_auth::storage::MemoryStorage;
72    ///
73    /// let storage = MemoryStorage::new();
74    /// ```
75    pub fn new() -> Self {
76        Self {
77            data: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
78        }
79    }
80
81    /// Creates a new in-memory storage instance with pre-allocated capacity.
82    ///
83    /// This can improve performance when you know approximately how many nonces
84    /// you'll be storing, as it avoids HashMap reallocations.
85    ///
86    /// # Arguments
87    ///
88    /// * `capacity` - Initial capacity hint for the internal HashMap
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// use nonce_auth::storage::MemoryStorage;
94    ///
95    /// // Pre-allocate for ~1000 nonces
96    /// let storage = MemoryStorage::with_capacity(1000);
97    /// ```
98    pub fn with_capacity(capacity: usize) -> Self {
99        Self {
100            data: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::with_capacity(capacity))),
101        }
102    }
103
104    /// Creates a storage key from nonce and context.
105    ///
106    /// The key format is "nonce:context" where context defaults to empty string.
107    /// This ensures that the same nonce can exist in different contexts.
108    fn make_key(nonce: &str, context: Option<&str>) -> String {
109        match context {
110            Some(ctx) => {
111                let mut key = String::with_capacity(nonce.len() + ctx.len() + 1);
112                key.push_str(nonce);
113                key.push(':');
114                key.push_str(ctx);
115                key
116            }
117            None => {
118                let mut key = String::with_capacity(nonce.len() + 1);
119                key.push_str(nonce);
120                key.push(':');
121                key
122            }
123        }
124    }
125}
126
127#[async_trait]
128impl NonceStorage for MemoryStorage {
129    async fn get(
130        &self,
131        nonce: &str,
132        context: Option<&str>,
133    ) -> Result<Option<NonceEntry>, NonceError> {
134        let key = Self::make_key(nonce, context);
135        let data = self.data.read().await;
136        Ok(data.get(&key).cloned())
137    }
138
139    async fn set(
140        &self,
141        nonce: &str,
142        context: Option<&str>,
143        _ttl: Duration,
144    ) -> Result<(), NonceError> {
145        let key = Self::make_key(nonce, context);
146        let entry = NonceEntry {
147            nonce: nonce.to_string(),
148            created_at: time_utils::current_timestamp()?,
149            context: context.map(|s| s.to_string()),
150        };
151
152        let mut data = self.data.write().await;
153        if data.contains_key(&key) {
154            return Err(NonceError::DuplicateNonce);
155        }
156        data.insert(key, entry);
157        Ok(())
158    }
159
160    async fn exists(&self, nonce: &str, context: Option<&str>) -> Result<bool, NonceError> {
161        let key = Self::make_key(nonce, context);
162        let data = self.data.read().await;
163        Ok(data.contains_key(&key))
164    }
165
166    async fn cleanup_expired(&self, cutoff_time: i64) -> Result<usize, NonceError> {
167        let mut data = self.data.write().await;
168        let initial_count = data.len();
169        data.retain(|_, entry| entry.created_at > cutoff_time);
170        Ok(initial_count - data.len())
171    }
172
173    async fn get_stats(&self) -> Result<StorageStats, NonceError> {
174        let data = self.data.read().await;
175
176        // More accurate memory usage calculation
177        let base_entry_size = std::mem::size_of::<NonceEntry>();
178        let mut total_memory = data.len() * base_entry_size;
179
180        // Add string storage overhead
181        for (key, entry) in data.iter() {
182            total_memory += key.len(); // Key string
183            total_memory += entry.nonce.len(); // Nonce string
184            if let Some(ctx) = &entry.context {
185                total_memory += ctx.len(); // Context string
186            }
187        }
188
189        // Add HashMap overhead (approximate)
190        total_memory += data.capacity() * std::mem::size_of::<(String, NonceEntry)>();
191
192        Ok(StorageStats {
193            total_records: data.len(),
194            backend_info: format!(
195                "In-memory HashMap storage (~{} bytes, capacity: {})",
196                total_memory,
197                data.capacity()
198            ),
199        })
200    }
201}
202
203/// Batch operations support for better performance
204impl MemoryStorage {
205    /// Insert multiple nonces in a batch operation.
206    ///
207    /// This method acquires the write lock once and performs all insertions,
208    /// which can be more efficient than individual set operations.
209    ///
210    /// # Arguments
211    ///
212    /// * `nonces` - Vector of (nonce, context) tuples to insert
213    /// * `_ttl` - Time-to-live (not used in memory storage but kept for consistency)
214    ///
215    /// # Returns
216    ///
217    /// Number of successfully inserted nonces (duplicates are skipped)
218    ///
219    /// # Example
220    ///
221    /// ```rust
222    /// use nonce_auth::storage::MemoryStorage;
223    /// use std::time::Duration;
224    ///
225    /// # async fn example() -> Result<(), nonce_auth::NonceError> {
226    /// let storage = MemoryStorage::new();
227    /// let nonces = vec![
228    ///     ("nonce1", None),
229    ///     ("nonce2", Some("ctx1")),
230    ///     ("nonce3", Some("ctx2")),
231    /// ];
232    ///
233    /// let inserted = storage.batch_set(nonces, Duration::from_secs(300)).await?;
234    /// assert_eq!(inserted, 3);
235    /// # Ok(())
236    /// # }
237    /// ```
238    pub async fn batch_set(
239        &self,
240        nonces: Vec<(&str, Option<&str>)>,
241        _ttl: Duration,
242    ) -> Result<usize, NonceError> {
243        let created_at = time_utils::current_timestamp()?;
244        let mut data = self.data.write().await;
245        let mut success_count = 0;
246
247        for (nonce, context) in nonces {
248            let key = Self::make_key(nonce, context);
249
250            if let std::collections::hash_map::Entry::Vacant(e) = data.entry(key) {
251                let entry = NonceEntry {
252                    nonce: nonce.to_string(),
253                    created_at,
254                    context: context.map(|s| s.to_string()),
255                };
256                e.insert(entry);
257                success_count += 1;
258            }
259            // Skip duplicates silently in batch operation
260        }
261
262        Ok(success_count)
263    }
264
265    /// Check existence of multiple nonces in a batch operation.
266    ///
267    /// This method acquires the read lock once and checks all nonces,
268    /// which can be more efficient than individual exists operations.
269    ///
270    /// # Arguments
271    ///
272    /// * `nonces` - Vector of (nonce, context) tuples to check
273    ///
274    /// # Returns
275    ///
276    /// Vector of boolean values indicating existence for each nonce
277    ///
278    /// # Example
279    ///
280    /// ```rust
281    /// use nonce_auth::storage::MemoryStorage;
282    ///
283    /// # async fn example() -> Result<(), nonce_auth::NonceError> {
284    /// let storage = MemoryStorage::new();
285    /// let check_nonces = vec![
286    ///     ("nonce1", None),
287    ///     ("nonce2", Some("ctx1")),
288    /// ];
289    ///
290    /// let results = storage.batch_exists(check_nonces).await?;
291    /// # Ok(())
292    /// # }
293    /// ```
294    pub async fn batch_exists(
295        &self,
296        nonces: Vec<(&str, Option<&str>)>,
297    ) -> Result<Vec<bool>, NonceError> {
298        let data = self.data.read().await;
299        let mut results = Vec::with_capacity(nonces.len());
300
301        for (nonce, context) in nonces {
302            let key = Self::make_key(nonce, context);
303            results.push(data.contains_key(&key));
304        }
305
306        Ok(results)
307    }
308
309    /// Get multiple nonces in a batch operation.
310    ///
311    /// This method acquires the read lock once and retrieves all nonces,
312    /// which can be more efficient than individual get operations.
313    ///
314    /// # Arguments
315    ///
316    /// * `nonces` - Vector of (nonce, context) tuples to retrieve
317    ///
318    /// # Returns
319    ///
320    /// Vector of optional NonceEntry values
321    ///
322    /// # Example
323    ///
324    /// ```rust
325    /// use nonce_auth::storage::MemoryStorage;
326    ///
327    /// # async fn example() -> Result<(), nonce_auth::NonceError> {
328    /// let storage = MemoryStorage::new();
329    /// let get_nonces = vec![
330    ///     ("nonce1", None),
331    ///     ("nonce2", Some("ctx1")),
332    /// ];
333    ///
334    /// let results = storage.batch_get(get_nonces).await?;
335    /// # Ok(())
336    /// # }
337    /// ```
338    pub async fn batch_get(
339        &self,
340        nonces: Vec<(&str, Option<&str>)>,
341    ) -> Result<Vec<Option<NonceEntry>>, NonceError> {
342        let data = self.data.read().await;
343        let mut results = Vec::with_capacity(nonces.len());
344
345        for (nonce, context) in nonces {
346            let key = Self::make_key(nonce, context);
347            results.push(data.get(&key).cloned());
348        }
349
350        Ok(results)
351    }
352}
353
354impl Default for MemoryStorage {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use std::time::{SystemTime, UNIX_EPOCH};
364
365    #[tokio::test]
366    async fn test_memory_storage_basic_operations() -> Result<(), NonceError> {
367        let storage = MemoryStorage::new();
368
369        // Test set and exists
370        storage
371            .set("test-nonce", None, Duration::from_secs(300))
372            .await?;
373        assert!(storage.exists("test-nonce", None).await?);
374
375        // Test get
376        let entry = storage.get("test-nonce", None).await?;
377        assert!(entry.is_some());
378        let entry = entry.unwrap();
379        assert_eq!(entry.nonce, "test-nonce");
380        assert!(entry.context.is_none());
381
382        Ok(())
383    }
384
385    #[tokio::test]
386    async fn test_memory_storage_duplicate_nonce() -> Result<(), NonceError> {
387        let storage = MemoryStorage::new();
388
389        // First set should succeed
390        storage
391            .set("test-nonce", None, Duration::from_secs(300))
392            .await?;
393
394        // Second set should fail
395        let result = storage
396            .set("test-nonce", None, Duration::from_secs(300))
397            .await;
398        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
399
400        Ok(())
401    }
402
403    #[tokio::test]
404    async fn test_memory_storage_context_isolation() -> Result<(), NonceError> {
405        let storage = MemoryStorage::new();
406
407        // Same nonce, different contexts should work
408        storage
409            .set("test-nonce", Some("context1"), Duration::from_secs(300))
410            .await?;
411        storage
412            .set("test-nonce", Some("context2"), Duration::from_secs(300))
413            .await?;
414
415        // Both should exist
416        assert!(storage.exists("test-nonce", Some("context1")).await?);
417        assert!(storage.exists("test-nonce", Some("context2")).await?);
418
419        // But not in wrong context
420        assert!(!storage.exists("test-nonce", Some("context3")).await?);
421
422        Ok(())
423    }
424
425    #[tokio::test]
426    async fn test_memory_storage_cleanup() -> Result<(), NonceError> {
427        let storage = MemoryStorage::new();
428
429        // Add some nonces
430        storage
431            .set("old-nonce", None, Duration::from_secs(300))
432            .await?;
433        storage
434            .set("new-nonce", None, Duration::from_secs(300))
435            .await?;
436
437        // Cleanup with cutoff time in the future should remove all
438        let future_time = SystemTime::now()
439            .duration_since(UNIX_EPOCH)
440            .unwrap()
441            .as_secs() as i64
442            + 3600;
443
444        let removed = storage.cleanup_expired(future_time).await?;
445        assert_eq!(removed, 2);
446
447        // Both should be gone
448        assert!(!storage.exists("old-nonce", None).await?);
449        assert!(!storage.exists("new-nonce", None).await?);
450
451        Ok(())
452    }
453
454    #[tokio::test]
455    async fn test_memory_storage_stats() -> Result<(), NonceError> {
456        let storage = MemoryStorage::new();
457
458        // Initial stats
459        let stats = storage.get_stats().await?;
460        assert_eq!(stats.total_records, 0);
461        assert!(stats.backend_info.contains("In-memory"));
462
463        // Add some nonces
464        storage
465            .set("nonce1", None, Duration::from_secs(300))
466            .await?;
467        storage
468            .set("nonce2", Some("context"), Duration::from_secs(300))
469            .await?;
470
471        // Updated stats
472        let stats = storage.get_stats().await?;
473        assert_eq!(stats.total_records, 2);
474        assert!(stats.backend_info.contains("In-memory"));
475        assert!(stats.backend_info.contains("bytes"));
476        assert!(stats.backend_info.contains("capacity"));
477
478        Ok(())
479    }
480
481    #[tokio::test]
482    async fn test_memory_storage_with_capacity() -> Result<(), NonceError> {
483        let storage = MemoryStorage::with_capacity(100);
484
485        // Add some nonces
486        storage.set("test1", None, Duration::from_secs(300)).await?;
487        storage
488            .set("test2", Some("ctx"), Duration::from_secs(300))
489            .await?;
490
491        let stats = storage.get_stats().await?;
492        assert_eq!(stats.total_records, 2);
493        // Should have pre-allocated capacity
494        assert!(stats.backend_info.contains("capacity"));
495
496        Ok(())
497    }
498
499    #[tokio::test]
500    async fn test_batch_operations() -> Result<(), NonceError> {
501        let storage = MemoryStorage::new();
502
503        // Test batch_set
504        let nonces = vec![
505            ("batch1", None),
506            ("batch2", Some("ctx1")),
507            ("batch3", Some("ctx2")),
508            ("batch1", None), // Duplicate, should be skipped
509        ];
510
511        let inserted = storage.batch_set(nonces, Duration::from_secs(300)).await?;
512        assert_eq!(inserted, 3); // Only 3 unique nonces inserted
513
514        // Test batch_exists
515        let check_nonces = vec![
516            ("batch1", None),
517            ("batch2", Some("ctx1")),
518            ("batch3", Some("ctx2")),
519            ("batch4", None), // Doesn't exist
520        ];
521
522        let exists_results = storage.batch_exists(check_nonces).await?;
523        assert_eq!(exists_results, vec![true, true, true, false]);
524
525        // Test batch_get
526        let get_nonces = vec![
527            ("batch1", None),
528            ("batch4", None), // Doesn't exist
529        ];
530
531        let get_results = storage.batch_get(get_nonces).await?;
532        assert!(get_results[0].is_some());
533        assert!(get_results[1].is_none());
534
535        Ok(())
536    }
537
538    #[tokio::test]
539    async fn test_memory_storage_key_generation() {
540        // Test key generation logic
541        assert_eq!(MemoryStorage::make_key("nonce1", None), "nonce1:");
542        assert_eq!(MemoryStorage::make_key("nonce1", Some("ctx")), "nonce1:ctx");
543        assert_eq!(MemoryStorage::make_key("nonce1", Some("")), "nonce1:");
544    }
545
546    #[tokio::test]
547    async fn test_memory_storage_concurrent_access() -> Result<(), NonceError> {
548        let storage = std::sync::Arc::new(MemoryStorage::new());
549        let mut handles = vec![];
550
551        // Spawn multiple tasks that try to insert the same nonce
552        for i in 0..10 {
553            let storage_clone = std::sync::Arc::clone(&storage);
554            let handle = tokio::spawn(async move {
555                storage_clone
556                    .set(&format!("nonce-{i}"), None, Duration::from_secs(300))
557                    .await
558            });
559            handles.push(handle);
560        }
561
562        // All should succeed since they have different nonce values
563        for handle in handles {
564            assert!(handle.await.unwrap().is_ok());
565        }
566
567        // Verify all nonces were stored
568        let stats = storage.get_stats().await?;
569        assert_eq!(stats.total_records, 10);
570
571        Ok(())
572    }
573
574    #[tokio::test]
575    async fn test_memory_usage_calculation() -> Result<(), NonceError> {
576        let storage = MemoryStorage::new();
577
578        // Add nonces with various sizes
579        storage.set("short", None, Duration::from_secs(300)).await?;
580        storage
581            .set(
582                "very_long_nonce_name_for_testing",
583                Some("long_context_name"),
584                Duration::from_secs(300),
585            )
586            .await?;
587
588        let stats = storage.get_stats().await?;
589        assert_eq!(stats.total_records, 2);
590
591        // Memory usage should account for string lengths
592        assert!(stats.backend_info.contains("bytes"));
593        assert!(stats.backend_info.contains("capacity"));
594
595        Ok(())
596    }
597}