//! Worker Configuration Module - TOML-based configuration for Hero workers use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; use std::time::Duration; /// Worker configuration loaded from TOML file #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkerConfig { /// Worker identification pub worker_id: String, /// Redis connection URL pub redis_url: String, /// Database path for Rhai engine pub db_path: String, /// Whether to preserve task details after completion #[serde(default = "default_preserve_tasks")] pub preserve_tasks: bool, /// Worker type configuration pub worker_type: WorkerType, /// Logging configuration #[serde(default)] pub logging: LoggingConfig, } /// Worker type configuration #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum WorkerType { /// Synchronous worker configuration #[serde(rename = "sync")] Sync, /// Asynchronous worker configuration #[serde(rename = "async")] Async { /// Default timeout for jobs in seconds #[serde(default = "default_timeout_seconds")] default_timeout_seconds: u64, }, } /// Logging configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { /// Whether to include timestamps in log output #[serde(default = "default_timestamps")] pub timestamps: bool, /// Log level (trace, debug, info, warn, error) #[serde(default = "default_log_level")] pub level: String, } impl Default for LoggingConfig { fn default() -> Self { Self { timestamps: default_timestamps(), level: default_log_level(), } } } impl WorkerConfig { /// Load configuration from TOML file pub fn from_file>(path: P) -> Result { let content = fs::read_to_string(&path) .map_err(|e| ConfigError::IoError(format!("Failed to read config file: {}", e)))?; let config: WorkerConfig = toml::from_str(&content) .map_err(|e| ConfigError::ParseError(format!("Failed to parse TOML: {}", e)))?; config.validate()?; Ok(config) } /// Validate the configuration fn validate(&self) -> Result<(), ConfigError> { if self.worker_id.is_empty() { return Err(ConfigError::ValidationError("worker_id cannot be empty".to_string())); } if self.redis_url.is_empty() { return Err(ConfigError::ValidationError("redis_url cannot be empty".to_string())); } if self.db_path.is_empty() { return Err(ConfigError::ValidationError("db_path cannot be empty".to_string())); } // Validate log level match self.logging.level.to_lowercase().as_str() { "trace" | "debug" | "info" | "warn" | "error" => {}, _ => return Err(ConfigError::ValidationError( format!("Invalid log level: {}. Must be one of: trace, debug, info, warn, error", self.logging.level) )), } Ok(()) } /// Get the default timeout duration for async workers pub fn get_default_timeout(&self) -> Option { match &self.worker_type { WorkerType::Sync => None, WorkerType::Async { default_timeout_seconds } => { Some(Duration::from_secs(*default_timeout_seconds)) } } } /// Check if this is a sync worker configuration pub fn is_sync(&self) -> bool { matches!(self.worker_type, WorkerType::Sync) } /// Check if this is an async worker configuration pub fn is_async(&self) -> bool { matches!(self.worker_type, WorkerType::Async { .. }) } } /// Configuration error types #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("IO error: {0}")] IoError(String), #[error("Parse error: {0}")] ParseError(String), #[error("Validation error: {0}")] ValidationError(String), } // Default value functions for serde fn default_preserve_tasks() -> bool { false } fn default_timeout_seconds() -> u64 { 300 // 5 minutes } fn default_timestamps() -> bool { true } fn default_log_level() -> String { "info".to_string() } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; #[test] fn test_sync_worker_config() { let config_toml = r#" worker_id = "sync_worker_1" redis_url = "redis://localhost:6379" db_path = "/tmp/worker_db" [worker_type] type = "sync" [logging] timestamps = false level = "debug" "#; let config: WorkerConfig = toml::from_str(config_toml).unwrap(); assert_eq!(config.worker_id, "sync_worker_1"); assert!(config.is_sync()); assert!(!config.is_async()); assert_eq!(config.get_default_timeout(), None); assert!(!config.logging.timestamps); assert_eq!(config.logging.level, "debug"); } #[test] fn test_async_worker_config() { let config_toml = r#" worker_id = "async_worker_1" redis_url = "redis://localhost:6379" db_path = "/tmp/worker_db" [worker_type] type = "async" default_timeout_seconds = 600 [logging] timestamps = true level = "info" "#; let config: WorkerConfig = toml::from_str(config_toml).unwrap(); assert_eq!(config.worker_id, "async_worker_1"); assert!(!config.is_sync()); assert!(config.is_async()); assert_eq!(config.get_default_timeout(), Some(Duration::from_secs(600))); assert!(config.logging.timestamps); assert_eq!(config.logging.level, "info"); } #[test] fn test_config_from_file() { let config_toml = r#" worker_id = "test_worker" redis_url = "redis://localhost:6379" db_path = "/tmp/test_db" [worker_type] type = "sync" "#; let mut temp_file = NamedTempFile::new().unwrap(); temp_file.write_all(config_toml.as_bytes()).unwrap(); let config = WorkerConfig::from_file(temp_file.path()).unwrap(); assert_eq!(config.worker_id, "test_worker"); assert!(config.is_sync()); } #[test] fn test_config_validation() { let config_toml = r#" worker_id = "" redis_url = "redis://localhost:6379" db_path = "/tmp/test_db" [worker_type] type = "sync" "#; let result: Result = toml::from_str(config_toml); assert!(result.is_ok()); let config = result.unwrap(); assert!(config.validate().is_err()); } }