Skip to main content

use_env_var/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, marker::PhantomData, str::FromStr};
5use std::env;
6
7/// Commonly used environment variable primitives.
8pub mod prelude {
9    pub use crate::{
10        EnvVarName, EnvVarNameError, EnvVarReadError, EnvVarValue, TypedEnvVar, TypedEnvVarError,
11        is_valid_env_var_name, read_env_var, read_optional_env_var,
12    };
13}
14
15/// Validation errors for environment variable names.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum EnvVarNameError {
18    /// The variable name was empty.
19    Empty,
20    /// The variable name started with an ASCII digit.
21    StartsWithDigit,
22    /// The variable name contained an unsupported character.
23    InvalidCharacter,
24}
25
26impl fmt::Display for EnvVarNameError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("environment variable name cannot be empty"),
30            Self::StartsWithDigit => {
31                formatter.write_str("environment variable name cannot start with a digit")
32            },
33            Self::InvalidCharacter => formatter.write_str(
34                "environment variable name must use ASCII letters, digits, and underscores",
35            ),
36        }
37    }
38}
39
40impl std::error::Error for EnvVarNameError {}
41
42/// An owned, validated environment variable name.
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44pub struct EnvVarName {
45    name: String,
46}
47
48impl EnvVarName {
49    /// Creates a validated environment variable name.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`EnvVarNameError`] when `name` is empty or contains unsupported characters.
54    pub fn new(name: impl Into<String>) -> Result<Self, EnvVarNameError> {
55        let name = name.into();
56        validate_env_var_name(&name)?;
57        Ok(Self { name })
58    }
59
60    /// Returns the environment variable name.
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        &self.name
64    }
65}
66
67impl AsRef<str> for EnvVarName {
68    fn as_ref(&self) -> &str {
69        self.as_str()
70    }
71}
72
73impl fmt::Display for EnvVarName {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        formatter.write_str(&self.name)
76    }
77}
78
79/// An owned environment variable value.
80#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
81pub struct EnvVarValue {
82    value: String,
83}
84
85impl EnvVarValue {
86    /// Creates an owned environment variable value.
87    #[must_use]
88    pub fn new(value: impl Into<String>) -> Self {
89        Self {
90            value: value.into(),
91        }
92    }
93
94    /// Returns the borrowed value.
95    #[must_use]
96    pub fn as_str(&self) -> &str {
97        &self.value
98    }
99
100    /// Returns the owned value.
101    #[must_use]
102    pub fn into_string(self) -> String {
103        self.value
104    }
105}
106
107impl AsRef<str> for EnvVarValue {
108    fn as_ref(&self) -> &str {
109        self.as_str()
110    }
111}
112
113impl From<String> for EnvVarValue {
114    fn from(value: String) -> Self {
115        Self::new(value)
116    }
117}
118
119impl From<&str> for EnvVarValue {
120    fn from(value: &str) -> Self {
121        Self::new(value)
122    }
123}
124
125impl fmt::Display for EnvVarValue {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter.write_str(&self.value)
128    }
129}
130
131/// Errors returned while reading environment variables.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum EnvVarReadError {
134    /// The environment variable was not present.
135    NotPresent { name: String },
136    /// The environment variable value was not valid Unicode.
137    NotUnicode { name: String },
138}
139
140impl fmt::Display for EnvVarReadError {
141    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            Self::NotPresent { name } => {
144                write!(formatter, "environment variable {name} is not set")
145            },
146            Self::NotUnicode { name } => {
147                write!(
148                    formatter,
149                    "environment variable {name} is not valid Unicode"
150                )
151            },
152        }
153    }
154}
155
156impl std::error::Error for EnvVarReadError {}
157
158/// A typed environment variable wrapper with caller-provided parsing.
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct TypedEnvVar<T> {
161    name: EnvVarName,
162    marker: PhantomData<fn() -> T>,
163}
164
165impl<T> TypedEnvVar<T> {
166    /// Creates a typed environment variable wrapper.
167    #[must_use]
168    pub const fn new(name: EnvVarName) -> Self {
169        Self {
170            name,
171            marker: PhantomData,
172        }
173    }
174
175    /// Returns the underlying environment variable name.
176    #[must_use]
177    pub const fn name(&self) -> &EnvVarName {
178        &self.name
179    }
180
181    /// Reads and parses the environment variable with a caller-provided parser.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`TypedEnvVarError::Read`] when the variable cannot be read and
186    /// [`TypedEnvVarError::Parse`] when the parser rejects the value.
187    pub fn read_with<E>(
188        &self,
189        parser: impl FnOnce(&str) -> Result<T, E>,
190    ) -> Result<T, TypedEnvVarError<E>> {
191        let value = read_env_var(&self.name).map_err(TypedEnvVarError::Read)?;
192        parser(value.as_str()).map_err(TypedEnvVarError::Parse)
193    }
194
195    /// Reads and parses the environment variable with [`FromStr`].
196    ///
197    /// # Errors
198    ///
199    /// Returns [`TypedEnvVarError::Read`] when the variable cannot be read and
200    /// [`TypedEnvVarError::Parse`] when `T::from_str` rejects the value.
201    pub fn read_parse(&self) -> Result<T, TypedEnvVarError<T::Err>>
202    where
203        T: FromStr,
204    {
205        self.read_with(str::parse)
206    }
207}
208
209/// Errors returned while reading a typed environment variable.
210#[derive(Clone, Debug, PartialEq, Eq)]
211pub enum TypedEnvVarError<E> {
212    /// Reading the environment variable failed.
213    Read(EnvVarReadError),
214    /// Parsing the environment variable value failed.
215    Parse(E),
216}
217
218impl<E: fmt::Display> fmt::Display for TypedEnvVarError<E> {
219    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::Read(error) => write!(formatter, "{error}"),
222            Self::Parse(error) => write!(formatter, "environment variable parse failed: {error}"),
223        }
224    }
225}
226
227impl<E> std::error::Error for TypedEnvVarError<E> where E: fmt::Debug + fmt::Display {}
228
229/// Returns whether `name` is valid for this crate's environment variable primitive.
230#[must_use]
231pub fn is_valid_env_var_name(name: &str) -> bool {
232    validate_env_var_name(name).is_ok()
233}
234
235/// Reads a present Unicode environment variable.
236///
237/// # Errors
238///
239/// Returns [`EnvVarReadError::NotPresent`] when the variable is missing and
240/// [`EnvVarReadError::NotUnicode`] when the value is not valid Unicode.
241pub fn read_env_var(name: &EnvVarName) -> Result<EnvVarValue, EnvVarReadError> {
242    env::var(name.as_str())
243        .map(EnvVarValue::new)
244        .map_err(|error| match error {
245            env::VarError::NotPresent => EnvVarReadError::NotPresent {
246                name: name.as_str().to_owned(),
247            },
248            env::VarError::NotUnicode(_) => EnvVarReadError::NotUnicode {
249                name: name.as_str().to_owned(),
250            },
251        })
252}
253
254/// Reads an optional Unicode environment variable.
255///
256/// # Errors
257///
258/// Returns [`EnvVarReadError::NotUnicode`] when the variable exists but is not valid Unicode.
259pub fn read_optional_env_var(name: &EnvVarName) -> Result<Option<EnvVarValue>, EnvVarReadError> {
260    match read_env_var(name) {
261        Ok(value) => Ok(Some(value)),
262        Err(EnvVarReadError::NotPresent { .. }) => Ok(None),
263        Err(error) => Err(error),
264    }
265}
266
267fn validate_env_var_name(name: &str) -> Result<(), EnvVarNameError> {
268    let bytes = name.as_bytes();
269    if bytes.is_empty() {
270        return Err(EnvVarNameError::Empty);
271    }
272
273    if bytes[0].is_ascii_digit() {
274        return Err(EnvVarNameError::StartsWithDigit);
275    }
276
277    if bytes
278        .iter()
279        .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
280    {
281        Ok(())
282    } else {
283        Err(EnvVarNameError::InvalidCharacter)
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::{
290        EnvVarName, EnvVarNameError, EnvVarValue, TypedEnvVar, is_valid_env_var_name,
291        read_optional_env_var,
292    };
293
294    #[test]
295    fn validates_env_var_names() {
296        assert!(is_valid_env_var_name("RUST_LOG"));
297        assert!(is_valid_env_var_name("_RUSTUSE"));
298        assert_eq!(EnvVarName::new(""), Err(EnvVarNameError::Empty));
299        assert_eq!(
300            EnvVarName::new("1RUST"),
301            Err(EnvVarNameError::StartsWithDigit)
302        );
303        assert_eq!(
304            EnvVarName::new("RUST-LOG"),
305            Err(EnvVarNameError::InvalidCharacter)
306        );
307    }
308
309    #[test]
310    fn stores_owned_values_and_typed_names() -> Result<(), EnvVarNameError> {
311        let name = EnvVarName::new("RUSTUSE_EXAMPLE")?;
312        let typed = TypedEnvVar::<u16>::new(name.clone());
313        let value = EnvVarValue::new("42");
314
315        assert_eq!(typed.name(), &name);
316        assert_eq!(value.as_str(), "42");
317        Ok(())
318    }
319
320    #[test]
321    fn optional_read_reports_missing_as_none() -> Result<(), Box<dyn std::error::Error>> {
322        let name = EnvVarName::new("RUSTUSE_USE_CLI_TEST_SHOULD_NOT_EXIST_9B6AE5E0")?;
323
324        assert_eq!(read_optional_env_var(&name)?, None);
325        Ok(())
326    }
327}