kegani 0.1.1

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Settings macro and utilities for configuration
//!
//! Provides configuration loading from environment variables and YAML files.

use std::env;
use serde::de::DeserializeOwned;

/// Configuration error
#[derive(Debug)]
pub enum ConfigError {
    Io(std::io::Error),
    Parse(serde_yaml::Error),
    Env(env::VarError),
    Missing(String),
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO error: {}", e),
            ConfigError::Parse(e) => write!(f, "Parse error: {}", e),
            ConfigError::Env(e) => write!(f, "Environment error: {}", e),
            ConfigError::Missing(key) => write!(f, "Missing required config: {}", key),
        }
    }
}

impl std::error::Error for ConfigError {}

impl From<std::io::Error> for ConfigError {
    fn from(e: std::io::Error) -> Self {
        ConfigError::Io(e)
    }
}

impl From<serde_yaml::Error> for ConfigError {
    fn from(e: serde_yaml::Error) -> Self {
        ConfigError::Parse(e)
    }
}

impl From<env::VarError> for ConfigError {
    fn from(e: env::VarError) -> Self {
        ConfigError::Env(e)
    }
}

/// Application configuration
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AppConfig {
    #[serde(default = "default_host")]
    pub host: String,

    #[serde(default = "default_port")]
    pub port: u16,

    #[serde(default)]
    pub database: DatabaseConfig,

    #[serde(default)]
    pub redis: RedisConfig,

    #[serde(default)]
    pub logging: LoggingConfig,

    #[serde(default)]
    pub cors: CorsConfig,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            host: default_host(),
            port: default_port(),
            database: DatabaseConfig::default(),
            redis: RedisConfig::default(),
            logging: LoggingConfig::default(),
            cors: CorsConfig::default(),
        }
    }
}

fn default_host() -> String {
    "0.0.0.0".to_string()
}

fn default_port() -> u16 {
    8080
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct DatabaseConfig {
    #[serde(default = "default_database_url")]
    pub url: String,

    #[serde(default = "default_max_connections")]
    pub max_connections: u32,

    #[serde(default = "default_min_connections")]
    pub min_connections: u32,
}

impl Default for DatabaseConfig {
    fn default() -> Self {
        Self {
            url: std::env::var("DATABASE_URL")
                .unwrap_or_else(|_| "postgres://localhost:5432/kegani".to_string()),
            max_connections: 10,
            min_connections: 2,
        }
    }
}

fn default_database_url() -> String {
    "postgres://localhost:5432/kegani".to_string()
}

fn default_max_connections() -> u32 {
    10
}

fn default_min_connections() -> u32 {
    2
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct RedisConfig {
    #[serde(default = "default_redis_url")]
    pub url: String,

    #[serde(default = "default_pool_size")]
    pub pool_size: u32,
}

impl Default for RedisConfig {
    fn default() -> Self {
        Self {
            url: std::env::var("REDIS_URL")
                .unwrap_or_else(|_| "redis://localhost:6379".to_string()),
            pool_size: 5,
        }
    }
}

fn default_redis_url() -> String {
    "redis://localhost:6379".to_string()
}

fn default_pool_size() -> u32 {
    5
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct LoggingConfig {
    #[serde(default = "default_log_level")]
    pub level: String,

    #[serde(default = "default_log_format")]
    pub format: String,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            level: std::env::var("LOG_LEVEL")
                .unwrap_or_else(|_| "info".to_string()),
            format: "json".to_string(),
        }
    }
}

fn default_log_level() -> String {
    "info".to_string()
}

fn default_log_format() -> String {
    "json".to_string()
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct CorsConfig {
    #[serde(default = "default_allowed_origins")]
    pub allowed_origins: Vec<String>,

    #[serde(default = "default_allowed_methods")]
    pub allowed_methods: Vec<String>,

    #[serde(default = "default_max_age")]
    pub max_age: u64,
}

impl Default for CorsConfig {
    fn default() -> Self {
        Self {
            allowed_origins: vec!["*".to_string()],
            allowed_methods: vec![
                "GET".to_string(),
                "POST".to_string(),
                "PUT".to_string(),
                "DELETE".to_string(),
                "PATCH".to_string(),
            ],
            max_age: 3600,
        }
    }
}

fn default_allowed_origins() -> Vec<String> {
    vec!["*".to_string()]
}

fn default_allowed_methods() -> Vec<String> {
    vec![
        "GET".to_string(),
        "POST".to_string(),
        "PUT".to_string(),
        "DELETE".to_string(),
    ]
}

fn default_max_age() -> u64 {
    3600
}

/// Settings trait for configuration loading
pub trait Settings: DeserializeOwned + Default {
    fn load() -> Result<Self, ConfigError>
    where
        Self: Sized;

    fn load_from_files(paths: Vec<&str>) -> Result<Self, ConfigError>
    where
        Self: Sized;
}

impl Settings for AppConfig {
    fn load() -> Result<Self, ConfigError> {
        Self::load_from_files(vec!["config.yaml", "config.yml"])
    }

    fn load_from_files(paths: Vec<&str>) -> Result<Self, ConfigError> {
        let mut config = Self::default();

        // Try to load from YAML files
        for path in paths {
            if let Ok(contents) = std::fs::read_to_string(path) {
                config = serde_yaml::from_str(&contents)?;
                break;
            }
        }

        // Override with environment variables
        if let Ok(host) = env::var("APP_HOST") {
            config.host = host;
        }
        if let Ok(port) = env::var("APP_PORT") {
            if let Ok(p) = port.parse() {
                config.port = p;
            }
        }
        if let Ok(url) = env::var("DATABASE_URL") {
            config.database.url = url;
        }
        if let Ok(url) = env::var("REDIS_URL") {
            config.redis.url = url;
        }
        if let Ok(level) = env::var("LOG_LEVEL") {
            config.logging.level = level;
        }

        Ok(config)
    }
}

/// Extension trait for configuration with hot reload support
pub trait SettingsExt: Settings + Sized {
    /// Reload configuration from files and environment
    fn reload(&mut self) -> Result<(), ConfigError>;

    /// Load configuration with a specific file
    fn load_file(path: &str) -> Result<Self, ConfigError>;
}

impl SettingsExt for AppConfig {
    fn reload(&mut self) -> Result<(), ConfigError> {
        let new_config = Self::load()?;
        *self = new_config;
        Ok(())
    }

    fn load_file(path: &str) -> Result<Self, ConfigError> {
        let mut config = Self::default();
        if let Ok(contents) = std::fs::read_to_string(path) {
            config = serde_yaml::from_str(&contents)?;
        }
        Ok(config)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = AppConfig::default();
        assert_eq!(config.host, "0.0.0.0");
        assert_eq!(config.port, 8080);
    }
}