bothan_lib/
store.rs

1//! Storage abstractions for asset information and registry data.
2//!
3//! This module provides traits and implementations for persistent storage of asset information
4//! and registry data. It defines a common interface for different storage backends while
5//! ensuring thread safety and atomic operations.
6//!
7//! The module provides:
8//!
9//! - The [`Store`] trait which defines the common interface for all storage implementations
10//! - Specialized store implementations for different storage backends
11//! - Worker-specific store functionality through the [`WorkerStore`] trait
12//!
13//! # Storage Capabilities
14//!
15//! The store is responsible for:
16//!
17//! - Storing and retrieving the registry and its IPFS hash
18//! - Managing asset information with prefix-based organization
19//! - Supporting both individual and batch operations for asset data
20//!
21//! # Extensibility
22//!
23//! While this module provides general-purpose implementations for common storage backends,
24//! the [`Store`] trait is designed for extensibility. Users can create custom storage
25//! implementations by implementing this trait to support specialized backends, caching
26//! strategies, or data transformation logic beyond what's provided in the standard implementations.
27//!
28//! # Implementation
29//!
30//! When implementing a custom store:
31//!
32//! 1. Implement the [`Store`] trait
33//! 2. Define an appropriate `Error` type that captures all possible storage failures
34//! 3. Ensure thread safety and atomicity in all operations
35//! 4. Make the implementation efficiently cloneable for use across tasks
36
37use std::error::Error as StdError;
38
39use async_trait::async_trait;
40pub use worker::WorkerStore;
41
42use crate::registry::{Registry, Valid};
43use crate::types::AssetInfo;
44
45mod worker;
46
47/// The universal trait for all stores. All implementations must be thread-safe and atomic.
48///
49/// This trait defines the common interface that all storage implementations must provide,
50/// enabling the system to interact with different storage backends in a uniform way.
51/// Each store is responsible for reliably persisting asset information and registry data.
52///
53/// # Thread Safety and Atomicity
54///
55/// All implementations must be thread-safe (Send + Sync) and provide atomic operations
56/// to ensure data consistency when used concurrently.
57///
58/// # Required Methods
59///
60/// Implementors must define:
61/// * Storage operations for registry data (`set_registry`, `get_registry`, `get_registry_ipfs_hash`)
62/// * Asset information operations (`get_asset_info`, `insert_asset_info`, `insert_batch_asset_info`)
63///
64/// # Example Use
65///
66/// Implementing a simple in-memory store:
67///
68/// ```rust
69/// use std::collections::HashMap;
70/// use std::error::Error;
71/// use std::fmt;
72/// use std::sync::{Arc, RwLock};
73/// use async_trait::async_trait;
74/// use bothan_lib::store::Store;
75/// use bothan_lib::registry::{Registry, Valid};
76/// use bothan_lib::types::AssetInfo;
77/// use thiserror::Error;
78///
79/// // Custom error type for the memory store
80/// #[derive(Debug, Error)]
81/// #[error("Memory store error: {0}")]
82/// struct MemoryStoreError(String);
83///
84/// // Memory-based store implementation
85/// #[derive(Clone)]
86/// struct MemoryStore {
87///     registry: Arc<RwLock<Option<(Registry<Valid>, String)>>>,
88///     assets: Arc<RwLock<HashMap<String, HashMap<String, AssetInfo>>>>,
89/// }
90///
91/// impl MemoryStore {
92///     fn new() -> Self {
93///         MemoryStore {
94///             registry: Arc::new(RwLock::new(None)),
95///             assets: Arc::new(RwLock::new(HashMap::new())),
96///         }
97///     }
98/// }
99///
100/// #[async_trait]
101/// impl Store for MemoryStore {
102///     type Error = MemoryStoreError;
103///
104///     async fn set_registry(
105///         &self,
106///         registry: Registry<Valid>,
107///         ipfs_hash: String,
108///     ) -> Result<(), Self::Error> {
109///         let mut reg = self.registry.write().map_err(|e| MemoryStoreError(e.to_string()))?;
110///         *reg = Some((registry, ipfs_hash));
111///         Ok(())
112///     }
113///
114///     async fn get_registry(&self) -> Result<Registry<Valid>, Self::Error> {
115///         let reg = self.registry.read().map_err(|e| MemoryStoreError(e.to_string()))?;
116///         match reg.as_ref() {
117///             Some((registry, _)) => Ok(registry.clone()),
118///             None => Err(MemoryStoreError("Registry not found".to_string())),
119///         }
120///     }
121///
122///     async fn get_registry_ipfs_hash(&self) -> Result<Option<String>, Self::Error> {
123///         let reg = self.registry.read().map_err(|e| MemoryStoreError(e.to_string()))?;
124///         Ok(reg.as_ref().map(|(_, hash)| hash.clone()))
125///     }
126///
127///     async fn get_asset_info(
128///         &self,
129///         prefix: &str,
130///         id: &str,
131///     ) -> Result<Option<AssetInfo>, Self::Error> {
132///         let assets = self.assets.read().map_err(|e| MemoryStoreError(e.to_string()))?;
133///         Ok(assets
134///             .get(prefix)
135///             .and_then(|prefix_map| prefix_map.get(id).cloned()))
136///     }
137///
138///     async fn insert_asset_info(
139///         &self,
140///         prefix: &str,
141///         asset_info: AssetInfo,
142///     ) -> Result<(), Self::Error> {
143///         let mut assets = self.assets.write().map_err(|e| MemoryStoreError(e.to_string()))?;
144///         let prefix_map = assets.entry(prefix.to_string()).or_insert_with(HashMap::new);
145///         prefix_map.insert(asset_info.id.clone(), asset_info);
146///         Ok(())
147///     }
148///
149///     async fn insert_batch_asset_info(
150///         &self,
151///         prefix: &str,
152///         asset_infos: Vec<AssetInfo>,
153///     ) -> Result<(), Self::Error> {
154///         let mut assets = self.assets.write().map_err(|e| MemoryStoreError(e.to_string()))?;
155///         let prefix_map = assets.entry(prefix.to_string()).or_insert_with(HashMap::new);
156///         
157///         for asset_info in asset_infos {
158///             prefix_map.insert(asset_info.id.clone(), asset_info);
159///         }
160///         Ok(())
161///     }
162/// }
163/// ```
164#[async_trait]
165pub trait Store: Send + Sync + Clone {
166    /// The type returned in the event of an operation failure.
167    ///
168    /// This should be a custom error type that implements the standard Error trait
169    /// and captures all possible error conditions specific to the storage backend.
170    type Error: StdError + Send + Sync + 'static;
171
172    /// Stores a validated registry and its IPFS hash in the storage backend.
173    ///
174    /// This operation should be atomic to ensure registry consistency.
175    /// It should replace any existing registry completely.
176    ///
177    /// # Errors
178    ///
179    /// Returns a storage-specific error if the operation fails, such as when
180    /// the storage backend is unavailable or the data cannot be written.
181    async fn set_registry(
182        &self,
183        registry: Registry<Valid>,
184        ipfs_hash: String,
185    ) -> Result<(), Self::Error>;
186
187    /// Retrieves the current validated registry from the storage backend.
188    ///
189    /// # Errors
190    ///
191    /// Returns a storage-specific error if the operation fails, such as when
192    /// the storage backend is unavailable or the registry does not exist.
193    async fn get_registry(&self) -> Result<Registry<Valid>, Self::Error>;
194
195    /// Retrieves the IPFS hash of the current registry from the storage backend.
196    ///
197    /// # Errors
198    ///
199    /// Returns a storage-specific error if the operation fails, such as when
200    /// the storage backend is unavailable.
201    async fn get_registry_ipfs_hash(&self) -> Result<Option<String>, Self::Error>;
202
203    /// Retrieves asset information for a specific asset ID within a namespace prefix.
204    ///
205    /// # Errors
206    ///
207    /// Returns a storage-specific error if the operation fails, such as when
208    /// the storage backend is unavailable.
209    async fn get_asset_info(
210        &self,
211        prefix: &str,
212        id: &str,
213    ) -> Result<Option<AssetInfo>, Self::Error>;
214
215    /// Stores asset information under the specified namespace prefix.
216    ///
217    /// If asset information already exists for the given prefix and asset ID,
218    /// it should be completely replaced with the new information.
219    ///
220    /// # Errors
221    ///
222    /// Returns a storage-specific error if the operation fails, such as when
223    /// the storage backend is unavailable or the data cannot be written.
224    async fn insert_asset_info(
225        &self,
226        prefix: &str,
227        asset_info: AssetInfo,
228    ) -> Result<(), Self::Error>;
229
230    /// Stores multiple asset information entries in a single batch operation.
231    ///
232    /// This operation should be optimized for bulk insertion and preferably
233    /// performed atomically if the backend supports transactions.
234    ///
235    /// # Errors
236    ///
237    /// Returns a storage-specific error if the operation fails, such as when
238    /// the storage backend is unavailable, the data cannot be written, or
239    /// the transaction cannot be committed.
240    async fn insert_batch_asset_info(
241        &self,
242        prefix: &str,
243        asset_infos: Vec<AssetInfo>,
244    ) -> Result<(), Self::Error>;
245}