Trait StorageBackend

Source
pub trait StorageBackend<K, V>
where K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static, V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
{ // Required methods fn load_all<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<HashMap<K, V>, PersistentError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn save<'life0, 'async_trait>( &'life0 self, key: K, value: V, ) -> Pin<Box<dyn Future<Output = Result<(), PersistentError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn delete<'life0, 'life1, 'async_trait>( &'life0 self, key: &'life1 K, ) -> Pin<Box<dyn Future<Output = Result<(), PersistentError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; // Provided methods fn flush<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<(), PersistentError>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait { ... } fn contains_key<'life0, 'life1, 'async_trait>( &'life0 self, key: &'life1 K, ) -> Pin<Box<dyn Future<Output = Result<bool, PersistentError>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait { ... } fn len<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<usize, PersistentError>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait { ... } fn is_empty<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<bool, PersistentError>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait { ... } }
Expand description

A trait for implementing storage backends for PersistentMap.

This trait defines the interface that all storage backends must implement. It provides methods for loading, saving, and deleting key-value pairs.

§Type Parameters

  • K: The key type, which must be hashable, serializable, and cloneable
  • V: The value type, which must be serializable and cloneable

§Examples

Implementing a custom backend:

The StorageBackend trait defines the interface for persistent storage backends. By implementing this trait, you can create custom storage solutions for the PersistentMap.

§Implementing a Custom Backend

To implement a custom backend, you need to:

  1. Create a struct to hold your backend-specific data
  2. Implement the required methods: load_all, save, and delete
  3. Optionally override the flush method if your backend buffers writes

§Example Implementation

Here’s an example of a custom backend that stores data in a JSON file:

use persistent_map::{StorageBackend, PersistentError, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use std::fs;
use serde::{Serialize, de::DeserializeOwned};
use std::hash::Hash;

struct JsonFileBackend {
    path: PathBuf,
}

impl JsonFileBackend {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }

    // Helper method to ensure the file exists
    fn ensure_file_exists(&self) -> std::io::Result<()> {
        if !self.path.exists() {
            // Create parent directories if they don't exist
            if let Some(parent) = self.path.parent() {
                if !parent.exists() {
                    fs::create_dir_all(parent)?;
                }
            }

            // Create the file with an empty JSON object
            fs::write(&self.path, "{}")?;
        }
        Ok(())
    }
}

#[async_trait::async_trait]
impl<K, V> StorageBackend<K, V> for JsonFileBackend
where
    K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + ToString + 'static,
    V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
{
    async fn load_all(&self) -> Result<HashMap<K, V>, PersistentError> {
        // Ensure the file exists
        self.ensure_file_exists()?;

        // If the file is empty or contains just "{}", return an empty HashMap
        let content = fs::read_to_string(&self.path)?;
        if content.trim() == "{}" {
            return Ok(HashMap::new());
        }

        // Parse the JSON file
        let map = serde_json::from_str(&content)
            .map_err(|e| PersistentError::Serde(e))?;

        Ok(map)
    }

    async fn save(&self, key: K, value: V) -> Result<(), PersistentError> {
        // Ensure the file exists
        self.ensure_file_exists()?;

        // Load existing data
        let mut map: HashMap<K, V> = self.load_all().await?;

        // Update the map
        map.insert(key, value);

        // Write back to the file
        let content = serde_json::to_string_pretty(&map)
            .map_err(|e| PersistentError::Serde(e))?;

        fs::write(&self.path, content)?;

        Ok(())
    }

    async fn delete(&self, key: &K) -> Result<(), PersistentError> {
        // Ensure the file exists
        self.ensure_file_exists()?;

        // Load existing data
        let mut map: HashMap<K, V> = self.load_all().await?;

        // Remove the key
        map.remove(key);

        // Write back to the file
        let content = serde_json::to_string_pretty(&map)
            .map_err(|e| PersistentError::Serde(e))?;

        fs::write(&self.path, content)?;

        Ok(())
    }

    async fn flush(&self) -> Result<(), PersistentError> {
        // No buffering in this implementation, so nothing to flush
        Ok(())
    }
}

§Best Practices for Custom Backends

  1. Error Handling: Convert backend-specific errors to PersistentError
  2. Concurrency: Ensure your backend is safe for concurrent access
  3. Performance: Consider caching or batching operations for better performance
  4. Resilience: Handle edge cases like missing files or corrupted data gracefully
  5. Testing: Create tests that verify persistence across application restarts

Required Methods§

Source

fn load_all<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<HashMap<K, V>, PersistentError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Load all key-value pairs from the storage backend.

This method is called when initializing a PersistentMap to populate the in-memory map with existing data.

§Errors

Returns a PersistentError if loading fails for any reason, such as:

  • The storage location doesn’t exist
  • The data is corrupted or in an invalid format
  • There are permission issues
§Implementation Notes
  • This method should be idempotent and safe to call multiple times
  • If the storage is empty or doesn’t exist yet, return an empty HashMap
  • Consider adding error recovery mechanisms for corrupted data
Source

fn save<'life0, 'async_trait>( &'life0 self, key: K, value: V, ) -> Pin<Box<dyn Future<Output = Result<(), PersistentError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Save a key-value pair to the storage backend.

This method is called whenever a key-value pair is inserted into the map.

§Errors

Returns a PersistentError if saving fails for any reason, such as:

  • The storage location is not writable
  • There are permission issues
  • The backend has reached capacity
§Implementation Notes
  • This method should be atomic if possible
  • Consider batching or caching writes for better performance
  • If your backend requires serialization, handle serialization errors appropriately
Source

fn delete<'life0, 'life1, 'async_trait>( &'life0 self, key: &'life1 K, ) -> Pin<Box<dyn Future<Output = Result<(), PersistentError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Delete a key-value pair from the storage backend.

This method is called whenever a key-value pair is removed from the map.

§Errors

Returns a PersistentError if deletion fails for any reason, such as:

  • The storage location is not writable
  • There are permission issues
§Implementation Notes
  • This method should be idempotent - deleting a non-existent key should not be an error
  • Consider optimizing for the case where the key doesn’t exist

Provided Methods§

Source

fn flush<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<(), PersistentError>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait,

Flush any buffered writes to the storage backend.

This method is called when the user explicitly requests to ensure all data is persisted.

§Errors

Returns a PersistentError if flushing fails for any reason, such as:

  • The storage location is not writable
  • There are permission issues
§Implementation Notes
  • This method is optional and has a default implementation that does nothing
  • Backends that buffer writes should override this method to ensure data is persisted
  • This method should be idempotent and safe to call multiple times
Source

fn contains_key<'life0, 'life1, 'async_trait>( &'life0 self, key: &'life1 K, ) -> Pin<Box<dyn Future<Output = Result<bool, PersistentError>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Check if a key exists in the storage backend.

This is an optional method with a default implementation that loads all data and checks if the key exists. Backend implementations can override this for better performance if they can check for existence without loading all data.

§Errors

Returns a PersistentError if the check fails for any reason.

§Implementation Notes
  • The default implementation is inefficient for large datasets
  • Override this method if your backend can check for existence more efficiently
Source

fn len<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<usize, PersistentError>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait,

Get the number of key-value pairs in the storage backend.

This is an optional method with a default implementation that loads all data and counts the entries. Backend implementations can override this for better performance if they can count entries without loading all data.

§Errors

Returns a PersistentError if the count operation fails for any reason.

§Implementation Notes
  • The default implementation is inefficient for large datasets
  • Override this method if your backend can count entries more efficiently
Source

fn is_empty<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<bool, PersistentError>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait,

Check if the storage backend is empty.

This is an optional method with a default implementation that uses len(). Backend implementations can override this for better performance.

§Errors

Returns a PersistentError if the check fails for any reason.

§Implementation Notes
  • The default implementation uses len(), which may be inefficient
  • Override this method if your backend can check emptiness more efficiently

Implementors§

Source§

impl<K, V> StorageBackend<K, V> for InMemoryBackend
where K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static, V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,

Source§

impl<K, V> StorageBackend<K, V> for SqliteBackend
where K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static + ToString + FromStr, <K as FromStr>::Err: Error + Send + Sync + 'static, V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,

Implementation of the StorageBackend trait for SqliteBackend.

This implementation provides methods for loading, saving, and deleting key-value pairs from a SQLite database.