use std::sync::Arc;
use crate::config::{AppConfig, ConfigError, StorageMode};
use crate::thingd::ThingdBackend;
pub struct StorageFactory;
impl StorageFactory {
pub fn build(config: &AppConfig) -> Result<Arc<dyn ThingdBackend>, ConfigError> {
match config.storage.mode {
StorageMode::Memory => Ok(Arc::new(crate::thingd::MemoryThingdBackend::new())),
StorageMode::Native | StorageMode::Persistent => {
#[cfg(feature = "thingd-native")]
{
let path = config.storage.persistent_path.as_ref().ok_or_else(|| {
ConfigError::MissingField {
field: "persistent_path".to_string(),
context: "required for native storage".to_string(),
}
})?;
let options = if let Some(key) = &config.storage.encryption_key {
let bytes = hex::decode(key.inner()).map_err(|error| {
ConfigError::InvalidValue {
field: "storage.encryption_key".to_string(),
value: "[REDACTED]".to_string(),
expected: format!("64 hexadecimal characters: {error}"),
}
})?;
let encryption =
thingd::EncryptionConfig::from_key(&bytes).map_err(|error| {
ConfigError::InvalidValue {
field: "storage.encryption_key".to_string(),
value: "[REDACTED]".to_string(),
expected: error.to_string(),
}
})?;
thingd::PersistentOpenOptions {
encryption: Some(encryption),
..Default::default()
}
} else {
thingd::PersistentOpenOptions::default()
};
let backend =
crate::thingd::NativeThingdBackend::persistent_with_options(path, options)
.map_err(|error| ConfigError::InvalidValue {
field: "persistent_path".to_string(),
value: path.display().to_string(),
expected: error.to_string(),
})?;
Ok(Arc::new(backend))
}
#[cfg(not(feature = "thingd-native"))]
{
Err(ConfigError::InvalidValue {
field: "storage.mode".to_string(),
value: "native".to_string(),
expected: "Arqen built with the thingd-native feature".to_string(),
})
}
}
StorageMode::Http => {
#[cfg(feature = "http-client")]
{
let url = config.storage.http_url.as_deref().ok_or_else(|| {
ConfigError::MissingField {
field: "http_url".to_string(),
context: "required for HTTP storage".to_string(),
}
})?;
let mut backend = crate::thingd::HttpThingdBackend::new(url);
if let Some(token) = &config.storage.auth_token {
backend = backend.with_auth(token.inner());
}
Ok(Arc::new(backend))
}
#[cfg(not(feature = "http-client"))]
{
Err(ConfigError::InvalidValue {
field: "storage.mode".to_string(),
value: "http".to_string(),
expected: "Arqen built with the http-client feature".to_string(),
})
}
}
StorageMode::Cloud => Err(ConfigError::InvalidValue {
field: "storage.mode".to_string(),
value: "cloud".to_string(),
expected: "a future public thingd-cloud adapter".to_string(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn memory_mode_constructs_standalone_backend() {
let backend = StorageFactory::build(&AppConfig::default()).unwrap();
backend
.put_object("test", "one", serde_json::json!({"ok":true}))
.await
.unwrap();
assert!(backend.get_object("test", "one").await.unwrap().is_some());
}
#[test]
fn cloud_mode_is_explicitly_not_silently_memory_backed() {
let mut config = AppConfig::default();
config.storage.mode = StorageMode::Cloud;
config.storage.cloud_url = Some("https://cloud.example".to_string());
let error = match StorageFactory::build(&config) {
Ok(_) => panic!("cloud mode must not construct a memory backend"),
Err(error) => error,
};
assert!(error.to_string().contains("future public thingd-cloud"));
}
}