use premortem::{ConfigEnv, RealEnv};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BackendType {
File,
Memory,
}
impl Default for BackendType {
fn default() -> Self {
Self::File
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub backend: BackendType,
#[serde(default = "default_pool_size")]
pub connection_pool_size: usize,
#[serde(default)]
pub retry_policy: RetryPolicy,
#[serde(with = "humantime_serde", default = "default_timeout")]
pub timeout: Duration,
pub backend_config: BackendConfig,
#[serde(default = "default_true")]
pub enable_locking: bool,
#[serde(default)]
pub enable_cache: bool,
#[serde(default)]
pub cache_config: CacheConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BackendConfig {
File(FileConfig),
Memory(MemoryConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileConfig {
pub base_dir: PathBuf,
#[serde(default = "default_true")]
pub use_global: bool,
#[serde(default = "default_true")]
pub enable_file_locks: bool,
#[serde(default = "default_max_file_size")]
pub max_file_size: u64,
#[serde(default)]
pub enable_compression: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
#[serde(default = "default_memory_limit")]
pub max_memory: u64,
#[serde(default)]
pub persist_to_disk: bool,
pub persistence_path: Option<PathBuf>,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
max_memory: 100 * 1024 * 1024, persist_to_disk: false,
persistence_path: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
#[serde(default = "default_max_retries")]
pub max_retries: u32,
#[serde(with = "humantime_serde", default = "default_retry_delay")]
pub initial_delay: Duration,
#[serde(with = "humantime_serde", default = "default_max_retry_delay")]
pub max_delay: Duration,
#[serde(default = "default_backoff_multiplier")]
pub backoff_multiplier: f64,
#[serde(default = "default_true")]
pub jitter: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: default_max_retries(),
initial_delay: default_retry_delay(),
max_delay: default_max_retry_delay(),
backoff_multiplier: default_backoff_multiplier(),
jitter: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
#[serde(default = "default_cache_size")]
pub max_entries: usize,
#[serde(with = "humantime_serde", default = "default_cache_ttl")]
pub ttl: Duration,
#[serde(default)]
pub cache_type: CacheType,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_entries: default_cache_size(),
ttl: default_cache_ttl(),
cache_type: CacheType::default(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CacheType {
#[default]
Memory,
}
fn default_pool_size() -> usize {
10
}
fn default_timeout() -> Duration {
Duration::from_secs(30)
}
fn default_true() -> bool {
true
}
fn default_max_file_size() -> u64 {
100 * 1024 * 1024 }
fn default_memory_limit() -> u64 {
1024 * 1024 * 1024 }
fn default_max_retries() -> u32 {
3
}
fn default_retry_delay() -> Duration {
Duration::from_secs(1)
}
fn default_max_retry_delay() -> Duration {
Duration::from_secs(30)
}
fn default_backoff_multiplier() -> f64 {
2.0
}
fn default_cache_size() -> usize {
1000
}
fn default_cache_ttl() -> Duration {
Duration::from_secs(3600) }
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: BackendType::default(),
connection_pool_size: default_pool_size(),
retry_policy: RetryPolicy::default(),
timeout: default_timeout(),
backend_config: BackendConfig::Memory(MemoryConfig::default()),
enable_locking: true,
enable_cache: false,
cache_config: CacheConfig::default(),
}
}
}
impl StorageConfig {
pub fn from_env() -> crate::LibResult<Self> {
Self::from_env_with(&RealEnv)
}
pub fn from_env_with<E: ConfigEnv>(env: &E) -> crate::LibResult<Self> {
let backend = env
.get_env("PRODIGY_STORAGE_TYPE")
.and_then(|s| match s.to_lowercase().as_str() {
"file" => Some(BackendType::File),
"memory" => Some(BackendType::Memory),
_ => None,
})
.unwrap_or_default();
let backend_config = match backend {
BackendType::File => BackendConfig::File(FileConfig {
base_dir: env
.get_env("PRODIGY_STORAGE_BASE_PATH")
.or_else(|| env.get_env("PRODIGY_STORAGE_DIR"))
.or_else(|| env.get_env("PRODIGY_STORAGE_PATH"))
.map(PathBuf::from)
.unwrap_or_else(|| {
directories::BaseDirs::new()
.map(|dirs| dirs.home_dir().join(".prodigy"))
.unwrap_or_else(|| PathBuf::from("/tmp").join(".prodigy"))
}),
use_global: true, enable_file_locks: true,
max_file_size: default_max_file_size(),
enable_compression: false,
}),
BackendType::Memory => BackendConfig::Memory(Default::default()),
};
Ok(Self {
backend,
connection_pool_size: default_pool_size(),
retry_policy: Default::default(),
timeout: default_timeout(),
backend_config,
enable_locking: true,
enable_cache: false,
cache_config: Default::default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use premortem::MockEnv;
use std::time::Duration;
#[test]
fn test_backend_type_default() {
assert_eq!(BackendType::default(), BackendType::File);
}
#[test]
fn test_backend_type_serialization() {
let backend = BackendType::File;
let json = serde_json::to_string(&backend).unwrap();
assert_eq!(json, r#""file""#);
let backend = BackendType::Memory;
let json = serde_json::to_string(&backend).unwrap();
assert_eq!(json, r#""memory""#);
}
#[test]
fn test_backend_type_deserialization() {
let backend: BackendType = serde_json::from_str(r#""file""#).unwrap();
assert_eq!(backend, BackendType::File);
let backend: BackendType = serde_json::from_str(r#""memory""#).unwrap();
assert_eq!(backend, BackendType::Memory);
}
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.backend, BackendType::File);
assert_eq!(config.connection_pool_size, 10);
assert_eq!(config.timeout, Duration::from_secs(30));
assert!(config.enable_locking);
assert!(!config.enable_cache);
}
#[test]
fn test_file_config_defaults() {
let config = FileConfig {
base_dir: PathBuf::from("/test"),
use_global: default_true(),
enable_file_locks: default_true(),
max_file_size: default_max_file_size(),
enable_compression: false,
};
assert_eq!(config.base_dir, PathBuf::from("/test"));
assert!(config.use_global);
assert!(config.enable_file_locks);
assert_eq!(config.max_file_size, 100 * 1024 * 1024); }
#[test]
fn test_memory_config_defaults() {
let config = MemoryConfig::default();
assert_eq!(config.max_memory, 100 * 1024 * 1024); }
#[test]
fn test_retry_policy_default() {
let policy = RetryPolicy::default();
assert!(policy.max_retries > 0);
assert!(policy.initial_delay > Duration::from_secs(0));
assert!(policy.max_delay > Duration::from_secs(0));
assert!(policy.backoff_multiplier > 1.0);
}
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert!(config.max_entries > 0);
assert!(config.ttl > Duration::from_secs(0));
}
#[test]
fn test_storage_config_from_env_defaults() {
let env = MockEnv::new();
let config = StorageConfig::from_env_with(&env).unwrap();
assert_eq!(config.backend, BackendType::File);
assert!(config.enable_locking);
assert!(!config.enable_cache);
if let BackendConfig::File(file_config) = config.backend_config {
assert!(file_config.base_dir.to_string_lossy().contains(".prodigy"));
assert!(file_config.use_global);
} else {
panic!("Expected FileConfig");
}
}
#[test]
fn test_storage_config_from_env_file_type() {
let env = MockEnv::new()
.with_env("PRODIGY_STORAGE_TYPE", "file")
.with_env("PRODIGY_STORAGE_BASE_PATH", "/custom/path");
let config = StorageConfig::from_env_with(&env).unwrap();
assert_eq!(config.backend, BackendType::File);
if let BackendConfig::File(file_config) = config.backend_config {
assert_eq!(file_config.base_dir, PathBuf::from("/custom/path"));
} else {
panic!("Expected FileConfig");
}
}
#[test]
fn test_storage_config_from_env_memory_type() {
let env = MockEnv::new().with_env("PRODIGY_STORAGE_TYPE", "memory");
let config = StorageConfig::from_env_with(&env).unwrap();
assert_eq!(config.backend, BackendType::Memory);
if let BackendConfig::Memory(memory_config) = config.backend_config {
assert_eq!(memory_config.max_memory, 100 * 1024 * 1024);
} else {
panic!("Expected MemoryConfig");
}
}
#[test]
fn test_storage_config_from_env_invalid_type() {
let env = MockEnv::new().with_env("PRODIGY_STORAGE_TYPE", "invalid");
let result = StorageConfig::from_env_with(&env);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.backend, BackendType::File);
}
#[test]
fn test_storage_config_serialization() {
let config = StorageConfig::default();
let json = serde_json::to_string(&config).unwrap();
let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.backend, deserialized.backend);
assert_eq!(
config.connection_pool_size,
deserialized.connection_pool_size
);
assert_eq!(config.enable_locking, deserialized.enable_locking);
}
#[test]
fn test_file_config_serialization() {
let config = FileConfig {
base_dir: PathBuf::from("/test/path"),
use_global: true,
enable_file_locks: false,
max_file_size: 1024,
enable_compression: true,
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: FileConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.base_dir, deserialized.base_dir);
assert_eq!(config.use_global, deserialized.use_global);
assert_eq!(config.enable_file_locks, deserialized.enable_file_locks);
assert_eq!(config.max_file_size, deserialized.max_file_size);
}
#[test]
fn test_default_helper_functions() {
assert!(default_true());
assert_eq!(default_pool_size(), 10);
assert_eq!(default_timeout(), Duration::from_secs(30));
assert_eq!(default_max_file_size(), 100 * 1024 * 1024);
}
#[test]
fn test_backend_config_untagged_enum() {
let file_json = r#"{
"base_dir": "/test",
"use_global": true,
"enable_file_locks": true,
"max_file_size": 1000000,
"enable_compression": false
}"#;
let backend_config: BackendConfig = serde_json::from_str(file_json).unwrap();
if let BackendConfig::File(config) = backend_config {
assert_eq!(config.base_dir, PathBuf::from("/test"));
assert!(config.use_global);
} else {
panic!("Expected FileConfig");
}
}
#[test]
fn test_retry_policy_validation() {
let policy = RetryPolicy {
max_retries: 5,
initial_delay: Duration::from_millis(50),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
jitter: true,
};
assert_eq!(policy.max_retries, 5);
assert_eq!(policy.initial_delay, Duration::from_millis(50));
assert_eq!(policy.max_delay, Duration::from_secs(60));
assert_eq!(policy.backoff_multiplier, 2.0);
assert!(policy.jitter);
}
#[test]
fn test_cache_config_with_custom_values() {
let config = CacheConfig {
max_entries: 5000,
ttl: Duration::from_secs(600),
cache_type: Default::default(),
};
assert_eq!(config.max_entries, 5000);
assert_eq!(config.ttl, Duration::from_secs(600));
}
}