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}