use super::traits::StorageBackend;
#[cfg(feature = "automerge-backend")]
use anyhow::Context;
use anyhow::{anyhow, Result};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct StorageConfig {
pub backend: String,
pub data_path: Option<PathBuf>,
pub in_memory: bool,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: "automerge-memory".to_string(),
data_path: None,
in_memory: false,
}
}
}
impl StorageConfig {
pub fn from_env() -> Result<Self> {
let backend =
std::env::var("CAP_STORAGE_BACKEND").unwrap_or_else(|_| "automerge-memory".to_string());
let data_path = std::env::var("CAP_DATA_PATH").ok().map(PathBuf::from);
let in_memory = std::env::var("CAP_IN_MEMORY")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
Ok(Self {
backend,
data_path,
in_memory,
})
}
pub fn validate(&self) -> Result<()> {
match self.backend.as_str() {
"automerge-memory" => {
Ok(())
}
"redb" => {
if self.data_path.is_none() {
return Err(anyhow!("redb backend requires CAP_DATA_PATH to be set"));
}
Ok(())
}
other => Err(anyhow!("Unknown storage backend: {}", other)),
}
}
}
pub fn create_storage_backend(config: &StorageConfig) -> Result<Arc<dyn StorageBackend>> {
config.validate()?;
match config.backend.as_str() {
"automerge-memory" => {
#[cfg(feature = "automerge-backend")]
{
use crate::storage::automerge_backend::AutomergeBackend;
use crate::storage::automerge_store::AutomergeStore;
let automerge_store = if config.in_memory {
tracing::info!("Creating AutomergeStore in MEMORY-ONLY mode");
AutomergeStore::in_memory()
} else {
let path = config.data_path.as_deref().ok_or_else(|| {
anyhow!("Automerge backend requires CAP_DATA_PATH to be set for persistence (or use CAP_IN_MEMORY=true)")
})?;
AutomergeStore::open(path).context("Failed to create Automerge backend")?
};
let backend = AutomergeBackend::new(Arc::new(automerge_store));
Ok(Arc::new(backend))
}
#[cfg(not(feature = "automerge-backend"))]
{
Err(anyhow!(
"Automerge backend not enabled.\n\
Rebuild with --features automerge-backend to use this backend."
))
}
}
"redb" => {
Err(anyhow!(
"Direct redb backend not available.\n\
Use CAP_STORAGE_BACKEND=automerge-memory for redb-backed storage."
))
}
other => Err(anyhow!(
"Unknown storage backend: {}\n\
Supported backends: automerge-memory, redb",
other
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.backend, "automerge-memory");
assert!(config.data_path.is_none());
}
#[test]
fn test_storage_config_validation_automerge_memory() {
let config = StorageConfig {
backend: "automerge-memory".to_string(),
data_path: None,
in_memory: false,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_storage_config_validation_redb_requires_path() {
let config = StorageConfig {
backend: "redb".to_string(),
data_path: None,
in_memory: false,
};
assert!(config.validate().is_err());
let config_with_path = StorageConfig {
backend: "redb".to_string(),
data_path: Some(PathBuf::from("/var/cap/data")),
in_memory: false,
};
assert!(config_with_path.validate().is_ok());
}
#[test]
fn test_storage_config_validation_unknown_backend() {
let config = StorageConfig {
backend: "unknown".to_string(),
data_path: None,
in_memory: false,
};
assert!(config.validate().is_err());
}
#[test]
fn test_create_backend_automerge_requires_data_path() {
let config = StorageConfig {
backend: "automerge-memory".to_string(),
data_path: None,
in_memory: false, };
let result = create_storage_backend(&config);
assert!(result.is_err());
match result {
#[cfg(feature = "automerge-backend")]
Err(e) => assert!(e.to_string().contains("CAP_DATA_PATH")),
#[cfg(not(feature = "automerge-backend"))]
Err(e) => assert!(e.to_string().contains("not enabled")),
Ok(_) => panic!("Expected error but got Ok"),
}
}
#[test]
fn test_create_backend_redb_not_available() {
let config = StorageConfig {
backend: "redb".to_string(),
data_path: Some(PathBuf::from("/tmp/test")),
in_memory: false,
};
let result = create_storage_backend(&config);
assert!(result.is_err());
match result {
Err(e) => assert!(e.to_string().contains("not available")),
Ok(_) => panic!("Expected error but got Ok"),
}
}
#[test]
fn test_create_backend_unknown() {
let config = StorageConfig {
backend: "unknown".to_string(),
data_path: None,
in_memory: false,
};
let result = create_storage_backend(&config);
assert!(result.is_err());
match result {
Err(e) => assert!(e.to_string().contains("Unknown storage backend")),
Ok(_) => panic!("Expected error but got Ok"),
}
}
}