cargo-hammerwork 1.6.0

A comprehensive cargo subcommand for managing Hammerwork job queues with webhook management, event streaming, database operations, and advanced monitoring capabilities
Documentation
use anyhow::Result;
use clap::Subcommand;
use tracing::info;

use crate::config::Config;

#[derive(Subcommand)]
pub enum ConfigCommand {
    #[command(about = "Show current configuration")]
    Show,
    #[command(about = "Set a configuration value")]
    Set {
        #[arg(help = "Configuration key")]
        key: String,
        #[arg(help = "Configuration value")]
        value: String,
    },
    #[command(about = "Get a configuration value")]
    Get {
        #[arg(help = "Configuration key")]
        key: String,
    },
    #[command(about = "Reset configuration to defaults")]
    Reset {
        #[arg(long, help = "Confirm the reset operation")]
        confirm: bool,
    },
    #[command(about = "Show configuration file path")]
    Path,
}

impl ConfigCommand {
    pub async fn execute(&self, config: &mut Config) -> Result<()> {
        match self {
            ConfigCommand::Show => {
                show_config(config).await?;
            }
            ConfigCommand::Set { key, value } => {
                set_config_value(config, key, value).await?;
            }
            ConfigCommand::Get { key } => {
                get_config_value(config, key).await?;
            }
            ConfigCommand::Reset { confirm } => {
                reset_config(config, *confirm).await?;
            }
            ConfigCommand::Path => {
                show_config_path().await?;
            }
        }
        Ok(())
    }
}

async fn show_config(config: &Config) -> Result<()> {
    println!("⚙️  Hammerwork Configuration");
    println!("═══════════════════════════");

    let mut table = comfy_table::Table::new();
    table.set_header(vec!["Setting", "Value", "Source"]);

    // Database URL
    let (db_url, db_source) = if let Some(url) = &config.database_url {
        (url.clone(), "Config File")
    } else if let Ok(env_url) = std::env::var("DATABASE_URL") {
        (env_url, "Environment")
    } else {
        ("Not set".to_string(), "Default")
    };
    table.add_row(vec!["database_url", &db_url, db_source]);

    // Default queue
    let (queue, queue_source) = if let Some(q) = &config.default_queue {
        (q.clone(), "Config File")
    } else if let Ok(env_queue) = std::env::var("HAMMERWORK_DEFAULT_QUEUE") {
        (env_queue, "Environment")
    } else {
        ("Not set".to_string(), "Default")
    };
    table.add_row(vec!["default_queue", &queue, queue_source]);

    // Default limit
    let limit = config.get_default_limit().to_string();
    table.add_row(vec!["default_limit", &limit, "Config File"]);

    // Log level
    let log_level = config.get_log_level();
    table.add_row(vec!["log_level", log_level, "Config File"]);

    // Connection pool size
    let pool_size = config.get_connection_pool_size().to_string();
    table.add_row(vec!["connection_pool_size", &pool_size, "Config File"]);

    println!("{}", table);

    println!("\n💡 Configuration priority: Environment Variables > Config File > Defaults");
    println!("📝 Use 'config set <key> <value>' to update configuration");

    Ok(())
}

async fn set_config_value(config: &mut Config, key: &str, value: &str) -> Result<()> {
    match key {
        "database_url" => {
            crate::utils::validation::validate_database_url(value)?;
            config.database_url = Some(value.to_string());
            info!("✅ Set database_url");
        }
        "default_queue" => {
            config.default_queue = Some(value.to_string());
            info!("✅ Set default_queue to: {}", value);
        }
        "default_limit" => {
            let limit: u32 = value
                .parse()
                .map_err(|_| anyhow::anyhow!("default_limit must be a positive integer"))?;
            config.default_limit = Some(limit);
            info!("✅ Set default_limit to: {}", limit);
        }
        "log_level" => {
            if !["trace", "debug", "info", "warn", "error"].contains(&value.to_lowercase().as_str())
            {
                return Err(anyhow::anyhow!(
                    "log_level must be one of: trace, debug, info, warn, error"
                ));
            }
            config.log_level = Some(value.to_lowercase());
            info!("✅ Set log_level to: {}", value);
        }
        "connection_pool_size" => {
            let size: u32 = value
                .parse()
                .map_err(|_| anyhow::anyhow!("connection_pool_size must be a positive integer"))?;
            if size == 0 || size > 100 {
                return Err(anyhow::anyhow!(
                    "connection_pool_size must be between 1 and 100"
                ));
            }
            config.connection_pool_size = Some(size);
            info!("✅ Set connection_pool_size to: {}", size);
        }
        _ => {
            return Err(anyhow::anyhow!(
                "Unknown configuration key: {}. Valid keys: database_url, default_queue, default_limit, log_level, connection_pool_size",
                key
            ));
        }
    }

    // Save the updated configuration
    config.save()?;
    println!("💾 Configuration saved");

    Ok(())
}

