use std::path::Path;
pub const DB_URL_ENV_VARS: &[&str] = &["DATABASE_URL", "DB_URL", "POSTGRES_URL"];
#[derive(Debug, Clone, Default)]
pub struct EnvConfig {
pub database_url: Option<String>,
pub dotenv_loaded: bool,
pub dotenv_path: Option<String>,
pub database_url_source: Option<String>,
}
impl EnvConfig {
pub fn load() -> Self {
Self::load_from_dir(Path::new("."))
}
pub fn load_from_dir(dir: &Path) -> Self {
let mut config = EnvConfig::default();
let env_path = dir.join(".env");
if env_path.exists() {
match dotenvy::from_path(&env_path) {
Ok(()) => {
config.dotenv_loaded = true;
config.dotenv_path = Some(env_path.to_string_lossy().to_string());
}
Err(e) => {
tracing::debug!("Failed to load .env file: {}", e);
}
}
} else {
if dotenvy::dotenv().is_ok() {
config.dotenv_loaded = true;
config.dotenv_path = dotenvy::var("DOTENV_FILE").ok();
}
}
for var_name in DB_URL_ENV_VARS {
if let Ok(url) = std::env::var(var_name) {
if !url.is_empty() {
config.database_url = Some(url);
config.database_url_source = Some(var_name.to_string());
break;
}
}
}
config
}
pub fn resolve_db_url(
&self,
cli_url: Option<&str>,
config_url: Option<&str>,
) -> Option<String> {
cli_url
.map(String::from)
.or_else(|| self.database_url.clone())
.or_else(|| config_url.map(String::from))
}
pub fn has_db_url(&self) -> bool {
self.database_url.is_some()
}
pub fn db_url_source_description(&self) -> Option<String> {
self.database_url_source.as_ref().map(|source| {
if self.dotenv_loaded {
format!("{} (from .env)", source)
} else {
format!("{} (from environment)", source)
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
fn clear_db_env_vars() {
std::env::remove_var("DATABASE_URL");
std::env::remove_var("DB_URL");
std::env::remove_var("POSTGRES_URL");
}
#[test]
fn test_priority_order() {
let _lock = ENV_MUTEX.lock().unwrap();
clear_db_env_vars();
std::env::set_var("POSTGRES_URL", "postgres://fallback");
let config = EnvConfig::load();
assert_eq!(config.database_url, Some("postgres://fallback".to_string()));
assert_eq!(config.database_url_source, Some("POSTGRES_URL".to_string()));
std::env::set_var("DB_URL", "postgres://medium");
let config = EnvConfig::load();
assert_eq!(config.database_url, Some("postgres://medium".to_string()));
assert_eq!(config.database_url_source, Some("DB_URL".to_string()));
std::env::set_var("DATABASE_URL", "postgres://primary");
let config = EnvConfig::load();
assert_eq!(config.database_url, Some("postgres://primary".to_string()));
assert_eq!(config.database_url_source, Some("DATABASE_URL".to_string()));
clear_db_env_vars();
}
#[test]
fn test_cli_takes_precedence() {
let _lock = ENV_MUTEX.lock().unwrap();
clear_db_env_vars();
std::env::set_var("DATABASE_URL", "postgres://from_env");
let config = EnvConfig::load();
let resolved = config.resolve_db_url(Some("postgres://from_cli"), None);
assert_eq!(resolved, Some("postgres://from_cli".to_string()));
let resolved = config.resolve_db_url(None, None);
assert_eq!(resolved, Some("postgres://from_env".to_string()));
let resolved = config.resolve_db_url(None, Some("postgres://from_config"));
assert_eq!(resolved, Some("postgres://from_env".to_string()));
clear_db_env_vars();
}
#[test]
fn test_empty_vars_ignored() {
let _lock = ENV_MUTEX.lock().unwrap();
clear_db_env_vars();
std::env::set_var("POSTGRES_URL", "");
let config = EnvConfig::load();
assert!(config.database_url.is_none());
clear_db_env_vars();
}
}