use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct EnvRepo {
vars: HashMap<String, String>,
}
impl EnvRepo {
pub fn new() -> Self {
Self {
vars: HashMap::new(),
}
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let mut repo = Self::new();
if path.is_file() {
let iter = dotenvy::from_filename_iter(path)?;
for entry in iter {
let (key, value) = entry?;
repo.vars.insert(key, value);
}
}
for (key, value) in std::env::vars() {
repo.vars.insert(key, value);
}
Ok(repo)
}
pub fn load_from_dir(dir: impl AsRef<Path>) -> Result<Self> {
Self::load(dir.as_ref().join(".env"))
}
pub fn get(&self, key: &str) -> Option<&str> {
self.vars.get(key).map(|s| s.as_str())
}
pub fn get_or<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
self.get(key).unwrap_or(default)
}
pub fn has(&self, key: &str) -> bool {
self.vars.contains_key(key)
}
pub fn len(&self) -> usize {
self.vars.len()
}
pub fn is_empty(&self) -> bool {
self.vars.is_empty()
}
}
impl Default for EnvRepo {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
#[test]
fn test_empty_repo() {
let repo = EnvRepo::new();
assert!(repo.is_empty());
assert_eq!(repo.get("NOT_SET"), None);
}
#[test]
fn test_load_dotenv_file() {
let dir = std::env::temp_dir().join("ravel_env_test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let env_path = dir.join(".env");
let mut f = fs::File::create(&env_path).unwrap();
writeln!(f, "APP_NAME=RavelApp").unwrap();
writeln!(f, "APP_PORT=8080").unwrap();
writeln!(f, "# a comment").unwrap();
writeln!(f, "").unwrap();
writeln!(f, "DB_HOST=localhost").unwrap();
drop(f);
let repo = EnvRepo::load(&env_path).unwrap();
assert_eq!(repo.get("APP_NAME"), Some("RavelApp"));
assert_eq!(repo.get("APP_PORT"), Some("8080"));
assert_eq!(repo.get("DB_HOST"), Some("localhost"));
assert!(!repo.has("# a comment"));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_real_env_overrides_file() {
unsafe { std::env::set_var("RAVEL_TEST_FOO", "from_real_env") };
let dir = std::env::temp_dir().join("ravel_env_override_test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let env_path = dir.join(".env");
let mut f = fs::File::create(&env_path).unwrap();
writeln!(f, "RAVEL_TEST_FOO=from_dotenv_file").unwrap();
drop(f);
let repo = EnvRepo::load(&env_path).unwrap();
assert_eq!(repo.get("RAVEL_TEST_FOO"), Some("from_real_env"));
unsafe { std::env::remove_var("RAVEL_TEST_FOO") };
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_missing_file_is_ok() {
let repo = EnvRepo::load("/nonexistent/path/.env").unwrap();
assert!(!repo.is_empty()); }
}