use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { /// Server host address #[serde(default = "default_host")] pub host: String, /// Server port #[serde(default = "default_port")] pub port: u16, /// Redis connection URL #[serde(default = "default_redis_url")] pub redis_url: String, /// Enable authentication #[serde(default)] pub auth: bool, /// Enable TLS/WSS #[serde(default)] pub tls: bool, /// Path to TLS certificate file pub cert: Option, /// Path to TLS private key file pub key: Option, /// Separate port for TLS connections pub tls_port: Option, /// Enable webhook handling #[serde(default)] pub webhooks: bool, /// Circles configuration - maps circle names to lists of member public keys #[serde(default)] pub circles: HashMap>, } impl Default for ServerConfig { fn default() -> Self { Self { host: default_host(), port: default_port(), redis_url: default_redis_url(), auth: false, tls: false, cert: None, key: None, tls_port: None, webhooks: false, circles: HashMap::new(), } } } impl ServerConfig { /// Load configuration from a JSON file pub fn from_file>(path: P) -> Result { let content = fs::read_to_string(path.as_ref()) .map_err(|e| ConfigError::FileRead(path.as_ref().to_path_buf(), e))?; let config: ServerConfig = serde_json::from_str(&content) .map_err(|e| ConfigError::JsonParse(e))?; config.validate()?; Ok(config) } /// Save configuration to a JSON file pub fn to_file>(&self, path: P) -> Result<(), ConfigError> { let content = serde_json::to_string_pretty(self) .map_err(|e| ConfigError::JsonSerialize(e))?; fs::write(path.as_ref(), content) .map_err(|e| ConfigError::FileWrite(path.as_ref().to_path_buf(), e))?; Ok(()) } /// Validate the configuration pub fn validate(&self) -> Result<(), ConfigError> { // Validate TLS configuration if self.tls && (self.cert.is_none() || self.key.is_none()) { return Err(ConfigError::InvalidTlsConfig( "TLS is enabled but certificate or key path is missing".to_string() )); } // Validate that circles are not empty if auth is enabled if self.auth && self.circles.is_empty() { return Err(ConfigError::InvalidAuthConfig( "Authentication is enabled but no circles are configured".to_string() )); } Ok(()) } /// Create a sample configuration file pub fn create_sample() -> Self { let mut circles = HashMap::new(); circles.insert( "example_circle".to_string(), vec![ "0x1234567890abcdef1234567890abcdef12345678".to_string(), "0xabcdef1234567890abcdef1234567890abcdef12".to_string(), ] ); Self { host: "127.0.0.1".to_string(), port: 8443, redis_url: "redis://127.0.0.1/".to_string(), auth: true, tls: false, cert: Some("cert.pem".to_string()), key: Some("key.pem".to_string()), tls_port: Some(8444), webhooks: false, circles, } } } #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("Failed to read config file {0}: {1}")] FileRead(std::path::PathBuf, std::io::Error), #[error("Failed to write config file {0}: {1}")] FileWrite(std::path::PathBuf, std::io::Error), #[error("Failed to parse JSON config: {0}")] JsonParse(serde_json::Error), #[error("Failed to serialize JSON config: {0}")] JsonSerialize(serde_json::Error), #[error("Invalid TLS configuration: {0}")] InvalidTlsConfig(String), #[error("Invalid authentication configuration: {0}")] InvalidAuthConfig(String), } // Default value functions fn default_host() -> String { "127.0.0.1".to_string() } fn default_port() -> u16 { 8443 } fn default_redis_url() -> String { "redis://127.0.0.1/".to_string() } #[cfg(test)] mod tests { use super::*; use tempfile::NamedTempFile; #[test] fn test_config_serialization() { let config = ServerConfig::create_sample(); let json = serde_json::to_string_pretty(&config).unwrap(); let deserialized: ServerConfig = serde_json::from_str(&json).unwrap(); assert_eq!(config.host, deserialized.host); assert_eq!(config.port, deserialized.port); assert_eq!(config.circles.len(), deserialized.circles.len()); } #[test] fn test_config_file_operations() { let config = ServerConfig::create_sample(); let temp_file = NamedTempFile::new().unwrap(); // Test writing config.to_file(temp_file.path()).unwrap(); // Test reading let loaded_config = ServerConfig::from_file(temp_file.path()).unwrap(); assert_eq!(config.host, loaded_config.host); assert_eq!(config.circles.len(), loaded_config.circles.len()); } #[test] fn test_config_validation() { let mut config = ServerConfig::default(); // Valid config should pass assert!(config.validate().is_ok()); // TLS enabled without cert/key should fail config.tls = true; assert!(config.validate().is_err()); // Fix TLS config config.cert = Some("cert.pem".to_string()); config.key = Some("key.pem".to_string()); assert!(config.validate().is_ok()); // Auth enabled without circles should fail config.auth = true; assert!(config.validate().is_err()); // Add circles config.circles.insert("test".to_string(), vec!["pubkey".to_string()]); assert!(config.validate().is_ok()); } }