1#![warn(
2 clippy::all,
3 clippy::pedantic,
4 clippy::nursery,
5 clippy::cargo,
6 clippy::unwrap_used,
7 missing_crate_level_docs,
8 missing_docs
9)]
10
11use std::{
52 env::{self, VarError},
53 ffi::OsString,
54 str::FromStr,
55};
56
57pub fn parse<T: FromStr>(name: &str) -> Result<T, Error>
65where
66 T::Err: Into<Box<dyn std::error::Error + Send + Sync>>,
67{
68 let value_result = env::var(name);
69 let value = match value_result {
70 Ok(value) => {
71 if value.is_empty() {
72 return Err(Error::Missing {
73 name: name.to_owned(),
74 });
75 }
76 value
77 }
78 Err(err) => match err {
79 VarError::NotPresent => {
80 return Err(Error::Missing {
81 name: name.to_owned(),
82 })
83 }
84 VarError::NotUnicode(value) => {
85 return Err(Error::InvalidUtf8 {
86 name: name.to_owned(),
87 value,
88 })
89 }
90 },
91 };
92 let parse_result = T::from_str(&value);
93 let parsed = match parse_result {
94 Ok(parsed) => parsed,
95 Err(err) => {
96 return Err(Error::InvalidValue {
97 name: name.to_owned(),
98 value,
99 source: err.into(),
100 })
101 }
102 };
103 Ok(parsed)
104}
105
106pub fn parse_optional<T: FromStr>(name: &str) -> Result<Option<T>, Error>
115where
116 T::Err: Into<Box<dyn std::error::Error + Send + Sync>>,
117{
118 let result = parse(name);
119 match result {
120 Ok(parsed) => Ok(Some(parsed)),
121 Err(Error::Missing { .. }) => Ok(None),
122 Err(err) => Err(err),
123 }
124}
125
126pub fn parse_or_default<T: FromStr + Default>(name: &str) -> Result<T, Error>
135where
136 T::Err: Into<Box<dyn std::error::Error + Send + Sync>>,
137{
138 parse_optional(name).map(Option::unwrap_or_default)
139}
140
141#[derive(Debug, thiserror::Error)]
142pub enum Error {
144 #[error("Missing or empty environment variable {name:?}")]
146 Missing {
147 name: String,
149 },
150 #[error("Invalid UTF-8 in environment variable {name:?}")]
151 InvalidUtf8 {
154 name: String,
156 value: OsString,
158 },
159 #[error("Error parsing environment variable {name:?}: {source}")]
160 InvalidValue {
163 name: String,
165 value: String,
167 #[source]
168 source: Box<dyn std::error::Error + Send + Sync>,
170 },
171}
172
173#[allow(
174 clippy::missing_const_for_fn,
175 clippy::unwrap_used,
176 clippy::wildcard_imports
177)]
178#[cfg(test)]
179mod tests {
180 use crate::*;
181 use os_str_bytes::OsStrBytes;
182 use serial_test::serial;
183 use std::ffi::OsStr;
184
185 mod parse {
186 use super::*;
187
188 #[test]
189 #[serial]
190 fn valid() {
191 let _guard = EnvGuard::with("TEST_VAR", "255");
192 let value: u8 = parse("TEST_VAR").unwrap();
193 assert_eq!(value, 255);
194 }
195
196 #[test]
197 #[serial]
198 fn missing() {
199 let _guard = EnvGuard::without("TEST_VAR");
200 let error = parse::<u8>("TEST_VAR").unwrap_err();
201 assert!(matches!(error, Error::Missing { .. }));
202 }
203
204 #[test]
205 #[serial]
206 fn empty() {
207 let _guard = EnvGuard::with("TEST_VAR", "");
208 let error = parse::<u8>("TEST_VAR").unwrap_err();
209 assert!(matches!(error, Error::Missing { .. }));
210 }
211
212 #[test]
213 #[serial]
214 fn invalid_utf8() {
215 let value = invalid_utf8_string();
216 let _guard = EnvGuard::with("TEST_VAR", value);
217 let error = parse::<u8>("TEST_VAR").unwrap_err();
218 assert!(matches!(error, Error::InvalidUtf8 { .. }));
219 }
220
221 #[test]
222 #[serial]
223 fn invalid_value() {
224 let _guard = EnvGuard::with("TEST_VAR", "256");
225 let error = parse::<u8>("TEST_VAR").unwrap_err();
226 assert!(matches!(error, Error::InvalidValue { .. }));
227 }
228 }
229
230 mod parse_optional {
231 use super::*;
232
233 #[test]
234 #[serial]
235 fn valid() {
236 let _guard = EnvGuard::with("TEST_VAR", "255");
237 let value: u8 = parse_optional("TEST_VAR").unwrap().unwrap();
238 assert_eq!(value, 255);
239 }
240
241 #[test]
242 #[serial]
243 fn missing() {
244 let _guard = EnvGuard::without("TEST_VAR");
245 let option = parse_optional::<u8>("TEST_VAR").unwrap();
246 assert_eq!(option, None);
247 }
248
249 #[test]
250 #[serial]
251 fn empty() {
252 let _guard = EnvGuard::with("TEST_VAR", "");
253 let option = parse_optional::<u8>("TEST_VAR").unwrap();
254 assert_eq!(option, None);
255 }
256
257 #[test]
258 #[serial]
259 fn invalid_utf8() {
260 let invalid_unicode_bytes = [b'f', b'o', b'o', 0x80];
261 let invalid_unicode = OsStr::from_raw_bytes(&invalid_unicode_bytes[..]).unwrap();
262 let _guard = EnvGuard::with("TEST_VAR", &invalid_unicode);
263 let error = parse_optional::<u8>("TEST_VAR").unwrap_err();
264 assert!(matches!(error, Error::InvalidUtf8 { .. }));
265 }
266
267 #[test]
268 #[serial]
269 fn invalid_value() {
270 let _guard = EnvGuard::with("TEST_VAR", "256");
271 let error = parse_optional::<u8>("TEST_VAR").unwrap_err();
272 assert!(matches!(error, Error::InvalidValue { .. }));
273 }
274 }
275
276 mod parse_or_default {
277 use super::*;
278
279 #[test]
280 #[serial]
281 fn valid() {
282 let _guard = EnvGuard::with("TEST_VAR", "255");
283 let value: u8 = parse_or_default("TEST_VAR").unwrap();
284 assert_eq!(value, 255);
285 }
286
287 #[test]
288 #[serial]
289 fn missing() {
290 let _guard = EnvGuard::without("TEST_VAR");
291 let value: u8 = parse_or_default::<u8>("TEST_VAR").unwrap();
292 assert_eq!(value, 0);
293 }
294
295 #[test]
296 #[serial]
297 fn empty() {
298 let _guard = EnvGuard::with("TEST_VAR", "");
299 let value: u8 = parse_or_default::<u8>("TEST_VAR").unwrap();
300 assert_eq!(value, 0);
301 }
302
303 #[test]
304 #[serial]
305 fn invalid_utf8() {
306 let invalid_unicode_bytes = [b'f', b'o', b'o', 0x80];
307 let invalid_unicode = OsStr::from_raw_bytes(&invalid_unicode_bytes[..]).unwrap();
308 let _guard = EnvGuard::with("TEST_VAR", &invalid_unicode);
309 let error = parse_or_default::<u8>("TEST_VAR").unwrap_err();
310 assert!(matches!(error, Error::InvalidUtf8 { .. }));
311 }
312
313 #[test]
314 #[serial]
315 fn invalid_value() {
316 let _guard = EnvGuard::with("TEST_VAR", "256");
317 let error = parse_or_default::<u8>("TEST_VAR").unwrap_err();
318 assert!(matches!(error, Error::InvalidValue { .. }));
319 }
320 }
321
322 mod error {
323 use super::*;
324
325 #[test]
326 fn is_send() {
327 assert_send::<Error>();
328 }
329
330 #[test]
331 fn is_sync() {
332 assert_sync::<Error>();
333 }
334
335 #[test]
336 fn is_static() {
337 assert_static::<Error>();
338 }
339
340 #[test]
341 fn is_into_anyhow() {
342 assert_into_anyhow::<Error>();
343 }
344
345 #[test]
346 fn missing() {
347 let error = Error::Missing {
348 name: "TEST_VAR".into(),
349 };
350 assert_eq!(
351 error.to_string(),
352 "Missing or empty environment variable \"TEST_VAR\"",
353 );
354 }
355
356 #[test]
357 fn invalid_utf8() {
358 let error = Error::InvalidUtf8 {
359 name: "TEST_VAR".into(),
360 value: invalid_utf8_string(),
361 };
362 assert_eq!(
363 error.to_string(),
364 "Invalid UTF-8 in environment variable \"TEST_VAR\"",
365 );
366 }
367
368 #[test]
369 fn invalid_value() {
370 let source = "".parse::<u8>().unwrap_err();
371 let error = Error::InvalidValue {
372 name: "TEST_VAR".into(),
373 value: "".into(),
374 source: source.into(),
375 };
376 assert_eq!(
377 error.to_string(),
378 "Error parsing environment variable \"TEST_VAR\": cannot parse integer from empty string",
379 );
380 }
381 }
382
383 fn assert_send<T: Send>() {}
386 fn assert_sync<T: Sync>() {}
387 fn assert_static<T: 'static>() {}
388 fn assert_into_anyhow<T: Into<anyhow::Error>>() {}
389
390 fn invalid_utf8_string() -> OsString {
391 let bytes = [b'f', b'o', b'o', 0x80];
392 std::str::from_utf8(&bytes).unwrap_err();
393 OsStr::from_raw_bytes(&bytes[..]).unwrap().to_os_string()
394 }
395
396 struct EnvGuard {
397 vars: Vec<(OsString, OsString)>,
398 }
399
400 impl EnvGuard {
401 fn new() -> Self {
402 Self {
403 vars: std::env::vars_os().collect(),
404 }
405 }
406 fn with(name: &str, value: impl AsRef<OsStr>) -> Self {
407 let guard = Self::new();
408 std::env::set_var(name, value);
409 guard
410 }
411 fn without(name: impl AsRef<OsStr>) -> Self {
412 let guard = Self::new();
413 std::env::remove_var(name);
414 guard
415 }
416 }
417
418 impl Drop for EnvGuard {
419 fn drop(&mut self) {
420 for (var, _) in std::env::vars_os() {
421 std::env::remove_var(var);
422 }
423 assert_eq!(std::env::vars_os().count(), 0);
424 for (var, value) in &self.vars {
425 std::env::set_var(var, value);
426 }
427 }
428 }
429}