guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
//! Core configuration module for Guardy
//!
//! This module provides the central configuration system with:
//! - Zero-cost static configuration via LazyLock
//! - CLI override support baked into defaults
//! - Direct field access for maximum performance

use std::sync::{Arc, LazyLock};

use serde::{Deserialize, Serialize};

use crate::{
    cli::commands::Commands,
    config::{
        config::ConfigConfig, general::GeneralConfig, hooks::HooksConfig, scanner::ScannerConfig,
        status::StatusConfig, sync::SyncConfig,
    },
};

/// Global configuration - initialized once, shared everywhere
pub static CONFIG: LazyLock<Arc<GuardyConfig>> = LazyLock::new(|| {
    // Default::default() handles CLI overrides internally
    Arc::new(GuardyConfig::default())
});

/// Main Guardy configuration with Arc-wrapped sub-configs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardyConfig {
    pub general: Arc<GeneralConfig>,
    pub scanner: Arc<ScannerConfig>,
    pub hooks: Arc<HooksConfig>,
    pub sync: Arc<SyncConfig>,
    pub status: Arc<StatusConfig>,
    pub config: Arc<ConfigConfig>,
    pub mcp: Arc<McpConfig>,
    pub external_tools: Arc<ExternalToolsConfig>,
}

// GeneralConfig is now generated by macro in general.rs

// HooksConfig is now generated by macro in hooks.rs

// Old nested GitHooksConfig, HookConfig, and CustomCommand removed
// New structure uses flat lefthook-style configuration in hooks.rs

// ScannerConfig is now generated by macro in scanner.rs

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
    pub port: u16,
    pub host: String,
    pub enabled: bool,
    pub tools: Arc<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalToolsConfig {
    pub git_crypt: String,
    pub nx: String,
    pub pnpm: String,
}

// SyncConfig, RepoConfig, and SyncProtectionConfig are now generated by macro in sync.rs

impl Default for GuardyConfig {
    fn default() -> Self {
        // Get CLI args if available
        let cli = crate::cli::CLI.get();

        // Extract command-specific args efficiently
        let (scan_args, hooks_args, sync_args, status_args, config_args) = if let Some(cli) = cli {
            let scan_args = if let Some(Commands::Scan(args)) = &cli.command {
                Some(args)
            } else {
                None
            };
            let hooks_args = if let Some(Commands::Hooks(args)) = &cli.command {
                Some(args)
            } else {
                None
            };
            let sync_args = if let Some(Commands::Sync(args)) = &cli.command {
                Some(args)
            } else {
                None
            };
            let status_args = if let Some(Commands::Status(args)) = &cli.command {
                Some(args)
            } else {
                None
            };
            let config_args = if let Some(Commands::Config(args)) = &cli.command {
                Some(args)
            } else {
                None
            };
            (scan_args, hooks_args, sync_args, status_args, config_args)
        } else {
            (None, None, None, None, None)
        };

        GuardyConfig {
            general: Arc::new(crate::config::general::GeneralConfig::build(cli)),
            scanner: Arc::new(crate::config::scanner::ScannerConfig::build(scan_args)),
            hooks: Arc::new(crate::config::hooks::HooksConfig::build(hooks_args)),
            sync: Arc::new(crate::config::sync::SyncConfig::build(sync_args)),
            status: Arc::new(crate::config::status::StatusConfig::build(status_args)),
            config: Arc::new(crate::config::config::ConfigConfig::build(config_args)),

            mcp: Arc::new(McpConfig {
                port: 8080,
                host: "127.0.0.1".into(),
                enabled: false,
                tools: Arc::new(vec![
                    "git-status".into(),
                    "hook-run".into(),
                    "security-scan".into(),
                ]),
            }),

            external_tools: Arc::new(ExternalToolsConfig {
                git_crypt: "git-crypt".into(),
                nx: "nx".into(),
                pnpm: "pnpm".into(),
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scan::types::ScanMode;

    #[test]
    fn test_default_config_loads() {
        let config = GuardyConfig::default();
        assert_eq!(config.scanner.mode, ScanMode::Auto);
        assert_eq!(config.scanner.thread_percentage, 70);
        assert!(!config.hooks.skip_all);
    }

    #[test]
    fn test_arc_clone_is_cheap() {
        let config = GuardyConfig::default();

        // Test that Arc sub-configs share data when cloned
        let original_scanner_ptr = std::sync::Arc::as_ptr(&config.scanner);
        let clone = config.clone();
        let clone_scanner_ptr = std::sync::Arc::as_ptr(&clone.scanner);

        // Arc clones should point to the same data
        assert_eq!(
            original_scanner_ptr, clone_scanner_ptr,
            "Arc clone should share the same data"
        );

        // Test that we can create many clones without issues
        let mut clones = Vec::new();
        for _ in 0..100 {
            clones.push(config.clone());
        }
        assert_eq!(clones.len(), 100);

        // All clones should share the same scanner config data
        for clone in &clones {
            assert_eq!(std::sync::Arc::as_ptr(&clone.scanner), original_scanner_ptr);
        }
    }

    #[test]
    fn test_global_config_loads() {
        let config = CONFIG.clone();
        assert_eq!(config.scanner.mode, ScanMode::Auto);
        assert!(!config.hooks.skip_all);
    }

    #[test]
    fn test_config_is_singleton() {
        let config1 = CONFIG.clone();
        let config2 = CONFIG.clone();
        // Should be the same Arc instance
        assert!(Arc::ptr_eq(&config1, &config2));
    }
}