guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use std::env;
// All tracing macros are used with full paths to avoid import issues in macro context

/// Generic helper to get values from JSON objects with dash/underscore flexibility
/// Tries underscore version first, then falls back to dash version
/// This allows configuration files to use either "pre_commit" or "pre-commit" format
#[inline]
pub fn get_flexible_key<T>(object: &serde_json::Value, key: &str) -> Option<T>
where
    T: serde::de::DeserializeOwned + Clone,
{
    // Try underscore version first (matches Rust field names)
    if let Some(value) = object.get(key) {
        match serde_json::from_value::<T>(value.clone()) {
            Ok(parsed) => return Some(parsed),
            Err(e) => {
                tracing::trace!("Failed to parse {} (underscore): {}", key, e);
            }
        }
    }

    // Try dash version as fallback (user-friendly YAML/JSON keys)
    let dash_key = key.replace('_', "-");
    if dash_key != key {
        // Only try if different
        if let Some(value) = object.get(&dash_key) {
            match serde_json::from_value::<T>(value.clone()) {
                Ok(parsed) => return Some(parsed),
                Err(e) => {
                    tracing::trace!("Failed to parse {} (dash '{}'): {}", key, dash_key, e);
                }
            }
        }
    }

    None
}

/// Get raw environment variable value as String
#[inline]
pub fn get_env_string(env_var: &str) -> Option<String> {
    env::var(env_var).ok()
}

/// Parse comma-separated environment variable as Vec<String>
#[inline]
pub fn parse_env_vec(env_var: &str) -> Option<Vec<String>> {
    env::var(env_var).ok().map(|value| {
        if value.is_empty() {
            Vec::new()
        } else {
            value.split(',').map(|s| s.trim().to_string()).collect()
        }
    })
}

/// High-performance generic parser with minimal allocations
/// Enhanced to handle boolean values like "1"/"0" in addition to "true"/"false"
#[inline]
pub fn parse_env<T: 'static + std::str::FromStr>(env_var: &str) -> Option<T> {
    let value = env::var(env_var).ok()?;
    let type_name = std::any::type_name::<T>();
    tracing::debug!("Found env var {} (type: {}): {}", env_var, type_name, value);

    // Special handling for boolean types to support truthy/falsy values
    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() {
        let bool_result = match value.as_str() {
            "1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON" => Some(true),
            "0" | "false" | "FALSE" | "no" | "NO" | "off" | "OFF" => Some(false),
            _ => None,
        };

        if let Some(bool_val) = bool_result {
            tracing::debug!("Parsed {} as boolean: {}", env_var, bool_val);
            // Safe transmute because we've verified T is bool
            return unsafe { std::mem::transmute_copy(&bool_val) };
        }
    }

    value.parse().ok()
}

