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