1use std::env;
21use std::str::FromStr;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25#[allow(missing_docs)]
26pub enum EnvError {
27 NotSet(String),
29 ParseError {
31 var: String,
32 value: String,
33 expected: String,
34 },
35 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#[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#[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
95pub 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#[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#[must_use]
148pub fn get_string(key: &str) -> Option<String> {
149 env::var(key).ok().filter(|s| !s.is_empty())
150}
151
152#[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#[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#[must_use]
188pub fn is_set(key: &str) -> bool {
189 env::var(key).map(|v| !v.is_empty()).unwrap_or(false)
190}
191
192#[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#[must_use]
207pub fn is_production() -> bool {
208 let env = get_environment();
209 env == "production" || env == "prod"
210}
211
212#[must_use]
214pub fn is_development() -> bool {
215 let env = get_environment();
216 env == "development" || env == "dev" || env.is_empty()
217}
218
219#[must_use]
221pub fn is_test() -> bool {
222 let env = get_environment();
223 env == "test" || env == "testing"
224}
225
226#[derive(Debug, Default)]
228pub struct EnvConfig {
229 vars: Vec<(String, Option<String>)>,
230}
231
232impl EnvConfig {
233 #[must_use]
235 pub fn new() -> Self {
236 Self::default()
237 }
238
239 pub fn require(&mut self, key: &str) -> &mut Self {
241 self.vars.push((key.to_string(), None));
242 self
243 }
244
245 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 #[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 #[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 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}