strict_env/
lib.rs

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
11//! Use this crate to parse environment variables into any type that
12//! implements [`FromStr`](std::str::FromStr).
13//!
14//! # Basic usage
15//! ```
16//! # fn main() -> Result<(), strict_env::Error> {
17//! std::env::set_var("PORT", "9001");
18//! let port: u16 = strict_env::parse("PORT")?;
19//! assert_eq!(port, 9001);
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! # Usage with remote types
25//! If you need to parse a type that originates from an external crate
26//! and does not implement [`FromStr`](std::str::FromStr), you can wrap
27//! the value in a newtype that implements the trait.
28//! ```
29//! // std::time::Duration does not implement FromStr!
30//! struct ConfigDuration(std::time::Duration);
31//!
32//! // Custom implementation using the awesome humantime crate
33//! impl std::str::FromStr for ConfigDuration {
34//!     type Err = humantime::DurationError;
35//!
36//!     fn from_str(s: &str) -> Result<Self, Self::Err> {
37//!         let inner = humantime::parse_duration(s)?;
38//!         Ok(Self(inner))
39//!     }
40//! }
41//!
42//! // Now we can use strict_env! (But we might have to use the turbofish.)
43//! # fn main() -> Result<(), strict_env::Error> {
44//! std::env::set_var("CACHE_DURATION", "2 minutes");
45//! let cache_duration = strict_env::parse::<ConfigDuration>("CACHE_DURATION")?.0;
46//! assert_eq!(cache_duration.as_secs(), 120);
47//! # Ok(())
48//! # }
49//! ```
50
51use std::{
52    env::{self, VarError},
53    ffi::OsString,
54    str::FromStr,
55};
56
57/// Parse an environment variable into a value that implements
58/// [`FromStr`](std::str::FromStr).
59///
60/// # Errors
61/// Returns an error if the requested environment variable is missing
62/// or empty, contains invalid UTF-8, or has a value that cannot be
63/// parsed into the target type.
64pub 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
106/// Like [`parse`](crate::parse), but allows the environment variable to
107/// be missing or empty.
108///
109/// The parsed object is wrapped in an [`Option`](Option) to allow this.
110///
111/// # Errors
112/// Returns an error if the requested environment variable contains invalid
113/// UTF-8 or has a value that cannot be parsed into the target type.
114pub 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
126/// Like [`parse`](crate::parse), but falls back to a default value when
127/// the environment variable is missing or empty.
128///
129/// The target type must implement [`Default`](Default).
130///
131/// # Errors
132/// Returns an error if the requested environment variable contains invalid
133/// UTF-8 or has a value that cannot be parsed into the target type.
134pub 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)]
142/// Error type for this library.
143pub enum Error {
144    /// The requested environment variable was missing or empty.
145    #[error("Missing or empty environment variable {name:?}")]
146    Missing {
147        /// Name of the requested environment variable.
148        name: String,
149    },
150    #[error("Invalid UTF-8 in environment variable {name:?}")]
151    /// The environment variable exists, but its value is not valid
152    /// UTF-8.
153    InvalidUtf8 {
154        /// Name of the requested environment variable.
155        name: String,
156        /// Value of the environment variable.
157        value: OsString,
158    },
159    #[error("Error parsing environment variable {name:?}: {source}")]
160    /// The environment variable exists and is valid UTF-8, but it
161    /// could not be parsed into the target type.
162    InvalidValue {
163        /// Name of the requested environment variable.
164        name: String,
165        /// Value of the environment variable.
166        value: String,
167        #[source]
168        /// The underlying error that occurred during parsing.
169        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    // utils
384
385    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}