use crate::error::ConfigError; use super::INJECTED_VARS; /// Crate-wide mutex for tests that mutate process environment variables. /// /// The process environment is global state shared across all threads. /// Per-module mutexes do NOT prevent races between modules running in /// parallel. Every `unsafe { set_var / remove_var }` call in tests /// MUST hold this single lock. #[cfg(test)] pub(crate) static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); pub(crate) fn optional_env(key: &str) -> Result, ConfigError> { // Check real env vars first (always win over injected secrets) match std::env::var(key) { Ok(val) if val.is_empty() => {} Ok(val) => return Ok(Some(val)), Err(std::env::VarError::NotPresent) => {} Err(e) => { return Err(ConfigError::ParseError(format!( "failed to read {key}: {e}" ))); } } // Fall back to thread-safe overlay (secrets injected from DB) if let Some(val) = INJECTED_VARS .lock() .unwrap_or_else(|p| p.into_inner()) .get(key) .cloned() { return Ok(Some(val)); } Ok(None) } pub(crate) fn parse_optional_env(key: &str, default: T) -> Result where T: std::str::FromStr, T::Err: std::fmt::Display, { optional_env(key)? .map(|s| { s.parse().map_err(|e| ConfigError::InvalidValue { key: key.to_string(), message: format!("{e}"), }) }) .transpose() .map(|opt| opt.unwrap_or(default)) } /// Parse a boolean from an env var with a default. /// /// Accepts "true"/"1" as true, "false"/"0" as false. pub(crate) fn parse_bool_env(key: &str, default: bool) -> Result { match optional_env(key)? { Some(s) => match s.to_lowercase().as_str() { "true" | "1" => Ok(true), "false" | "0" => Ok(false), _ => Err(ConfigError::InvalidValue { key: key.to_string(), message: format!("must be 'true' or 'false', got '{s}'"), }), }, None => Ok(default), } } /// Parse an env var into `Option` — returns `None` when unset, /// `Some(parsed)` when set to a valid value. pub(crate) fn parse_option_env(key: &str) -> Result, ConfigError> where T: std::str::FromStr, T::Err: std::fmt::Display, { optional_env(key)? .map(|s| { s.parse().map_err(|e| ConfigError::InvalidValue { key: key.to_string(), message: format!("{e}"), }) }) .transpose() } /// Parse a string from an env var with a default. pub(crate) fn parse_string_env( key: &str, default: impl Into, ) -> Result { Ok(optional_env(key)?.unwrap_or_else(|| default.into())) }