Skip to main content

aprender_present_lib/browser/
storage.rs

1//! Browser storage bindings for localStorage and sessionStorage.
2//!
3//! Provides a unified API for persisting data in the browser.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use presentar::browser::storage::{Storage, StorageType};
9//!
10//! let storage = Storage::new(StorageType::Local);
11//! storage.set("key", "value");
12//! let value = storage.get("key");
13//! ```
14
15use serde::{de::DeserializeOwned, Serialize};
16use std::collections::HashMap;
17
18/// Storage type (local or session).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum StorageType {
21    /// localStorage - persists across browser sessions
22    #[default]
23    Local,
24    /// sessionStorage - cleared when browser tab closes
25    Session,
26}
27
28/// Browser storage interface.
29///
30/// This provides a typed interface to browser storage APIs.
31/// In WASM, this uses actual localStorage/sessionStorage.
32/// In tests/non-WASM, this uses an in-memory fallback.
33#[derive(Debug)]
34pub struct Storage {
35    storage_type: StorageType,
36    /// In-memory fallback for non-WASM environments
37    #[cfg(not(target_arch = "wasm32"))]
38    memory: std::sync::Mutex<HashMap<String, String>>,
39}
40
41impl Default for Storage {
42    fn default() -> Self {
43        Self::new(StorageType::Local)
44    }
45}
46
47impl Storage {
48    /// Create a new storage instance.
49    #[must_use]
50    pub fn new(storage_type: StorageType) -> Self {
51        Self {
52            storage_type,
53            #[cfg(not(target_arch = "wasm32"))]
54            memory: std::sync::Mutex::new(HashMap::new()),
55        }
56    }
57
58    /// Create localStorage instance.
59    #[must_use]
60    pub fn local() -> Self {
61        Self::new(StorageType::Local)
62    }
63
64    /// Create sessionStorage instance.
65    #[must_use]
66    pub fn session() -> Self {
67        Self::new(StorageType::Session)
68    }
69
70    /// Get the storage type.
71    #[must_use]
72    pub const fn storage_type(&self) -> StorageType {
73        self.storage_type
74    }
75
76    /// Get a value from storage.
77    #[must_use]
78    pub fn get(&self, key: &str) -> Option<String> {
79        #[cfg(target_arch = "wasm32")]
80        {
81            self.get_wasm(key)
82        }
83        #[cfg(not(target_arch = "wasm32"))]
84        {
85            self.memory.lock().ok()?.get(key).cloned()
86        }
87    }
88
89    /// Set a value in storage.
90    pub fn set(&self, key: &str, value: &str) -> Result<(), StorageError> {
91        #[cfg(target_arch = "wasm32")]
92        {
93            self.set_wasm(key, value)
94        }
95        #[cfg(not(target_arch = "wasm32"))]
96        {
97            self.memory
98                .lock()
99                .map_err(|_| StorageError::AccessDenied)?
100                .insert(key.to_string(), value.to_string());
101            Ok(())
102        }
103    }
104
105    /// Remove a value from storage.
106    pub fn remove(&self, key: &str) -> Result<(), StorageError> {
107        #[cfg(target_arch = "wasm32")]
108        {
109            self.remove_wasm(key)
110        }
111        #[cfg(not(target_arch = "wasm32"))]
112        {
113            self.memory
114                .lock()
115                .map_err(|_| StorageError::AccessDenied)?
116                .remove(key);
117            Ok(())
118        }
119    }
120
121    /// Clear all values in storage.
122    pub fn clear(&self) -> Result<(), StorageError> {
123        #[cfg(target_arch = "wasm32")]
124        {
125            self.clear_wasm()
126        }
127        #[cfg(not(target_arch = "wasm32"))]
128        {
129            self.memory
130                .lock()
131                .map_err(|_| StorageError::AccessDenied)?
132                .clear();
133            Ok(())
134        }
135    }
136
137    /// Get the number of items in storage.
138    #[must_use]
139    pub fn len(&self) -> usize {
140        #[cfg(target_arch = "wasm32")]
141        {
142            self.len_wasm()
143        }
144        #[cfg(not(target_arch = "wasm32"))]
145        {
146            self.memory.lock().map(|m| m.len()).unwrap_or(0)
147        }
148    }
149
150    /// Check if storage is empty.
151    #[must_use]
152    pub fn is_empty(&self) -> bool {
153        self.len() == 0
154    }
155
156    /// Get a key at the given index.
157    #[must_use]
158    pub fn key(&self, index: usize) -> Option<String> {
159        #[cfg(target_arch = "wasm32")]
160        {
161            self.key_wasm(index)
162        }
163        #[cfg(not(target_arch = "wasm32"))]
164        {
165            self.memory.lock().ok()?.keys().nth(index).cloned()
166        }
167    }
168
169    /// Get all keys in storage.
170    #[must_use]
171    pub fn keys(&self) -> Vec<String> {
172        let mut keys = Vec::new();
173        for i in 0..self.len() {
174            if let Some(key) = self.key(i) {
175                keys.push(key);
176            }
177        }
178        keys
179    }
180
181    /// Get a value and deserialize it as JSON.
182    pub fn get_json<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, StorageError> {
183        match self.get(key) {
184            Some(json) => {
185                let value = serde_json::from_str(&json)
186                    .map_err(|e| StorageError::SerializationError(e.to_string()))?;
187                Ok(Some(value))
188            }
189            None => Ok(None),
190        }
191    }
192
193    /// Serialize a value as JSON and store it.
194    pub fn set_json<T: Serialize>(&self, key: &str, value: &T) -> Result<(), StorageError> {
195        let json = serde_json::to_string(value)
196            .map_err(|e| StorageError::SerializationError(e.to_string()))?;
197        self.set(key, &json)
198    }
199
200    // WASM implementations
201    #[cfg(target_arch = "wasm32")]
202    fn get_storage(&self) -> Option<web_sys::Storage> {
203        let window = web_sys::window()?;
204        match self.storage_type {
205            StorageType::Local => window.local_storage().ok()?,
206            StorageType::Session => window.session_storage().ok()?,
207        }
208    }
209
210    #[cfg(target_arch = "wasm32")]
211    fn get_wasm(&self, key: &str) -> Option<String> {
212        self.get_storage()?.get_item(key).ok()?
213    }
214
215    #[cfg(target_arch = "wasm32")]
216    fn set_wasm(&self, key: &str, value: &str) -> Result<(), StorageError> {
217        self.get_storage()
218            .ok_or(StorageError::NotAvailable)?
219            .set_item(key, value)
220            .map_err(|_| StorageError::QuotaExceeded)
221    }
222
223    #[cfg(target_arch = "wasm32")]
224    fn remove_wasm(&self, key: &str) -> Result<(), StorageError> {
225        self.get_storage()
226            .ok_or(StorageError::NotAvailable)?
227            .remove_item(key)
228            .map_err(|_| StorageError::AccessDenied)
229    }
230
231    #[cfg(target_arch = "wasm32")]
232    fn clear_wasm(&self) -> Result<(), StorageError> {
233        self.get_storage()
234            .ok_or(StorageError::NotAvailable)?
235            .clear()
236            .map_err(|_| StorageError::AccessDenied)
237    }
238
239    #[cfg(target_arch = "wasm32")]
240    fn len_wasm(&self) -> usize {
241        self.get_storage()
242            .and_then(|s| s.length().ok())
243            .unwrap_or(0) as usize
244    }
245
246    #[cfg(target_arch = "wasm32")]
247    fn key_wasm(&self, index: usize) -> Option<String> {
248        self.get_storage()?.key(index as u32).ok()?
249    }
250}
251
252/// Storage error types.
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub enum StorageError {
255    /// Storage is not available (e.g., in incognito mode)
256    NotAvailable,
257    /// Storage quota exceeded
258    QuotaExceeded,
259    /// Access denied
260    AccessDenied,
261    /// Serialization/deserialization error
262    SerializationError(String),
263}
264
265impl std::fmt::Display for StorageError {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        match self {
268            Self::NotAvailable => write!(f, "storage not available"),
269            Self::QuotaExceeded => write!(f, "storage quota exceeded"),
270            Self::AccessDenied => write!(f, "storage access denied"),
271            Self::SerializationError(msg) => write!(f, "serialization error: {msg}"),
272        }
273    }
274}
275
276impl std::error::Error for StorageError {}
277
278/// Scoped storage with automatic key prefixing.
279///
280/// Useful for isolating storage between different parts of an application.
281#[derive(Debug)]
282pub struct ScopedStorage {
283    inner: Storage,
284    prefix: String,
285}
286
287impl ScopedStorage {
288    /// Create a new scoped storage with the given prefix.
289    #[must_use]
290    pub fn new(storage: Storage, prefix: impl Into<String>) -> Self {
291        Self {
292            inner: storage,
293            prefix: prefix.into(),
294        }
295    }
296
297    /// Create a localStorage instance with the given prefix.
298    #[must_use]
299    pub fn local(prefix: impl Into<String>) -> Self {
300        Self::new(Storage::local(), prefix)
301    }
302
303    /// Create a sessionStorage instance with the given prefix.
304    #[must_use]
305    pub fn session(prefix: impl Into<String>) -> Self {
306        Self::new(Storage::session(), prefix)
307    }
308
309    /// Get the prefix.
310    #[must_use]
311    pub fn prefix(&self) -> &str {
312        &self.prefix
313    }
314
315    fn prefixed_key(&self, key: &str) -> String {
316        format!("{}:{}", self.prefix, key)
317    }
318
319    /// Get a value from storage.
320    #[must_use]
321    pub fn get(&self, key: &str) -> Option<String> {
322        self.inner.get(&self.prefixed_key(key))
323    }
324
325    /// Set a value in storage.
326    pub fn set(&self, key: &str, value: &str) -> Result<(), StorageError> {
327        self.inner.set(&self.prefixed_key(key), value)
328    }
329
330    /// Remove a value from storage.
331    pub fn remove(&self, key: &str) -> Result<(), StorageError> {
332        self.inner.remove(&self.prefixed_key(key))
333    }
334
335    /// Get a value and deserialize it as JSON.
336    pub fn get_json<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, StorageError> {
337        self.inner.get_json(&self.prefixed_key(key))
338    }
339
340    /// Serialize a value as JSON and store it.
341    pub fn set_json<T: Serialize>(&self, key: &str, value: &T) -> Result<(), StorageError> {
342        self.inner.set_json(&self.prefixed_key(key), value)
343    }
344
345    /// Clear all values with this prefix.
346    pub fn clear(&self) -> Result<(), StorageError> {
347        let keys: Vec<_> = self
348            .inner
349            .keys()
350            .into_iter()
351            .filter(|k| k.starts_with(&format!("{}:", self.prefix)))
352            .collect();
353
354        for key in keys {
355            // Remove the raw key (already prefixed)
356            self.inner.remove(&key)?;
357        }
358        Ok(())
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_storage_type_default() {
368        assert_eq!(StorageType::default(), StorageType::Local);
369    }
370
371    #[test]
372    fn test_storage_new() {
373        let storage = Storage::new(StorageType::Local);
374        assert_eq!(storage.storage_type(), StorageType::Local);
375
376        let storage = Storage::new(StorageType::Session);
377        assert_eq!(storage.storage_type(), StorageType::Session);
378    }
379
380    #[test]
381    fn test_storage_local() {
382        let storage = Storage::local();
383        assert_eq!(storage.storage_type(), StorageType::Local);
384    }
385
386    #[test]
387    fn test_storage_session() {
388        let storage = Storage::session();
389        assert_eq!(storage.storage_type(), StorageType::Session);
390    }
391
392    #[test]
393    fn test_storage_set_get() {
394        let storage = Storage::local();
395        storage.set("test_key", "test_value").unwrap();
396        assert_eq!(storage.get("test_key"), Some("test_value".to_string()));
397    }
398
399    #[test]
400    fn test_storage_get_nonexistent() {
401        let storage = Storage::local();
402        assert_eq!(storage.get("nonexistent"), None);
403    }
404
405    #[test]
406    fn test_storage_remove() {
407        let storage = Storage::local();
408        storage.set("to_remove", "value").unwrap();
409        assert!(storage.get("to_remove").is_some());
410        storage.remove("to_remove").unwrap();
411        assert!(storage.get("to_remove").is_none());
412    }
413
414    #[test]
415    fn test_storage_clear() {
416        let storage = Storage::local();
417        storage.set("key1", "value1").unwrap();
418        storage.set("key2", "value2").unwrap();
419        assert!(!storage.is_empty());
420        storage.clear().unwrap();
421        assert!(storage.is_empty());
422    }
423
424    #[test]
425    fn test_storage_len() {
426        let storage = Storage::local();
427        assert_eq!(storage.len(), 0);
428        storage.set("key1", "value1").unwrap();
429        assert_eq!(storage.len(), 1);
430        storage.set("key2", "value2").unwrap();
431        assert_eq!(storage.len(), 2);
432    }
433
434    #[test]
435    fn test_storage_is_empty() {
436        let storage = Storage::local();
437        assert!(storage.is_empty());
438        storage.set("key", "value").unwrap();
439        assert!(!storage.is_empty());
440    }
441
442    #[test]
443    fn test_storage_keys() {
444        let storage = Storage::local();
445        storage.set("a", "1").unwrap();
446        storage.set("b", "2").unwrap();
447        let keys = storage.keys();
448        assert_eq!(keys.len(), 2);
449        assert!(keys.contains(&"a".to_string()));
450        assert!(keys.contains(&"b".to_string()));
451    }
452
453    #[test]
454    fn test_storage_json() {
455        #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
456        struct TestData {
457            name: String,
458            value: i32,
459        }
460
461        let storage = Storage::local();
462        let data = TestData {
463            name: "test".to_string(),
464            value: 42,
465        };
466
467        storage.set_json("json_key", &data).unwrap();
468        let loaded: Option<TestData> = storage.get_json("json_key").unwrap();
469        assert_eq!(loaded, Some(data));
470    }
471
472    #[test]
473    fn test_storage_json_nonexistent() {
474        let storage = Storage::local();
475        let result: Result<Option<String>, _> = storage.get_json("nonexistent");
476        assert_eq!(result.unwrap(), None);
477    }
478
479    #[test]
480    fn test_scoped_storage_new() {
481        let scoped = ScopedStorage::local("myapp");
482        assert_eq!(scoped.prefix(), "myapp");
483    }
484
485    #[test]
486    fn test_scoped_storage_set_get() {
487        let scoped = ScopedStorage::local("test");
488        scoped.set("key", "value").unwrap();
489        assert_eq!(scoped.get("key"), Some("value".to_string()));
490
491        // Note: In non-WASM mode, each Storage instance has its own memory,
492        // so we can only verify the scoped behavior, not cross-instance sharing.
493        // WASM mode uses actual browser localStorage which is shared.
494    }
495
496    #[test]
497    fn test_scoped_storage_isolation() {
498        let scope1 = ScopedStorage::local("scope1");
499        let scope2 = ScopedStorage::local("scope2");
500
501        scope1.set("key", "value1").unwrap();
502        scope2.set("key", "value2").unwrap();
503
504        assert_eq!(scope1.get("key"), Some("value1".to_string()));
505        assert_eq!(scope2.get("key"), Some("value2".to_string()));
506    }
507
508    #[test]
509    fn test_scoped_storage_json() {
510        let scoped = ScopedStorage::local("json_test");
511        scoped.set_json("data", &vec![1, 2, 3]).unwrap();
512        let loaded: Option<Vec<i32>> = scoped.get_json("data").unwrap();
513        assert_eq!(loaded, Some(vec![1, 2, 3]));
514    }
515
516    #[test]
517    fn test_storage_error_display() {
518        assert_eq!(
519            StorageError::NotAvailable.to_string(),
520            "storage not available"
521        );
522        assert_eq!(
523            StorageError::QuotaExceeded.to_string(),
524            "storage quota exceeded"
525        );
526        assert_eq!(
527            StorageError::AccessDenied.to_string(),
528            "storage access denied"
529        );
530        assert_eq!(
531            StorageError::SerializationError("test".to_string()).to_string(),
532            "serialization error: test"
533        );
534    }
535
536    #[test]
537    fn test_storage_default() {
538        let storage = Storage::default();
539        assert_eq!(storage.storage_type(), StorageType::Local);
540    }
541}