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