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}