persistent_map/lib.rs
1//! # `PersistentMap`
2//!
3//! `persistent-map` provides an in-memory key-value store with async, pluggable persistence.
4//!
5//! It combines the speed of [`DashMap`](https://docs.rs/dashmap) for in-memory operations
6//! with the durability of various storage backends for persistence.
7//!
8//! ## Features
9//!
10//! - **Fast in-memory access**: Uses `DashMap` for concurrent read/write operations
11//! - **Async API**: Non-blocking I/O operations for persistence
12//! - **Multiple backends**: `SQLite`, `CSV`, in-memory, and extensible for more
13//! - **Type-safe**: Works with any types that implement the required traits
14//!
15//! ## Example
16//!
17//! ```rust,no_run
18//! use persistent_map::{PersistentMap, Result};
19//! # #[cfg(feature = "sqlite")]
20//! use persistent_map::sqlite::SqliteBackend;
21//!
22//! # #[cfg(feature = "sqlite")]
23//! # async fn example() -> Result<()> {
24//! # // Create a SQLite backend
25//! # let backend = SqliteBackend::new("my_database.db").await?;
26//! #
27//! # // Initialize the persistent map
28//! # let map = PersistentMap::new(backend).await?;
29//! #
30//! # // Insert a key-value pair (persists automatically)
31//! # map.insert("hello".to_string(), "world".to_string()).await?;
32//! #
33//! # // Retrieve a value (from memory)
34//! # assert_eq!(map.get(&"hello".to_string()), Some("world".to_string()));
35//! # Ok(())
36//! # }
37//! #
38//! # #[cfg(not(feature = "sqlite"))]
39//! # fn example() {}
40//! ```
41
42use dashmap::DashMap;
43use serde::{de::DeserializeOwned, Serialize};
44use std::{collections::HashMap, hash::Hash};
45use thiserror::Error;
46/// A trait for implementing storage backends for `PersistentMap`.
47///
48/// This trait defines the interface that all storage backends must implement.
49/// It provides methods for loading, saving, and deleting key-value pairs.
50///
51/// # Type Parameters
52///
53/// * `K`: The key type, which must be hashable, serializable, and cloneable
54/// * `V`: The value type, which must be serializable and cloneable
55///
56/// # Examples
57///
58/// Implementing a custom backend:
59///
60/// The `StorageBackend` trait defines the interface for persistent storage backends.
61/// By implementing this trait, you can create custom storage solutions for the `PersistentMap`.
62///
63/// # Implementing a Custom Backend
64///
65/// To implement a custom backend, you need to:
66///
67/// 1. Create a struct to hold your backend-specific data
68/// 2. Implement the required methods: `load_all`, `save`, and `delete`
69/// 3. Optionally override the `flush` method if your backend buffers writes
70///
71/// # Example Implementation
72///
73/// Here's an example of a custom backend that stores data in a JSON file:
74///
75/// ```rust
76/// use persistent_map::{StorageBackend, PersistentError, Result};
77/// use std::collections::HashMap;
78/// use std::path::PathBuf;
79/// use std::fs;
80/// use serde::{Serialize, de::DeserializeOwned};
81/// use std::hash::Hash;
82///
83/// struct JsonFileBackend {
84/// path: PathBuf,
85/// }
86///
87/// impl JsonFileBackend {
88/// pub fn new(path: impl Into<PathBuf>) -> Self {
89/// Self { path: path.into() }
90/// }
91///
92/// // Helper method to ensure the file exists
93/// fn ensure_file_exists(&self) -> std::io::Result<()> {
94/// if !self.path.exists() {
95/// // Create parent directories if they don't exist
96/// if let Some(parent) = self.path.parent() {
97/// if !parent.exists() {
98/// fs::create_dir_all(parent)?;
99/// }
100/// }
101///
102/// // Create the file with an empty JSON object
103/// fs::write(&self.path, "{}")?;
104/// }
105/// Ok(())
106/// }
107/// }
108///
109/// #[async_trait::async_trait]
110/// impl<K, V> StorageBackend<K, V> for JsonFileBackend
111/// where
112/// K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + ToString + 'static,
113/// V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
114/// {
115/// async fn load_all(&self) -> Result<HashMap<K, V>, PersistentError> {
116/// // Ensure the file exists
117/// self.ensure_file_exists()?;
118///
119/// // If the file is empty or contains just "{}", return an empty HashMap
120/// let content = fs::read_to_string(&self.path)?;
121/// if content.trim() == "{}" {
122/// return Ok(HashMap::new());
123/// }
124///
125/// // Parse the JSON file
126/// let map = serde_json::from_str(&content)
127/// .map_err(|e| PersistentError::Serde(e))?;
128///
129/// Ok(map)
130/// }
131///
132/// async fn save(&self, key: K, value: V) -> Result<(), PersistentError> {
133/// // Ensure the file exists
134/// self.ensure_file_exists()?;
135///
136/// // Load existing data
137/// let mut map: HashMap<K, V> = self.load_all().await?;
138///
139/// // Update the map
140/// map.insert(key, value);
141///
142/// // Write back to the file
143/// let content = serde_json::to_string_pretty(&map)
144/// .map_err(|e| PersistentError::Serde(e))?;
145///
146/// fs::write(&self.path, content)?;
147///
148/// Ok(())
149/// }
150///
151/// async fn delete(&self, key: &K) -> Result<(), PersistentError> {
152/// // Ensure the file exists
153/// self.ensure_file_exists()?;
154///
155/// // Load existing data
156/// let mut map: HashMap<K, V> = self.load_all().await?;
157///
158/// // Remove the key
159/// map.remove(key);
160///
161/// // Write back to the file
162/// let content = serde_json::to_string_pretty(&map)
163/// .map_err(|e| PersistentError::Serde(e))?;
164///
165/// fs::write(&self.path, content)?;
166///
167/// Ok(())
168/// }
169///
170/// async fn flush(&self) -> Result<(), PersistentError> {
171/// // No buffering in this implementation, so nothing to flush
172/// Ok(())
173/// }
174/// }
175/// ```
176///
177/// # Best Practices for Custom Backends
178///
179/// 1. **Error Handling**: Convert backend-specific errors to `PersistentError`
180/// 2. **Concurrency**: Ensure your backend is safe for concurrent access
181/// 3. **Performance**: Consider caching or batching operations for better performance
182/// 4. **Resilience**: Handle edge cases like missing files or corrupted data gracefully
183/// 5. **Testing**: Create tests that verify persistence across application restarts
184#[async_trait::async_trait]
185pub trait StorageBackend<K, V>
186where
187 K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
188 V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
189{
190 /// Load all key-value pairs from the storage backend.
191 ///
192 /// This method is called when initializing a `PersistentMap` to populate
193 /// the in-memory map with existing data.
194 ///
195 /// # Errors
196 ///
197 /// Returns a `PersistentError` if loading fails for any reason, such as:
198 /// - The storage location doesn't exist
199 /// - The data is corrupted or in an invalid format
200 /// - There are permission issues
201 ///
202 /// # Implementation Notes
203 ///
204 /// - This method should be idempotent and safe to call multiple times
205 /// - If the storage is empty or doesn't exist yet, return an empty `HashMap`
206 /// - Consider adding error recovery mechanisms for corrupted data
207 async fn load_all(&self) -> Result<HashMap<K, V>, PersistentError>;
208
209 /// Save a key-value pair to the storage backend.
210 ///
211 /// This method is called whenever a key-value pair is inserted into the map.
212 ///
213 /// # Errors
214 ///
215 /// Returns a `PersistentError` if saving fails for any reason, such as:
216 /// - The storage location is not writable
217 /// - There are permission issues
218 /// - The backend has reached capacity
219 ///
220 /// # Implementation Notes
221 ///
222 /// - This method should be atomic if possible
223 /// - Consider batching or caching writes for better performance
224 /// - If your backend requires serialization, handle serialization errors appropriately
225 async fn save(&self, key: K, value: V) -> Result<(), PersistentError>;
226
227 /// Delete a key-value pair from the storage backend.
228 ///
229 /// This method is called whenever a key-value pair is removed from the map.
230 ///
231 /// # Errors
232 ///
233 /// Returns a `PersistentError` if deletion fails for any reason, such as:
234 /// - The storage location is not writable
235 /// - There are permission issues
236 ///
237 /// # Implementation Notes
238 ///
239 /// - This method should be idempotent - deleting a non-existent key should not be an error
240 /// - Consider optimizing for the case where the key doesn't exist
241 async fn delete(&self, key: &K) -> Result<(), PersistentError>;
242
243 /// Flush any buffered writes to the storage backend.
244 ///
245 /// This method is called when the user explicitly requests to ensure all data is persisted.
246 ///
247 /// # Errors
248 ///
249 /// Returns a `PersistentError` if flushing fails for any reason, such as:
250 /// - The storage location is not writable
251 /// - There are permission issues
252 ///
253 /// # Implementation Notes
254 ///
255 /// - This method is optional and has a default implementation that does nothing
256 /// - Backends that buffer writes should override this method to ensure data is persisted
257 /// - This method should be idempotent and safe to call multiple times
258 async fn flush(&self) -> Result<(), PersistentError> {
259 Ok(())
260 }
261
262 /// Check if a key exists in the storage backend.
263 ///
264 /// This is an optional method with a default implementation that loads all data
265 /// and checks if the key exists. Backend implementations can override this
266 /// for better performance if they can check for existence without loading all data.
267 ///
268 /// # Errors
269 ///
270 /// Returns a `PersistentError` if the check fails for any reason.
271 ///
272 /// # Implementation Notes
273 ///
274 /// - The default implementation is inefficient for large datasets
275 /// - Override this method if your backend can check for existence more efficiently
276 async fn contains_key(&self, key: &K) -> Result<bool, PersistentError> {
277 let all = self.load_all().await?;
278 Ok(all.contains_key(key))
279 }
280
281 /// Get the number of key-value pairs in the storage backend.
282 ///
283 /// This is an optional method with a default implementation that loads all data
284 /// and counts the entries. Backend implementations can override this
285 /// for better performance if they can count entries without loading all data.
286 ///
287 /// # Errors
288 ///
289 /// Returns a `PersistentError` if the count operation fails for any reason.
290 ///
291 /// # Implementation Notes
292 ///
293 /// - The default implementation is inefficient for large datasets
294 /// - Override this method if your backend can count entries more efficiently
295 async fn len(&self) -> Result<usize, PersistentError> {
296 let all = self.load_all().await?;
297 Ok(all.len())
298 }
299
300 /// Check if the storage backend is empty.
301 ///
302 /// This is an optional method with a default implementation that uses `len()`.
303 /// Backend implementations can override this for better performance.
304 ///
305 /// # Errors
306 ///
307 /// Returns a `PersistentError` if the check fails for any reason.
308 ///
309 /// # Implementation Notes
310 ///
311 /// - The default implementation uses `len()`, which may be inefficient
312 /// - Override this method if your backend can check emptiness more efficiently
313 async fn is_empty(&self) -> Result<bool, PersistentError> {
314 Ok(self.len().await? == 0)
315 }
316}
317
318/// Errors that can occur when using `PersistentMap`.
319///
320/// This enum represents all the possible errors that can occur when using
321/// the various storage backends.
322#[derive(Error, Debug)]
323pub enum PersistentError {
324 /// An error occurred in the `SQLite` backend.
325 #[cfg(feature = "sqlite")]
326 #[error("sqlite error: {0}")]
327 Sqlite(#[from] tokio_rusqlite::Error),
328
329 /// An error occurred in the CSV backend.
330 #[cfg(feature = "csv_backend")]
331 #[error("csv error: {0}")]
332 Csv(String),
333
334 /// An I/O error occurred.
335 #[error("io error: {0}")]
336 Io(#[from] std::io::Error),
337
338 /// A serialization or deserialization error occurred.
339 #[error("serde error: {0}")]
340 Serde(#[from] serde_json::Error),
341
342 /// An error occurred in the Sled backend.
343 #[cfg(feature = "sled_backend")]
344 #[error("sled error: {0}")]
345 Sled(#[from] sled::Error),
346}
347
348/// Shorthand Result with error defaulting to `PersistentError`.
349pub type Result<T, E = PersistentError> = std::result::Result<T, E>;
350
351// Re-export backends
352#[cfg(feature = "csv_backend")]
353pub use crate::backends::csv;
354
355#[cfg(feature = "in_memory")]
356pub use crate::backends::in_memory;
357
358#[cfg(feature = "sqlite")]
359pub use crate::backends::sqlite;
360
361mod backends;
362
363/// A persistent key-value map with in-memory caching.
364///
365/// `PersistentMap` combines a fast in-memory `DashMap` with a persistent
366/// storage backend. It provides a simple API for storing and retrieving
367/// key-value pairs, with automatic persistence.
368///
369/// # Type Parameters
370///
371/// * `K`: The key type, which must be hashable, serializable, and cloneable
372/// * `V`: The value type, which must be serializable and cloneable
373/// * `B`: The storage backend type, which must implement `StorageBackend<K, V>`
374///
375/// # Examples
376///
377/// ```rust,no_run
378/// use persistent_map::{PersistentMap, Result};
379/// # #[cfg(feature = "sqlite")]
380/// use persistent_map::sqlite::SqliteBackend;
381///
382/// # #[cfg(feature = "sqlite")]
383/// # async fn example() -> Result<()> {
384/// # // Create a SQLite backend
385/// # let backend = SqliteBackend::new("my_database.db").await?;
386/// #
387/// # // Initialize the persistent map
388/// # let map = PersistentMap::new(backend).await?;
389/// #
390/// # // Insert a key-value pair (persists automatically)
391/// # map.insert("hello".to_string(), "world".to_string()).await?;
392/// #
393/// # // Retrieve a value (from memory)
394/// # assert_eq!(map.get(&"hello".to_string()), Some("world".to_string()));
395/// #
396/// # // Remove a key-value pair (removes from persistence too)
397/// # let old_value = map.remove(&"hello".to_string()).await?;
398/// # assert_eq!(old_value, Some("world".to_string()));
399/// # Ok(())
400/// # }
401/// #
402/// # #[cfg(not(feature = "sqlite"))]
403/// # fn example() {}
404/// ```
405pub struct PersistentMap<K, V, B>
406where
407 K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
408 V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
409 B: StorageBackend<K, V> + Send + Sync + 'static,
410{
411 /// The in-memory map for fast access
412 map: DashMap<K, V>,
413
414 /// The storage backend for persistence
415 backend: B,
416}
417
418impl<K, V, B> PersistentMap<K, V, B>
419where
420 K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
421 V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
422 B: StorageBackend<K, V> + Send + Sync + 'static,
423{
424 /// Creates a new `PersistentMap` with the given storage backend.
425 ///
426 /// This method initializes the map and loads all existing key-value pairs
427 /// from the storage backend into memory.
428 ///
429 /// # Examples
430 ///
431 /// ```rust,no_run
432 /// use persistent_map::{PersistentMap, Result};
433 /// # #[cfg(feature = "sqlite")]
434 /// use persistent_map::sqlite::SqliteBackend;
435 ///
436 /// # #[cfg(feature = "sqlite")]
437 /// # async fn example() -> Result<()> {
438 /// # let backend = SqliteBackend::new("my_database.db").await?;
439 /// # let map: PersistentMap<String, String, _> = PersistentMap::new(backend).await?;
440 /// # Ok(())
441 /// # }
442 /// #
443 /// # #[cfg(not(feature = "sqlite"))]
444 /// # fn example() {}
445 /// ```
446 /// # Errors
447 ///
448 /// Returns an error if loading from the backend fails.
449 #[inline]
450 pub async fn new(backend: B) -> Result<Self> {
451 let map = DashMap::new();
452 let pm = Self { map, backend };
453 pm.load().await?;
454 Ok(pm)
455 }
456
457 /// Loads all key-value pairs from the storage backend into memory.
458 ///
459 /// This method is called automatically when creating a new `PersistentMap`,
460 /// but can also be called manually to refresh the in-memory cache.
461 ///
462 /// # Examples
463 ///
464 /// ```rust,no_run
465 /// # use persistent_map::{PersistentMap, StorageBackend, Result};
466 /// #
467 /// # async fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) -> Result<()> {
468 /// // Reload all data from the storage backend
469 /// map.load().await?;
470 /// # Ok(())
471 /// # }
472 /// ```
473 /// # Errors
474 ///
475 /// Returns an error if loading from the backend fails.
476 #[inline]
477 pub async fn load(&self) -> Result<(), PersistentError> {
478 let all = self.backend.load_all().await?;
479 for (k, v) in all {
480 self.map.insert(k, v);
481 }
482 Ok(())
483 }
484
485 /// Inserts a key-value pair into the map and persists it to the storage backend.
486 ///
487 /// If the map already contains the key, the value is updated and the old value
488 /// is returned. Otherwise, `None` is returned.
489 ///
490 /// # Examples
491 ///
492 /// ```rust,no_run
493 /// # use persistent_map::{PersistentMap, StorageBackend, Result};
494 /// #
495 /// # async fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) -> Result<()> {
496 /// // Insert a new key-value pair
497 /// let old = map.insert("key".to_string(), "value".to_string()).await?;
498 /// assert_eq!(old, None);
499 ///
500 /// // Update an existing key
501 /// let old = map.insert("key".to_string(), "new value".to_string()).await?;
502 /// assert_eq!(old, Some("value".to_string()));
503 /// # Ok(())
504 /// # }
505 /// ```
506 /// # Errors
507 ///
508 /// Returns an error if saving to the backend fails.
509 #[inline]
510 pub async fn insert(&self, key: K, value: V) -> Result<Option<V>> {
511 let old = self.map.insert(key.clone(), value.clone());
512 self.backend.save(key, value).await?;
513 Ok(old)
514 }
515
516 /// Retrieves a value from the map by its key.
517 ///
518 /// This method only accesses the in-memory map and does not interact with
519 /// the storage backend, making it very fast.
520 ///
521 /// # Examples
522 ///
523 /// ```rust,no_run
524 /// # use persistent_map::{PersistentMap, StorageBackend};
525 /// #
526 /// # fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) {
527 /// // Get a value
528 /// if let Some(value) = map.get(&"key".to_string()) {
529 /// println!("Value: {}", value);
530 /// }
531 /// # }
532 /// ```
533 #[inline]
534 pub fn get(&self, key: &K) -> Option<V> {
535 self.map.get(key).map(|r| r.value().clone())
536 }
537
538 /// Removes a key-value pair from the map and the storage backend.
539 ///
540 /// If the map contains the key, the key-value pair is removed and the old value
541 /// is returned. Otherwise, `None` is returned.
542 ///
543 /// # Examples
544 ///
545 /// ```rust,no_run
546 /// # use persistent_map::{PersistentMap, StorageBackend, Result};
547 /// #
548 /// # async fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) -> Result<()> {
549 /// // Remove a key-value pair
550 /// let old = map.remove(&"key".to_string()).await?;
551 /// if let Some(value) = old {
552 /// println!("Removed value: {}", value);
553 /// }
554 /// # Ok(())
555 /// # }
556 /// ```
557 /// # Errors
558 ///
559 /// Returns an error if deleting from the backend fails.
560 #[inline]
561 pub async fn remove(&self, key: &K) -> Result<Option<V>> {
562 let old = self.map.remove(key).map(|(_, v)| v);
563 if old.is_some() {
564 match self.backend.delete(key).await {
565 Ok(()) => {}
566 Err(e) => return Err(e),
567 }
568 }
569 Ok(old)
570 }
571
572 /// Returns the number of key-value pairs in the map.
573 ///
574 /// # Examples
575 ///
576 /// ```rust,no_run
577 /// # use persistent_map::{PersistentMap, StorageBackend};
578 /// #
579 /// # fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) {
580 /// let count = map.len();
581 /// println!("Map contains {} entries", count);
582 /// # }
583 /// ```
584 #[inline]
585 pub fn len(&self) -> usize {
586 self.map.len()
587 }
588
589 /// Returns `true` if the map contains no key-value pairs.
590 ///
591 /// # Examples
592 ///
593 /// ```rust,no_run
594 /// # use persistent_map::{PersistentMap, StorageBackend};
595 /// #
596 /// # fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) {
597 /// if map.is_empty() {
598 /// println!("Map is empty");
599 /// }
600 /// # }
601 /// ```
602 #[inline]
603 pub fn is_empty(&self) -> bool {
604 self.map.is_empty()
605 }
606
607 /// Returns `true` if the map contains the specified key.
608 ///
609 /// # Examples
610 ///
611 /// ```rust,no_run
612 /// # use persistent_map::{PersistentMap, StorageBackend};
613 /// #
614 /// # fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) {
615 /// if map.contains_key(&"key".to_string()) {
616 /// println!("Map contains the key");
617 /// }
618 /// # }
619 /// ```
620 #[inline]
621 pub fn contains_key(&self, key: &K) -> bool {
622 self.map.contains_key(key)
623 }
624
625 /// Clears the in-memory map without affecting the storage backend.
626 ///
627 /// This method only clears the in-memory cache and does not delete any data
628 /// from the storage backend. To completely clear the storage, you should
629 /// delete the underlying storage file or database.
630 ///
631 /// # Examples
632 ///
633 /// ```rust,no_run
634 /// # use persistent_map::{PersistentMap, StorageBackend};
635 /// #
636 /// # fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) {
637 /// // Clear the in-memory cache
638 /// map.clear();
639 /// assert_eq!(map.len(), 0);
640 /// # }
641 /// ```
642 #[inline]
643 pub fn clear(&self) {
644 self.map.clear();
645 }
646
647 /// Flushes any buffered writes to the storage backend.
648 ///
649 /// This method is useful for backends that buffer writes for performance.
650 /// It ensures that all data is persisted to the storage medium.
651 ///
652 /// # Examples
653 ///
654 /// ```rust,no_run
655 /// # use persistent_map::{PersistentMap, StorageBackend, Result};
656 /// #
657 /// # async fn example(map: PersistentMap<String, String, impl StorageBackend<String, String> + Send + Sync>) -> Result<()> {
658 /// // Ensure all data is persisted
659 /// map.flush().await?;
660 /// # Ok(())
661 /// # }
662 /// ```
663 /// # Errors
664 ///
665 /// Returns an error if flushing the backend fails.
666 #[inline]
667 pub async fn flush(&self) -> Result<(), PersistentError> {
668 self.backend.flush().await
669 }
670
671 /// Returns a reference to the storage backend.
672 ///
673 /// This method is useful for accessing backend-specific functionality.
674 ///
675 /// # Examples
676 ///
677 /// ```rust,no_run
678 /// # use persistent_map::{PersistentMap, StorageBackend};
679 /// #
680 /// # fn example<B>(map: PersistentMap<String, String, B>)
681 /// # where B: StorageBackend<String, String> + Send + Sync
682 /// # {
683 /// let backend = map.backend();
684 /// // Use backend-specific functionality
685 /// # }
686 /// ```
687 #[inline]
688 pub const fn backend(&self) -> &B {
689 &self.backend
690 }
691}