Skip to main content

ferro_rs/config/
env.rs

1use std::path::Path;
2
3/// Environment type enumeration
4#[derive(Debug, Clone, PartialEq)]
5pub enum Environment {
6    /// Local development environment.
7    Local,
8    /// Shared development environment.
9    Development,
10    /// Pre-production staging environment.
11    Staging,
12    /// Production environment.
13    Production,
14    /// Automated test environment.
15    Testing,
16    /// User-defined environment name.
17    Custom(String),
18}
19
20impl Environment {
21    /// Detect environment from APP_ENV or default to Local
22    pub fn detect() -> Self {
23        match std::env::var("APP_ENV").ok().as_deref() {
24            Some("production") => Self::Production,
25            Some("staging") => Self::Staging,
26            Some("development") => Self::Development,
27            Some("testing") => Self::Testing,
28            Some("local") | None => Self::Local,
29            Some(other) => Self::Custom(other.to_string()),
30        }
31    }
32
33    /// Get the .env file suffix for this environment
34    pub fn env_file_suffix(&self) -> Option<&str> {
35        match self {
36            Self::Local => Some("local"),
37            Self::Production => Some("production"),
38            Self::Staging => Some("staging"),
39            Self::Development => Some("development"),
40            Self::Testing => Some("testing"),
41            Self::Custom(name) => Some(name.as_str()),
42        }
43    }
44
45    /// Check if this is a production environment
46    pub fn is_production(&self) -> bool {
47        matches!(self, Self::Production)
48    }
49
50    /// Check if this is a development environment (local or development)
51    pub fn is_development(&self) -> bool {
52        matches!(self, Self::Local | Self::Development)
53    }
54}
55
56impl std::fmt::Display for Environment {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::Local => write!(f, "local"),
60            Self::Development => write!(f, "development"),
61            Self::Staging => write!(f, "staging"),
62            Self::Production => write!(f, "production"),
63            Self::Testing => write!(f, "testing"),
64            Self::Custom(name) => write!(f, "{name}"),
65        }
66    }
67}
68
69/// Load environment variables from .env files with proper precedence
70///
71/// Precedence (later files override earlier):
72/// 1. .env (base defaults)
73/// 2. .env.local (local overrides, not committed)
74/// 3. .env.{environment} (environment-specific)
75/// 4. .env.{environment}.local (environment-specific local overrides)
76/// 5. Actual system environment variables (highest priority)
77pub fn load_dotenv(project_root: &Path) -> Environment {
78    let env = Environment::detect();
79
80    // Load in REVERSE order of precedence because dotenvy doesn't overwrite existing vars
81    // So we load most specific first, then less specific files won't override
82
83    // 4. Environment-specific local (e.g., .env.production.local) - highest file priority
84    if let Some(suffix) = env.env_file_suffix() {
85        let path = project_root.join(format!(".env.{suffix}.local"));
86        let _ = dotenvy::from_path(&path);
87    }
88
89    // 3. Environment-specific (e.g., .env.production)
90    if let Some(suffix) = env.env_file_suffix() {
91        let path = project_root.join(format!(".env.{suffix}"));
92        let _ = dotenvy::from_path(&path);
93    }
94
95    // 2. .env.local
96    let _ = dotenvy::from_path(project_root.join(".env.local"));
97
98    // 1. .env (base) - lowest file priority
99    let _ = dotenvy::from_path(project_root.join(".env"));
100
101    env
102}
103
104/// Get an environment variable with a default value
105///
106/// # Example
107/// ```
108/// use ferro_rs::config::env;
109///
110/// let port: u16 = env("SERVER_PORT", 8080);
111/// let host = env("SERVER_HOST", "127.0.0.1".to_string());
112/// ```
113pub fn env<T: std::str::FromStr>(key: &str, default: T) -> T {
114    std::env::var(key)
115        .ok()
116        .and_then(|v| v.parse().ok())
117        .unwrap_or(default)
118}
119
120/// Get a required environment variable (panics if not set or invalid)
121///
122/// # Panics
123/// Panics if the environment variable is not set or cannot be parsed
124///
125/// # Example
126/// ```ignore
127/// use ferro_rs::config::env_required;
128///
129/// let secret: String = env_required("APP_SECRET");
130/// ```
131pub fn env_required<T: std::str::FromStr>(key: &str) -> T {
132    std::env::var(key)
133        .ok()
134        .and_then(|v| v.parse().ok())
135        .unwrap_or_else(|| panic!("Required environment variable {key} is not set or invalid"))
136}
137
138/// Get an optional environment variable
139///
140/// # Example
141/// ```
142/// use ferro_rs::config::env_optional;
143///
144/// let debug: Option<bool> = env_optional("APP_DEBUG");
145/// ```
146pub fn env_optional<T: std::str::FromStr>(key: &str) -> Option<T> {
147    std::env::var(key).ok().and_then(|v| v.parse().ok())
148}