1use std::env;
21use std::str::FromStr;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
30#[allow(missing_docs)]
31pub enum EnvError {
32 NotSet(String),
34 ParseError {
36 var: String,
37 value: String,
38 expected: String,
41 },
42 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#[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#[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
102pub 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#[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#[must_use]
159pub fn get_string(key: &str) -> Option<String> {
160 env::var(key).ok().filter(|s| !s.is_empty())
161}
162
163#[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#[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#[must_use]
202pub fn is_set(key: &str) -> bool {
203 env::var(key).map(|v| !v.is_empty()).unwrap_or(false)
204}
205
206#[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#[must_use]
221pub fn is_production() -> bool {
222 let env = get_environment();
223 env == "production" || env == "prod"
224}
225
226#[must_use]
228pub fn is_development() -> bool {
229 let env = get_environment();
230 env == "development" || env == "dev" || env.is_empty()
231}
232
233#[must_use]
235pub fn is_test() -> bool {
236 let env = get_environment();
237 env == "test" || env == "testing"
238}
239
240#[derive(Debug, Default)]
242pub struct EnvConfig {
243 vars: Vec<(String, Option<String>)>,
244}
245
246impl EnvConfig {
247 #[must_use]
249 pub fn new() -> Self {
250 Self::default()
251 }
252
253 #[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 #[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 #[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 #[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 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}