checkmate-cli 0.4.1

Checkmate - API Testing Framework CLI
//! Config subcommand - Configuration management
//!
//! Configuration hierarchy (lowest to highest priority):
//! 1. Built-in defaults
//! 2. User-level config (~/.config/checkmate/config.toml)
//! 3. Project-level config (.checkmate/config.toml)
//! 4. Environment variables (CM_*)

use clap::Subcommand;
use figment::{
    providers::{Env, Format, Serialized, Toml},
    Figment,
};
use serde::{Deserialize, Serialize};

use crate::project::CheckmateProject;

/// Main configuration structure
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
    #[serde(default)]
    pub env: EnvConfig,
    #[serde(default)]
    pub defaults: DefaultsConfig,
}

/// Environment/connection settings
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct EnvConfig {
    pub base_url: Option<String>,
    #[serde(default = "default_timeout")]
    pub timeout_ms: u64,
}

impl Default for EnvConfig {
    fn default() -> Self {
        Self {
            base_url: None,
            timeout_ms: default_timeout(),
        }
    }
}

/// Test execution defaults
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DefaultsConfig {
    #[serde(default)]
    pub fail_fast: bool,
    #[serde(default = "default_status")]
    pub expect_status: u16,
}

fn default_timeout() -> u64 {
    5000
}

fn default_status() -> u16 {
    200
}

impl Default for Config {
    fn default() -> Self {
        Self {
            env: EnvConfig::default(),
            defaults: DefaultsConfig::default(),
        }
    }
}

impl Default for DefaultsConfig {
    fn default() -> Self {
        Self {
            fail_fast: false,
            expect_status: default_status(),
        }
    }
}

impl Config {
    /// Load configuration from all sources (defaults < user < project < env)
    pub fn load(project: Option<&CheckmateProject>) -> Self {
        let mut figment = Figment::new().merge(Serialized::defaults(Config::default()));

        // User-level config (~/.config/checkmate/config.toml)
        if let Some(config_dir) = directories::ProjectDirs::from("", "", "checkmate") {
            let user_config = config_dir.config_dir().join("config.toml");
            if user_config.exists() {
                figment = figment.merge(Toml::file(user_config));
            }
        }

        // Project-level config (.checkmate/config.toml)
        if let Some(project) = project {
            if project.config_path.exists() {
                figment = figment.merge(Toml::file(&project.config_path));
            }
        }

        // Environment variables: CM_ENV__BASE_URL, CM_DEFAULTS__FAIL_FAST, etc.
        // (double underscore for nested keys)
        figment = figment.merge(Env::prefixed("CM_").split("__"));

        figment.extract().unwrap_or_default()
    }

    /// Load config, auto-discovering project
    pub fn load_auto() -> Self {
        let project = CheckmateProject::discover();
        Self::load(project.as_ref())
    }
}

#[derive(Subcommand)]
pub enum ConfigCommands {
    /// Show current configuration
    Show {
        /// Show as JSON instead of TOML
        #[arg(long)]
        json: bool,
    },
    /// Show configuration sources
    Sources,
}

pub fn run(command: ConfigCommands) -> Result<(), Box<dyn std::error::Error>> {
    match command {
        ConfigCommands::Show { json } => show_config(json),
        ConfigCommands::Sources => show_sources(),
    }
}

fn show_config(as_json: bool) -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::load_auto();

    if as_json {
        println!("{}", serde_json::to_string_pretty(&config)?);
    } else {
        println!("{}", toml::to_string_pretty(&config)?);
    }

    Ok(())
}

fn show_sources() -> Result<(), Box<dyn std::error::Error>> {
    println!("Configuration sources (in priority order):\n");

    // User-level config
    if let Some(config_dir) = directories::ProjectDirs::from("", "", "checkmate") {
        let user_config = config_dir.config_dir().join("config.toml");
        if user_config.exists() {
            println!("  [✓] User:    {}", user_config.display());
        } else {
            println!("  [ ] User:    {} (not found)", user_config.display());
        }
    }

    // Project-level config
    if let Some(project) = CheckmateProject::discover() {
        if project.config_path.exists() {
            println!("  [✓] Project: {}", project.config_path.display());
        } else {
            println!("  [ ] Project: {} (not found)", project.config_path.display());
        }
    } else {
        println!("  [ ] Project: (no .checkmate/ found)");
    }

    println!("\n  Environment variables: CM_* (e.g., CM_ENV__BASE_URL, CM_DEFAULTS__FAIL_FAST)");

    Ok(())
}