1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, marker::PhantomData, str::FromStr};
5use std::env;
6
7pub 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#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum EnvVarNameError {
18 Empty,
20 StartsWithDigit,
22 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44pub struct EnvVarName {
45 name: String,
46}
47
48impl EnvVarName {
49 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 #[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#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
81pub struct EnvVarValue {
82 value: String,
83}
84
85impl EnvVarValue {
86 #[must_use]
88 pub fn new(value: impl Into<String>) -> Self {
89 Self {
90 value: value.into(),
91 }
92 }
93
94 #[must_use]
96 pub fn as_str(&self) -> &str {
97 &self.value
98 }
99
100 #[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#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum EnvVarReadError {
134 NotPresent { name: String },
136 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#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct TypedEnvVar<T> {
161 name: EnvVarName,
162 marker: PhantomData<fn() -> T>,
163}
164
165impl<T> TypedEnvVar<T> {
166 #[must_use]
168 pub const fn new(name: EnvVarName) -> Self {
169 Self {
170 name,
171 marker: PhantomData,
172 }
173 }
174
175 #[must_use]
177 pub const fn name(&self) -> &EnvVarName {
178 &self.name
179 }
180
181 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 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#[derive(Clone, Debug, PartialEq, Eq)]
211pub enum TypedEnvVarError<E> {
212 Read(EnvVarReadError),
214 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#[must_use]
231pub fn is_valid_env_var_name(name: &str) -> bool {
232 validate_env_var_name(name).is_ok()
233}
234
235pub 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
254pub 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}