1use std::path::Path;
14
15pub const DB_URL_ENV_VARS: &[&str] = &["DATABASE_URL", "DB_URL", "POSTGRES_URL"];
17
18#[derive(Debug, Clone, Default)]
20pub struct EnvConfig {
21 pub database_url: Option<String>,
23 pub dotenv_loaded: bool,
25 pub dotenv_path: Option<String>,
27 pub database_url_source: Option<String>,
29}
30
31impl EnvConfig {
32 pub fn load() -> Self {
46 Self::load_from_dir(Path::new("."))
47 }
48
49 pub fn load_from_dir(dir: &Path) -> Self {
51 let mut config = EnvConfig::default();
52
53 let env_path = dir.join(".env");
55 if env_path.exists() {
56 match dotenvy::from_path(&env_path) {
57 Ok(()) => {
58 config.dotenv_loaded = true;
59 config.dotenv_path = Some(env_path.to_string_lossy().to_string());
60 }
61 Err(e) => {
62 tracing::debug!("Failed to load .env file: {}", e);
64 }
65 }
66 } else {
67 if dotenvy::dotenv().is_ok() {
69 config.dotenv_loaded = true;
70 config.dotenv_path = dotenvy::var("DOTENV_FILE").ok();
71 }
72 }
73
74 for var_name in DB_URL_ENV_VARS {
76 if let Ok(url) = std::env::var(var_name) {
77 if !url.is_empty() {
78 config.database_url = Some(url);
79 config.database_url_source = Some(var_name.to_string());
80 break;
81 }
82 }
83 }
84
85 config
86 }
87
88 pub fn resolve_db_url(
97 &self,
98 cli_url: Option<&str>,
99 config_url: Option<&str>,
100 ) -> Option<String> {
101 cli_url
103 .map(String::from)
104 .or_else(|| self.database_url.clone())
105 .or_else(|| config_url.map(String::from))
106 }
107
108 pub fn has_db_url(&self) -> bool {
110 self.database_url.is_some()
111 }
112
113 pub fn db_url_source_description(&self) -> Option<String> {
115 self.database_url_source.as_ref().map(|source| {
116 if self.dotenv_loaded {
117 format!("{} (from .env)", source)
118 } else {
119 format!("{} (from environment)", source)
120 }
121 })
122 }
123}
124
125#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::sync::Mutex;
133
134 static ENV_MUTEX: Mutex<()> = Mutex::new(());
136
137 fn clear_db_env_vars() {
138 std::env::remove_var("DATABASE_URL");
139 std::env::remove_var("DB_URL");
140 std::env::remove_var("POSTGRES_URL");
141 }
142
143 #[test]
144 fn test_priority_order() {
145 let _lock = ENV_MUTEX.lock().unwrap();
146 clear_db_env_vars();
147
148 std::env::set_var("POSTGRES_URL", "postgres://fallback");
150 let config = EnvConfig::load();
151 assert_eq!(config.database_url, Some("postgres://fallback".to_string()));
152 assert_eq!(config.database_url_source, Some("POSTGRES_URL".to_string()));
153
154 std::env::set_var("DB_URL", "postgres://medium");
156 let config = EnvConfig::load();
157 assert_eq!(config.database_url, Some("postgres://medium".to_string()));
158 assert_eq!(config.database_url_source, Some("DB_URL".to_string()));
159
160 std::env::set_var("DATABASE_URL", "postgres://primary");
162 let config = EnvConfig::load();
163 assert_eq!(config.database_url, Some("postgres://primary".to_string()));
164 assert_eq!(config.database_url_source, Some("DATABASE_URL".to_string()));
165
166 clear_db_env_vars();
168 }
169
170 #[test]
171 fn test_cli_takes_precedence() {
172 let _lock = ENV_MUTEX.lock().unwrap();
173 clear_db_env_vars();
174
175 std::env::set_var("DATABASE_URL", "postgres://from_env");
176 let config = EnvConfig::load();
177
178 let resolved = config.resolve_db_url(Some("postgres://from_cli"), None);
180 assert_eq!(resolved, Some("postgres://from_cli".to_string()));
181
182 let resolved = config.resolve_db_url(None, None);
184 assert_eq!(resolved, Some("postgres://from_env".to_string()));
185
186 let resolved = config.resolve_db_url(None, Some("postgres://from_config"));
188 assert_eq!(resolved, Some("postgres://from_env".to_string()));
189
190 clear_db_env_vars();
191 }
192
193 #[test]
194 fn test_empty_vars_ignored() {
195 let _lock = ENV_MUTEX.lock().unwrap();
196 clear_db_env_vars();
197
198 std::env::set_var("POSTGRES_URL", "");
199
200 let config = EnvConfig::load();
201 assert!(config.database_url.is_none());
202
203 clear_db_env_vars();
204 }
205}