async fn get_config_value(config: &Config, key: &str) -> Result<()> {
    let value = match key {
        "database_url" => config.get_database_url().unwrap_or("Not set").to_string(),
        "default_queue" => config.get_default_queue().unwrap_or("Not set").to_string(),
        "default_limit" => config.get_default_limit().to_string(),
        "log_level" => config.get_log_level().to_string(),
        "connection_pool_size" => config.get_connection_pool_size().to_string(),
        _ => {
            return Err(anyhow::anyhow!(
                "Unknown configuration key: {}. Valid keys: database_url, default_queue, default_limit, log_level, connection_pool_size",
                key
            ));
        }
    };

    println!("{}", value);
    Ok(())
}

async fn reset_config(config: &mut Config, confirm: bool) -> Result<()> {
    if !confirm {
        println!("⚠️  This will reset all configuration to defaults. Use --confirm to proceed.");
        return Ok(());
    }

    *config = Config::default();
    config.save()?;

    println!("🔄 Configuration reset to defaults");
    info!("Configuration has been reset to defaults");

    Ok(())
}

async fn show_config_path() -> Result<()> {
    let config_dir = dirs::config_dir()
        .or_else(dirs::home_dir)
        .ok_or_else(|| anyhow::anyhow!("Cannot find config directory"))?;

    let config_path = config_dir.join("hammerwork").join("config.toml");

    println!("📁 Configuration file path:");
    println!("{}", config_path.display());

    if config_path.exists() {
        println!("✅ File exists");

        // Show file size and modification time
        let metadata = std::fs::metadata(&config_path)?;
        let size = metadata.len();
        let modified = metadata.modified()?;
        let modified_time = chrono::DateTime::<chrono::Utc>::from(modified);

        println!("📊 Size: {} bytes", size);
        println!(
            "📅 Last modified: {}",
            modified_time.format("%Y-%m-%d %H:%M:%S UTC")
        );
    } else {
        println!("❌ File does not exist (will be created when configuration is saved)");
    }

    Ok(())
}

#[cfg(test)]
mod tests {

    #[test]
    fn test_config_command_structure() {
        let commands = vec!["show", "set", "get", "reset", "path"];

        for cmd in commands {
            assert!(!cmd.is_empty());
            assert!(cmd.chars().all(|c| c.is_ascii_lowercase()));
        }
    }

    #[test]
    fn test_config_key_validation() {
        let valid_keys = vec![
            "database_url",
            "default_queue",
            "default_limit",
            "log_level",
            "connection_pool_size",
        ];

        for key in valid_keys {
            assert!(is_valid_config_key(key));
        }

        let invalid_keys = vec!["", "invalid_key", "key with spaces", "key/with/slashes"];

        for key in invalid_keys {
            assert!(!is_valid_config_key(key));
        }
    }

    #[test]
    fn test_config_value_validation() {
        // Test database URL values
        assert!(is_valid_config_value(
            "database_url",
            "postgres://localhost/db"
        ));
        assert!(!is_valid_config_value("database_url", "invalid_url"));

        // Test log level values
        assert!(is_valid_config_value("log_level", "info"));
        assert!(is_valid_config_value("log_level", "debug"));
        assert!(!is_valid_config_value("log_level", "invalid"));

        // Test numeric values
        assert!(is_valid_config_value("default_limit", "100"));
        assert!(is_valid_config_value("connection_pool_size", "5"));
        assert!(!is_valid_config_value("default_limit", "not_a_number"));
    }

    fn is_valid_config_key(key: &str) -> bool {
        matches!(
            key,
            "database_url"
                | "default_queue"
                | "default_limit"
                | "log_level"
                | "connection_pool_size"
        )
    }

    fn is_valid_config_value(key: &str, value: &str) -> bool {
        match key {
            "database_url" => {
                !value.is_empty()
                    && (value.starts_with("postgres://") || value.starts_with("mysql://"))
            }
            "log_level" => matches!(value, "error" | "warn" | "info" | "debug" | "trace"),
            "default_limit" | "connection_pool_size" => value.parse::<u32>().is_ok(),
            "default_queue" => !value.is_empty() && !value.contains(' '),
            _ => false,
        }
    }
}