use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Default)]
pub struct EnvConfig {
pub base_url: Option<String>,
pub auth_token: Option<String>,
pub model: Option<String>,
pub extras: HashMap<String, String>,
}
impl EnvConfig {
pub fn load() -> Self {
Self::load_from_dir(".")
}
pub fn load_from_dir(dir: &str) -> Self {
let mut config = Self::default();
let env_path = Path::new(dir).join(".env");
if env_path.exists() {
if let Ok(content) = fs::read_to_string(&env_path) {
config.parse_env_file(&content);
}
}
let mut current = Path::new(dir);
for _ in 0..3 {
if let Some(parent) = current.parent() {
let parent_env = parent.join(".env");
if parent_env.exists() && parent_env != env_path {
if let Ok(content) = fs::read_to_string(&parent_env) {
config.parse_env_file(&content);
}
}
current = parent;
} else {
break;
}
}
config.load_from_env();
config
}
fn parse_env_file(&mut self, content: &str) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
let value = value.trim_matches('"').trim_matches('\'');
self.set_value(key, value);
}
}
}
fn load_from_env(&mut self) {
if let Ok(val) = std::env::var("AI_BASE_URL") {
self.base_url = Some(val);
}
if let Ok(val) = std::env::var("AI_AUTH_TOKEN") {
self.auth_token = Some(val);
}
if let Ok(val) = std::env::var("AI_MODEL") {
self.model = Some(val);
}
for (key, value) in std::env::vars() {
if key.starts_with("AI_") {
match key.as_str() {
"AI_BASE_URL" | "AI_AUTH_TOKEN" | "AI_MODEL" => {} _ => {
self.extras.insert(key, value);
}
}
}
}
}
fn set_value(&mut self, key: &str, value: &str) {
match key {
"AI_BASE_URL" => self.base_url = Some(value.to_string()),
"AI_AUTH_TOKEN" => self.auth_token = Some(value.to_string()),
"AI_MODEL" => self.model = Some(value.to_string()),
_ => {
if key.starts_with("AI_") {
self.extras.insert(key.to_string(), value.to_string());
}
}
}
}
pub fn get(&self, key: &str) -> Option<&str> {
match key {
"AI_BASE_URL" => self.base_url.as_deref(),
"AI_AUTH_TOKEN" => self.auth_token.as_deref(),
"AI_MODEL" => self.model.as_deref(),
_ => self.extras.get(key).map(|s| s.as_str()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_env_file() {
let mut config = EnvConfig::default();
config.parse_env_file(r#"
# Comment
AI_BASE_URL="http://localhost:8000"
AI_AUTH_TOKEN='test-token'
AI_MODEL=claude-sonnet-4-6
"#);
assert_eq!(config.base_url, Some("http://localhost:8000".to_string()));
assert_eq!(config.auth_token, Some("test-token".to_string()));
assert_eq!(config.model, Some("claude-sonnet-4-6".to_string()));
}
#[test]
fn test_get_values() {
let config = EnvConfig {
base_url: Some("http://test".to_string()),
auth_token: Some("token".to_string()),
model: Some("model".to_string()),
extras: HashMap::new(),
};
assert_eq!(config.get("AI_BASE_URL"), Some("http://test"));
assert_eq!(config.get("AI_AUTH_TOKEN"), Some("token"));
assert_eq!(config.get("AI_MODEL"), Some("model"));
assert_eq!(config.get("UNKNOWN"), None);
}
}