Skip to main content

commons/
env.rs

1//! Environment variable utilities.
2//!
3//! Provides typed access to environment variables with defaults and validation.
4//!
5//! # Example
6//!
7//! ```rust
8//! use commons::env::{get_env, get_env_or, require_env};
9//!
10//! // Get optional env var
11//! let port: Option<u16> = get_env("PORT");
12//!
13//! // Get with default
14//! let host: String = get_env_or("HOST", "localhost".to_string());
15//!
16//! // Require env var (panics if missing)
17//! // let api_key: String = require_env("API_KEY");
18//! ```
19
20use std::env;
21use std::str::FromStr;
22
23/// Error type for environment variable operations.
24///
25/// The `expected` field in [`ParseError`](EnvError::ParseError) is populated
26/// via [`std::any::type_name`], which is not guaranteed to be stable across
27/// compiler versions. It is intended for human-readable diagnostics only —
28/// do not match on its string value programmatically.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[allow(missing_docs)]
31pub enum EnvError {
32    /// Variable is not set.
33    NotSet(String),
34    /// Variable value cannot be parsed.
35    ParseError {
36        var: String,
37        value: String,
38        /// Human-readable type name (from `std::any::type_name`).
39        /// Not stable across compiler versions — for display only.
40        expected: String,
41    },
42    /// Variable value is empty.
43    Empty(String),
44}
45
46impl std::fmt::Display for EnvError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::NotSet(var) => write!(f, "Environment variable not set: {var}"),
50            Self::ParseError {
51                var,
52                value,
53                expected,
54            } => {
55                write!(f, "Cannot parse {var}={value} as {expected}")
56            }
57            Self::Empty(var) => write!(f, "Environment variable is empty: {var}"),
58        }
59    }
60}
61
62impl std::error::Error for EnvError {}
63
64/// Get an environment variable, parsed to the specified type.
65///
66/// Returns `None` if the variable is not set or cannot be parsed.
67///
68/// # Example
69///
70/// ```rust
71/// use commons::env::get_env;
72///
73/// let port: Option<u16> = get_env("PORT");
74/// let debug: Option<bool> = get_env("DEBUG");
75/// ```
76#[must_use]
77pub fn get_env<T>(key: &str) -> Option<T>
78where
79    T: FromStr,
80{
81    env::var(key).ok().and_then(|v| v.parse().ok())
82}
83
84/// Get an environment variable or return a default value.
85///
86/// # Example
87///
88/// ```rust
89/// use commons::env::get_env_or;
90///
91/// let port: u16 = get_env_or("PORT", 8080);
92/// let host: String = get_env_or("HOST", "localhost".to_string());
93/// ```
94#[must_use]
95pub fn get_env_or<T>(key: &str, default: T) -> T
96where
97    T: FromStr,
98{
99    get_env(key).unwrap_or(default)
100}
101
102/// Get an environment variable, returning an error if not set or invalid.
103///
104/// # Errors
105///
106/// Returns an error if the variable is not set, empty, or cannot be parsed.
107///
108/// # Example
109///
110/// ```rust
111/// use commons::env::try_get_env;
112///
113/// let port: Result<u16, _> = try_get_env("PORT");
114/// ```
115pub fn try_get_env<T>(key: &str) -> Result<T, EnvError>
116where
117    T: FromStr,
118{
119    let value = env::var(key).map_err(|_| EnvError::NotSet(key.to_string()))?;
120
121    if value.is_empty() {
122        return Err(EnvError::Empty(key.to_string()));
123    }
124
125    value.parse().map_err(|_| EnvError::ParseError {
126        var: key.to_string(),
127        value,
128        expected: std::any::type_name::<T>().to_string(),
129    })
130}
131
132/// Require an environment variable, panicking if not set.
133///
134/// # Panics
135///
136/// Panics if the variable is not set or cannot be parsed.
137///
138/// # Example
139///
140/// ```rust,no_run
141/// use commons::env::require_env;
142///
143/// let api_key: String = require_env("API_KEY");
144/// ```
145#[must_use]
146pub fn require_env<T>(key: &str) -> T
147where
148    T: FromStr,
149    <T as FromStr>::Err: std::fmt::Debug,
150{
151    env::var(key)
152        .unwrap_or_else(|_| panic!("Required environment variable not set: {key}"))
153        .parse()
154        .unwrap_or_else(|e| panic!("Cannot parse environment variable {key}: {e:?}"))
155}
156
157/// Get an environment variable as a string.
158#[must_use]
159pub fn get_string(key: &str) -> Option<String> {
160    env::var(key).ok().filter(|s| !s.is_empty())
161}
162
163/// Get an environment variable as a boolean.
164///
165/// Recognizes: "true", "1", "yes", "on" as true (case-insensitive).
166/// Everything else is false.
167#[must_use]
168pub fn get_bool(key: &str) -> bool {
169    env::var(key).is_ok_and(|v| {
170        v == "1"
171            || v.eq_ignore_ascii_case("true")
172            || v.eq_ignore_ascii_case("yes")
173            || v.eq_ignore_ascii_case("on")
174    })
175}
176
177/// Get an environment variable as a list, split by a delimiter.
178///
179/// # Example
180///
181/// ```rust
182/// use commons::env::get_list;
183///
184/// // If FEATURES="a,b,c"
185/// // let features: Vec<String> = get_list("FEATURES", ",");
186/// // features == ["a", "b", "c"]
187/// ```
188#[must_use]
189pub fn get_list(key: &str, delimiter: &str) -> Vec<String> {
190    env::var(key)
191        .map(|v| {
192            v.split(delimiter)
193                .map(|s| s.trim().to_string())
194                .filter(|s| !s.is_empty())
195                .collect()
196        })
197        .unwrap_or_default()
198}
199
200/// Check if an environment variable is set (and non-empty).
201#[must_use]
202pub fn is_set(key: &str) -> bool {
203    env::var(key).map(|v| !v.is_empty()).unwrap_or(false)
204}
205
206/// Get the current environment name (development, staging, production).
207///
208/// Checks `ENV`, `ENVIRONMENT`, `RUST_ENV`, `APP_ENV` in order.
209#[must_use]
210pub fn get_environment() -> String {
211    for key in &["ENV", "ENVIRONMENT", "RUST_ENV", "APP_ENV"] {
212        if let Some(env) = get_string(key) {
213            return env.to_lowercase();
214        }
215    }
216    "development".to_string()
217}
218
219/// Check if running in production environment.
220#[must_use]
221pub fn is_production() -> bool {
222    let env = get_environment();
223    env == "production" || env == "prod"
224}
225
226/// Check if running in development environment.
227#[must_use]
228pub fn is_development() -> bool {
229    let env = get_environment();
230    env == "development" || env == "dev" || env.is_empty()
231}
232
233/// Check if running in test environment.
234#[must_use]
235pub fn is_test() -> bool {
236    let env = get_environment();
237    env == "test" || env == "testing"
238}
239
240/// Environment configuration builder.
241#[derive(Debug, Default)]
242pub struct EnvConfig {
243    vars: Vec<(String, Option<String>)>,
244}
245
246impl EnvConfig {
247    /// Create a new environment configuration.
248    #[must_use]
249    pub fn new() -> Self {
250        Self::default()
251    }
252
253    /// Add a required variable.
254    #[must_use]
255    pub fn require(&mut self, key: &str) -> &mut Self {
256        self.vars.push((key.to_string(), None));
257        self
258    }
259
260    /// Add an optional variable with a default.
261    #[must_use]
262    pub fn optional(&mut self, key: &str, default: &str) -> &mut Self {
263        self.vars.push((key.to_string(), Some(default.to_string())));
264        self
265    }
266
267    /// Validate all required variables are set.
268    ///
269    /// Returns a list of missing required variables.
270    #[must_use]
271    pub fn validate(&self) -> Vec<String> {
272        self.vars
273            .iter()
274            .filter(|(_, default)| default.is_none())
275            .filter(|(key, _)| !is_set(key))
276            .map(|(key, _)| key.clone())
277            .collect()
278    }
279
280    /// Check if configuration is valid.
281    #[must_use]
282    pub fn is_valid(&self) -> bool {
283        self.validate().is_empty()
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_get_env_missing() {
293        let value: Option<String> = get_env("NONEXISTENT_VAR_12345");
294        assert_eq!(value, None);
295    }
296
297    #[test]
298    fn test_get_env_or_default() {
299        let value: u16 = get_env_or("NONEXISTENT_PORT", 3000);
300        assert_eq!(value, 3000);
301    }
302
303    #[test]
304    fn test_get_bool_missing() {
305        assert!(!get_bool("NONEXISTENT_BOOL_VAR"));
306    }
307
308    #[test]
309    fn test_is_set_missing() {
310        assert!(!is_set("NONEXISTENT_VAR_99999"));
311    }
312
313    #[test]
314    fn test_get_list_missing() {
315        let list = get_list("NONEXISTENT_LIST_VAR", ",");
316        assert!(list.is_empty());
317    }
318
319    #[test]
320    fn test_env_config_validation() {
321        let mut config = EnvConfig::new();
322        let _ = config
323            .require("DEFINITELY_NOT_SET_VAR")
324            .optional("OPTIONAL_VAR", "default");
325
326        let missing = config.validate();
327        assert_eq!(missing, vec!["DEFINITELY_NOT_SET_VAR"]);
328        assert!(!config.is_valid());
329    }
330
331    #[test]
332    fn test_get_environment_default() {
333        // Without any ENV vars set, should return "development"
334        let env = get_environment();
335        assert!(!env.is_empty());
336    }
337
338    #[test]
339    fn test_try_get_env_missing() {
340        let result: Result<String, EnvError> = try_get_env("NONEXISTENT_TRY_VAR");
341        assert!(result.is_err());
342        assert!(matches!(result.unwrap_err(), EnvError::NotSet(_)));
343    }
344}