Skip to main content

peat_protocol/storage/
backend.rs

1//! Storage backend factory and configuration
2//!
3//! This module provides runtime backend selection through configuration.
4//! The backend can be chosen via environment variables or programmatic configuration.
5//!
6//! # Environment Variables
7//!
8//! - `CAP_STORAGE_BACKEND`: Backend type ("automerge-memory", "redb")
9//! - `CAP_DATA_PATH`: Data directory path for persistent backends
10//!
11//! # Example
12//!
13//! ```ignore
14//! use peat_protocol::storage::{StorageConfig, create_storage_backend};
15//!
16//! // Load from environment
17//! let config = StorageConfig::from_env()?;
18//! let storage = create_storage_backend(&config)?;
19//!
20//! // Or create manually
21//! let config = StorageConfig {
22//!     backend: "automerge-memory".to_string(),
23//!     data_path: Some("/var/cap/data".to_string()),
24//! };
25//! let storage = create_storage_backend(&config)?;
26//! ```
27
28use super::traits::StorageBackend;
29#[cfg(feature = "automerge-backend")]
30use anyhow::Context;
31use anyhow::{anyhow, Result};
32use std::path::PathBuf;
33use std::sync::Arc;
34
35/// Storage backend configuration
36///
37/// Determines which storage implementation to use and how to configure it.
38#[derive(Clone, Debug)]
39pub struct StorageConfig {
40    /// Backend type identifier
41    ///
42    /// Supported values:
43    /// - `"automerge-memory"`: Automerge in-memory (POC, testing)
44    /// - `"redb"`: redb persistence (production target)
45    pub backend: String,
46
47    /// Data directory path for persistent backends
48    ///
49    /// Required for redb, optional for others.
50    /// Example: `/var/cap/data`, `./data`, `/tmp/cap-test`
51    pub data_path: Option<PathBuf>,
52
53    /// Run in pure in-memory mode (no disk persistence)
54    ///
55    /// When true, the automerge backend will skip all disk writes and store
56    /// documents only in the LRU cache. Useful for high-throughput testing
57    /// where persistence is not needed.
58    pub in_memory: bool,
59}
60
61impl Default for StorageConfig {
62    /// Create configuration with defaults
63    ///
64    /// Uses the Automerge in-memory backend.
65    ///
66    /// # Returns
67    ///
68    /// Default configuration (Automerge in-memory backend)
69    fn default() -> Self {
70        Self {
71            backend: "automerge-memory".to_string(),
72            data_path: None,
73            in_memory: false,
74        }
75    }
76}
77
78impl StorageConfig {
79    /// Create configuration from environment variables
80    ///
81    /// # Environment Variables
82    ///
83    /// - `CAP_STORAGE_BACKEND` (default: "automerge-memory")
84    /// - `CAP_DATA_PATH` (optional, required for some backends)
85    ///
86    /// # Returns
87    ///
88    /// StorageConfig loaded from environment
89    ///
90    /// # Example
91    ///
92    /// ```bash
93    /// export CAP_STORAGE_BACKEND=automerge-memory
94    /// export CAP_DATA_PATH=/var/cap/data
95    /// ```
96    ///
97    /// ```ignore
98    /// let config = StorageConfig::from_env()?;
99    /// assert_eq!(config.backend, "automerge-memory");
100    /// ```
101    pub fn from_env() -> Result<Self> {
102        let backend =
103            std::env::var("CAP_STORAGE_BACKEND").unwrap_or_else(|_| "automerge-memory".to_string());
104
105        let data_path = std::env::var("CAP_DATA_PATH").ok().map(PathBuf::from);
106
107        // CAP_IN_MEMORY=true enables pure in-memory mode (no disk persistence)
108        let in_memory = std::env::var("CAP_IN_MEMORY")
109            .map(|v| v == "1" || v.to_lowercase() == "true")
110            .unwrap_or(false);
111
112        Ok(Self {
113            backend,
114            data_path,
115            in_memory,
116        })
117    }
118
119    /// Validate configuration
120    ///
121    /// Checks that required fields are present for the selected backend.
122    ///
123    /// # Returns
124    ///
125    /// Ok(()) if valid, Err with description if invalid
126    ///
127    /// # Errors
128    ///
129    /// - redb requires data_path
130    /// - Unknown backend type
131    pub fn validate(&self) -> Result<()> {
132        match self.backend.as_str() {
133            "automerge-memory" => {
134                // In-memory, no data path needed
135                Ok(())
136            }
137            "redb" => {
138                if self.data_path.is_none() {
139                    return Err(anyhow!("redb backend requires CAP_DATA_PATH to be set"));
140                }
141                Ok(())
142            }
143            other => Err(anyhow!("Unknown storage backend: {}", other)),
144        }
145    }
146}
147
148/// Create storage backend from configuration
149///
150/// Factory function that instantiates the appropriate backend based on configuration.
151///
152/// # Arguments
153///
154/// * `config` - Storage configuration (backend type, data path, etc.)
155///
156/// # Returns
157///
158/// Arc-wrapped trait object for the selected backend
159///
160/// # Errors
161///
162/// - Unknown backend type
163/// - Backend initialization fails
164/// - Invalid configuration
165///
166/// # Example
167///
168/// ```ignore
169/// let config = StorageConfig::from_env()?;
170/// let storage = create_storage_backend(&config)?;
171///
172/// // Use storage
173/// let cells = storage.collection("cells");
174/// cells.upsert("cell-1", data)?;
175/// ```
176pub fn create_storage_backend(config: &StorageConfig) -> Result<Arc<dyn StorageBackend>> {
177    // Validate configuration first
178    config.validate()?;
179
180    match config.backend.as_str() {
181        "automerge-memory" => {
182            #[cfg(feature = "automerge-backend")]
183            {
184                use crate::storage::automerge_backend::AutomergeBackend;
185                use crate::storage::automerge_store::AutomergeStore;
186
187                // Create AutomergeStore - in-memory or with persistence
188                let automerge_store = if config.in_memory {
189                    tracing::info!("Creating AutomergeStore in MEMORY-ONLY mode");
190                    AutomergeStore::in_memory()
191                } else {
192                    // Determine storage path (use data_path if provided, otherwise temp)
193                    let path = config.data_path.as_deref().ok_or_else(|| {
194                        anyhow!("Automerge backend requires CAP_DATA_PATH to be set for persistence (or use CAP_IN_MEMORY=true)")
195                    })?;
196                    AutomergeStore::open(path).context("Failed to create Automerge backend")?
197                };
198
199                // Wrap in AutomergeBackend trait adapter (without transport for now)
200                let backend = AutomergeBackend::new(Arc::new(automerge_store));
201
202                Ok(Arc::new(backend))
203            }
204            #[cfg(not(feature = "automerge-backend"))]
205            {
206                Err(anyhow!(
207                    "Automerge backend not enabled.\n\
208                     Rebuild with --features automerge-backend to use this backend."
209                ))
210            }
211        }
212        "redb" => {
213            // redb is used internally by automerge-backend
214            // There's no standalone redb backend - use automerge-memory instead
215            Err(anyhow!(
216                "Direct redb backend not available.\n\
217                 Use CAP_STORAGE_BACKEND=automerge-memory for redb-backed storage."
218            ))
219        }
220        other => Err(anyhow!(
221            "Unknown storage backend: {}\n\
222             Supported backends: automerge-memory, redb",
223            other
224        )),
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_storage_config_default() {
234        let config = StorageConfig::default();
235        assert_eq!(config.backend, "automerge-memory");
236        assert!(config.data_path.is_none());
237    }
238
239    #[test]
240    fn test_storage_config_validation_automerge_memory() {
241        let config = StorageConfig {
242            backend: "automerge-memory".to_string(),
243            data_path: None,
244            in_memory: false,
245        };
246        assert!(config.validate().is_ok());
247    }
248
249    #[test]
250    fn test_storage_config_validation_redb_requires_path() {
251        let config = StorageConfig {
252            backend: "redb".to_string(),
253            data_path: None,
254            in_memory: false,
255        };
256        assert!(config.validate().is_err());
257
258        let config_with_path = StorageConfig {
259            backend: "redb".to_string(),
260            data_path: Some(PathBuf::from("/var/cap/data")),
261            in_memory: false,
262        };
263        assert!(config_with_path.validate().is_ok());
264    }
265
266    #[test]
267    fn test_storage_config_validation_unknown_backend() {
268        let config = StorageConfig {
269            backend: "unknown".to_string(),
270            data_path: None,
271            in_memory: false,
272        };
273        assert!(config.validate().is_err());
274    }
275
276    #[test]
277    fn test_create_backend_automerge_requires_data_path() {
278        let config = StorageConfig {
279            backend: "automerge-memory".to_string(),
280            data_path: None,
281            in_memory: false, // Not in-memory, so needs data_path
282        };
283        let result = create_storage_backend(&config);
284        assert!(result.is_err());
285        match result {
286            #[cfg(feature = "automerge-backend")]
287            Err(e) => assert!(e.to_string().contains("CAP_DATA_PATH")),
288            #[cfg(not(feature = "automerge-backend"))]
289            Err(e) => assert!(e.to_string().contains("not enabled")),
290            Ok(_) => panic!("Expected error but got Ok"),
291        }
292    }
293
294    #[test]
295    fn test_create_backend_redb_not_available() {
296        let config = StorageConfig {
297            backend: "redb".to_string(),
298            data_path: Some(PathBuf::from("/tmp/test")),
299            in_memory: false,
300        };
301        let result = create_storage_backend(&config);
302        assert!(result.is_err());
303        match result {
304            Err(e) => assert!(e.to_string().contains("not available")),
305            Ok(_) => panic!("Expected error but got Ok"),
306        }
307    }
308
309    #[test]
310    fn test_create_backend_unknown() {
311        let config = StorageConfig {
312            backend: "unknown".to_string(),
313            data_path: None,
314            in_memory: false,
315        };
316        let result = create_storage_backend(&config);
317        assert!(result.is_err());
318        match result {
319            Err(e) => assert!(e.to_string().contains("Unknown storage backend")),
320            Ok(_) => panic!("Expected error but got Ok"),
321        }
322    }
323}