/// Optimized configuration struct generator macro
///
/// Features:
/// - Selective environment variable loading (only checks declared vars)
/// - Zero-cost abstractions (everything inlined at compile time)
/// - Memory efficient (minimal allocations, stack-based structures)
/// - Type-safe boolean parsing with truthy/falsy support
/// - Optional source specification (env, cli, file, default)
/// - Clear precedence: CLI > Env > File > Default
///
/// Usage:
/// ```rust,ignore
/// config_build! {
///     MyConfig<CliArgs> {
///         field_name: FieldType => {
///             env: "ENV_VAR_NAME",           // Optional
///             default: default_value,        // Required
///             cli: |args| args.field,        // Optional
///             file: "config.field.path",     // Optional (future)
///         },
///     }
/// }
/// ```
#[macro_export]
macro_rules! config_build {
    (
        $struct_name:ident<$cli_type:ty> {
            $(
                $field:ident: $field_type:ty => {
                    $(cli: $cli_expr:expr,)?
                    $(env: $env_var:expr,)?
                    default: $default:expr,
                    $(file: $file_path:expr,)?
                }
            ),* $(,)?
        }
    ) => {
        // Generate the main configuration struct
        #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
        #[serde(rename_all = "kebab-case")]
        pub struct $struct_name {
            $(pub $field: $field_type,)*
        }

        // Generate a lightweight environment variables holder
        // Uses Option<T> to avoid allocations for missing values
        #[derive(Debug, Default)]
        struct EnvVars {
            $(
                $field: Option<$field_type>,
            )*
        }

        impl $struct_name {
            /// Load environment variables selectively and efficiently
            /// Only checks environment variables that were explicitly declared
            #[tracing::instrument(level = "debug", skip_all)]
            fn load_env_vars() -> EnvVars {
                let start = std::time::Instant::now();
                let mut env = EnvVars::default();
                let mut loaded_count = 0;

                // Initialize all fields first
                $(
                    env.$field = None;
                )*

                // Then load only fields with env vars
                $(
                    $(
                        // Only generate env loading code for fields with env declared
                        env.$field = config_build!(@parse_env_value $field, $field_type, $env_var);
                        if env.$field.is_some() {
                            loaded_count += 1;
                        }
                    )?
                )*

                let duration = start.elapsed();
                tracing::debug!("⚡ Loaded {} env vars in {:?}", loaded_count, duration);
                env
            }

            /// Build configuration from all available sources with optimal precedence
            /// CLI args > Environment variables > File config > Defaults
            pub fn build(
                cli_args: Option<&$cli_type>,
            ) -> Self {
                // Load environment variables once (selective, fast)
                let env = Self::load_env_vars();

                // Get our file section once (optimal caching)
                let section_name = stringify!($struct_name)
                    .to_lowercase()
                    .replace("config", "");
                let file_section = &$crate::config::files::MERGED_FILE_CONFIG
                    .get(&section_name)
                    .unwrap_or(&serde_json::Value::Null);

                // Trace-level debug output for config sections
                if tracing::enabled!(tracing::Level::TRACE) {
                    if let Ok(pretty) = serde_json::to_string_pretty(file_section) {
                        tracing::trace!("{} section from file: {}", section_name, pretty);
                    }
                }

                let config = Self {
                    $(
                        $field: config_build!(@resolve_value
                            $field,
                            $field_type,
                            cli_args,
                            env.$field,
                            $crate::config::macros::get_flexible_key::<$field_type>(
                                file_section,
                                stringify!($field)
                            ),
                            $default
                            $(, $cli_expr)?
                        ),
                    )*
                };

                // Log the final configuration for debugging
                tracing::debug!("{} config check: {:?}", section_name, config);

                config
            }
        }
    };

    // Internal helper: Parse environment values with type-specific optimizations
    // Test if we can match on the field name instead of type
    (@parse_env_value repo, $type:ty, $env_var:expr) => {
        $crate::config::macros::get_env_string($env_var).map(Some)
    };
    (@parse_env_value version, $type:ty, $env_var:expr) => {
        $crate::config::macros::get_env_string($env_var).map(Some)
    };
    (@parse_env_value source_path, $type:ty, $env_var:expr) => {
        $crate::config::macros::get_env_string($env_var).map(Some)
    };
    (@parse_env_value dest_path, $type:ty, $env_var:expr) => {
        $crate::config::macros::get_env_string($env_var).map(Some)
    };
    (@parse_env_value include, $type:ty, $env_var:expr) => {
        $crate::config::macros::parse_env_vec($env_var)
    };
    (@parse_env_value exclude, $type:ty, $env_var:expr) => {
        $crate::config::macros::parse_env_vec($env_var)
    };
    (@parse_env_value $field:ident, $type:ty, $env_var:expr) => {
        $crate::config::macros::parse_env::<$type>($env_var)
    };

    // Internal helper: Resolve final value with precedence using serde for type unification
    (@resolve_value $field:ident, $field_type:ty, $cli_args:expr, $env_value:expr, $file_value:expr, $default:expr, $cli_expr:expr) => {{
        if let Some(cli_args) = $cli_args {
            // Convert CLI result to serde Value for type-erased handling
            let cli_result = $cli_expr(cli_args);
            match serde_json::to_value(&cli_result) {
                Ok(serde_json::Value::Null) => {
                    // CLI returned None/null - use Env > File > Default precedence
                    $env_value.or($file_value).unwrap_or($default)
                },
                Ok(cli_value) => {
                    // CLI provided a value - deserialize and use it (highest precedence)
                    match serde_json::from_value::<$field_type>(cli_value) {
                        Ok(deserialized) => deserialized,
                        Err(_) => {
                            // Deserialization failed, fall back to env/file/default
                            tracing::warn!("Failed to deserialize CLI value for field {}, using env/file/default", stringify!($field));
                            $env_value.or($file_value).unwrap_or($default)
                        }
                    }
                },
                Err(_) => {
                    // Serialization failed, fall back to env/file/default
                    tracing::warn!("Failed to serialize CLI value for field {}, using env/file/default", stringify!($field));
                    $env_value.or($file_value).unwrap_or($default)
                }
            }
        } else {
            // No CLI args at all: Env > File > Default
            $env_value.or($file_value).unwrap_or($default)
        }
    }};
    (@resolve_value $field:ident, $field_type:ty, $cli_args:expr, $env_value:expr, $file_value:expr, $default:expr) => {
        // No CLI expression provided, so Env > File > Default
        $env_value.or($file_value).unwrap_or($default)
    };
}

// Macro is already exported via #[macro_export]

// Test functionality manually to verify functions work correctly