use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageBackendConfig {
pub id: String,
#[serde(flatten)]
pub spec: StorageBackendSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all_fields = "camelCase", deny_unknown_fields)]
pub enum StorageBackendSpec {
#[serde(rename = "memory")]
Memory {
#[serde(default)]
enable_archive: bool,
},
#[serde(untagged)]
Plugin {
kind: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StorageBackendRef {
Named(String),
Inline(StorageBackendSpec),
}
impl StorageBackendSpec {
pub fn validate(&self) -> Result<(), String> {
match self {
StorageBackendSpec::Memory { .. } => Ok(()),
StorageBackendSpec::Plugin { kind } => {
if kind.trim().is_empty() {
return Err("Storage backend 'kind' must not be empty".to_string());
}
if kind.trim() == "memory" {
return Err(
"Storage backend kind 'memory' is reserved for the in-memory backend"
.to_string(),
);
}
Ok(())
}
}
}
pub fn is_volatile(&self) -> bool {
matches!(self, StorageBackendSpec::Memory { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_roundtrip_both_variants() {
let memory = StorageBackendSpec::Memory {
enable_archive: true,
};
let memory_value = serde_json::to_value(&memory).unwrap();
assert_eq!(memory_value["kind"], "memory");
match serde_json::from_value::<StorageBackendSpec>(memory_value).unwrap() {
StorageBackendSpec::Memory { enable_archive } => assert!(enable_archive),
_ => panic!("Expected Memory variant after JSON round-trip"),
}
let plugin = StorageBackendSpec::Plugin {
kind: "rocksdb".to_string(),
};
let plugin_value = serde_json::to_value(&plugin).unwrap();
assert_eq!(plugin_value, serde_json::json!({ "kind": "rocksdb" }));
match serde_json::from_value::<StorageBackendSpec>(plugin_value).unwrap() {
StorageBackendSpec::Plugin { kind } => assert_eq!(kind, "rocksdb"),
_ => panic!("Expected Plugin variant after JSON round-trip"),
}
}
#[test]
fn test_memory_serde() {
let yaml = r#"
kind: memory
enableArchive: true
"#;
let spec: StorageBackendSpec = serde_yaml::from_str(yaml).unwrap();
match spec {
StorageBackendSpec::Memory { enable_archive } => {
assert!(enable_archive);
}
_ => panic!("Expected Memory variant"),
}
let serialized = serde_yaml::to_string(&spec).unwrap();
let deserialized: StorageBackendSpec = serde_yaml::from_str(&serialized).unwrap();
match deserialized {
StorageBackendSpec::Memory { enable_archive } => {
assert!(enable_archive);
}
_ => panic!("Expected Memory variant after round-trip"),
}
}
#[test]
fn test_plugin_properties_rejected() {
let yaml = r#"
id: rocks
kind: rocksdb
path: /data/drasi
enableArchive: true
directIo: false
"#;
assert!(serde_yaml::from_str::<StorageBackendConfig>(yaml).is_err());
let json = serde_json::json!({
"id": "rocks",
"kind": "rocksdb",
"path": "/data/drasi"
});
assert!(serde_json::from_value::<StorageBackendConfig>(json).is_err());
}
#[test]
fn test_plugin_without_config_serde() {
let spec: StorageBackendSpec = serde_yaml::from_str("kind: rocksdb").unwrap();
match &spec {
StorageBackendSpec::Plugin { kind } => assert_eq!(kind, "rocksdb"),
_ => panic!("Expected Plugin variant"),
}
assert!(spec.validate().is_ok());
}
#[test]
fn test_storage_backend_config_serde() {
let yaml = r#"
id: rocks_persistent
kind: rocksdb
"#;
let config: StorageBackendConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.id, "rocks_persistent");
match config.spec {
StorageBackendSpec::Plugin { kind } => assert_eq!(kind, "rocksdb"),
_ => panic!("Expected Plugin variant"),
}
}
#[test]
fn test_storage_backend_config_memory_serde() {
let yaml = r#"
id: mem
kind: memory
enableArchive: true
"#;
let config: StorageBackendConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.id, "mem");
match config.spec {
StorageBackendSpec::Memory { enable_archive } => assert!(enable_archive),
_ => panic!("Expected Memory variant"),
}
}
#[test]
fn test_storage_backend_ref_named() {
let yaml = r#""rocks_persistent""#;
let ref_val: StorageBackendRef = serde_yaml::from_str(yaml).unwrap();
match ref_val {
StorageBackendRef::Named(name) => {
assert_eq!(name, "rocks_persistent");
}
_ => panic!("Expected Named variant"),
}
}
#[test]
fn test_storage_backend_ref_inline() {
let yaml = r#"
kind: memory
enableArchive: false
"#;
let ref_val: StorageBackendRef = serde_yaml::from_str(yaml).unwrap();
match ref_val {
StorageBackendRef::Inline(spec) => match spec {
StorageBackendSpec::Memory { enable_archive } => {
assert!(!enable_archive);
}
_ => panic!("Expected Memory variant"),
},
_ => panic!("Expected Inline variant"),
}
}
#[test]
fn test_validate_memory() {
let spec = StorageBackendSpec::Memory {
enable_archive: true,
};
assert!(spec.validate().is_ok());
}
#[test]
fn test_validate_plugin_without_config() {
let spec = StorageBackendSpec::Plugin {
kind: "rocksdb".to_string(),
};
assert!(spec.validate().is_ok());
}
#[test]
fn test_validate_plugin_memory_kind() {
let spec = StorageBackendSpec::Plugin {
kind: "memory".to_string(),
};
let err = spec.validate().unwrap_err();
assert!(err.contains("reserved"));
}
#[test]
fn test_validate_plugin_empty_kind() {
let spec = StorageBackendSpec::Plugin {
kind: " ".to_string(),
};
assert!(spec.validate().is_err());
let err = spec.validate().unwrap_err();
assert!(err.contains("must not be empty"));
}
#[test]
fn test_is_volatile() {
let memory_spec = StorageBackendSpec::Memory {
enable_archive: false,
};
assert!(memory_spec.is_volatile());
let plugin_spec = StorageBackendSpec::Plugin {
kind: "rocksdb".to_string(),
};
assert!(!plugin_spec.is_volatile());
}
}