init4_bin_base/utils/from_env.rs
1use std::{convert::Infallible, env::VarError, num::ParseIntError, str::FromStr};
2
3/// The `derive(FromEnv)` macro.
4///
5/// This macro generates a [`FromEnv`] implementation for the struct it is
6/// applied to. It will generate a `from_env` function that loads the struct
7/// from the environment. It will also generate an `inventory` function that
8/// returns a list of all environment variables that are required to load the
9/// struct.
10///
11/// The macro also generates a `__EnvError` type that captures errors that can
12/// occur when trying to create an instance of the struct from environment
13/// variables. This error type is used in the `FromEnv` trait implementation.
14///
15/// ## Conditions of use
16///
17/// There are a few usage requirements:
18///
19/// - Struct props MUST implement either [`FromEnvVar`] or [`FromEnv`].
20/// - If the prop implements [`FromEnvVar`], it must be tagged as follows:
21/// - `var = "ENV_VAR_NAME"`: The environment variable name to load.
22/// - `desc = "description"`: A description of the environment variable.
23/// - If the prop is an [`Option<T>`], it must be tagged as follows:
24/// - `optional`
25/// - If the prop's associated error type is [`Infallible`], it must be tagged
26/// as follows:
27/// - `infallible`
28/// - If used within this crate (`init4_bin_base`), the entire struct must be
29/// tagged with `#[from_env(crate)]` (see the [`SlotCalculator`] for an
30/// example).
31///
32/// # Examples
33///
34/// The following example shows how to use the macro:
35///
36/// ```
37/// # // I am unsure why we need this, as identical code works in
38/// # // integration tests. However, compile test fails without it.
39/// # #![allow(proc_macro_derive_resolution_fallback)]
40/// use init4_bin_base::utils::from_env::{FromEnv};
41///
42/// #[derive(Debug, FromEnv)]
43/// pub struct MyCfg {
44/// #[from_env(var = "COOL_DUDE", desc = "Some u8 we like :o)")]
45/// pub my_cool_u8: u8,
46///
47/// #[from_env(var = "CHUCK", desc = "Charles is a u64")]
48/// pub charles: u64,
49///
50/// #[from_env(
51/// var = "PERFECT",
52/// desc = "A bold and neat string",
53/// infallible,
54/// )]
55/// pub strings_cannot_fail: String,
56///
57/// #[from_env(
58/// var = "MAYBE_NOT_NEEDED",
59/// desc = "This is an optional string",
60/// optional,
61/// infallible,
62/// )]
63/// maybe_not_needed: Option<String>,
64/// }
65///
66/// // The `FromEnv` trait is implemented for the struct, and the struct can
67/// // be loaded from the environment.
68/// # fn use_it() {
69/// if let Err(missing) = MyCfg::check_inventory() {
70/// println!("Missing environment variables:");
71/// for var in missing {
72/// println!("{}: {}", var.var, var.description);
73/// }
74/// }
75/// # }
76/// ```
77///
78/// This will generate a `FromEnv` implementation for the struct, and a
79/// `MyCfgEnvError` type that is used to represent errors that can occur when
80/// loading from the environment. The error generated will look like this:
81///
82/// ```ignore
83/// pub enum MyCfgEnvError {
84/// MyCoolU8(<u8 as FromEnvVar>::Error),
85/// Charles(<u64 as FromEnvVar>::Error),
86/// // No variants for infallible errors.
87/// }
88/// ```
89///
90/// [`Infallible`]: std::convert::Infallible
91/// [`SlotCalculator`]: crate::utils::SlotCalculator
92pub use init4_from_env_derive::FromEnv;
93
94/// Details about an environment variable. This is used to generate
95/// documentation for the environment variables and by the [`FromEnv`] trait to
96/// check if necessary environment variables are present.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct EnvItemInfo {
99 /// The environment variable name.
100 pub var: &'static str,
101 /// A description of the environment variable function in the CFG.
102 pub description: &'static str,
103 /// Whether the environment variable is optional or not.
104 pub optional: bool,
105}
106
107/// Error type for loading from the environment. See the [`FromEnv`] trait for
108/// more information.
109#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
110pub enum FromEnvErr<Inner> {
111 /// The environment variable is missing.
112 #[error("error reading variable {0}: {1}")]
113 EnvError(String, VarError),
114 /// The environment variable is empty.
115 #[error("environment variable {0} is empty")]
116 Empty(String),
117 /// The environment variable is present, but the value could not be parsed.
118 #[error("failed to parse environment variable {0}")]
119 ParseError(#[from] Inner),
120}
121
122impl FromEnvErr<Infallible> {
123 /// Convert the error into another error type.
124 pub fn infallible_into<T>(self) -> FromEnvErr<T> {
125 match self {
126 Self::EnvError(s, e) => FromEnvErr::EnvError(s, e),
127 Self::Empty(s) => FromEnvErr::Empty(s),
128 Self::ParseError(_) => unreachable!(),
129 }
130 }
131}
132
133impl<Inner> FromEnvErr<Inner> {
134 /// Create a new error from another error type.
135 pub fn from<Other>(other: FromEnvErr<Other>) -> Self
136 where
137 Inner: From<Other>,
138 {
139 match other {
140 FromEnvErr::EnvError(s, e) => Self::EnvError(s, e),
141 FromEnvErr::Empty(s) => Self::Empty(s),
142 FromEnvErr::ParseError(e) => Self::ParseError(Inner::from(e)),
143 }
144 }
145
146 /// Map the error to another type. This is useful for converting the error
147 /// type to a different type, while keeping the other error information
148 /// intact.
149 pub fn map<New>(self, f: impl FnOnce(Inner) -> New) -> FromEnvErr<New> {
150 match self {
151 Self::EnvError(s, e) => FromEnvErr::EnvError(s, e),
152 Self::Empty(s) => FromEnvErr::Empty(s),
153 Self::ParseError(e) => FromEnvErr::ParseError(f(e)),
154 }
155 }
156
157 /// Missing env var.
158 pub fn env_err(var: &str, e: VarError) -> Self {
159 Self::EnvError(var.to_string(), e)
160 }
161
162 /// Empty env var.
163 pub fn empty(var: &str) -> Self {
164 Self::Empty(var.to_string())
165 }
166
167 /// Error while parsing.
168 pub const fn parse_error(err: Inner) -> Self {
169 Self::ParseError(err)
170 }
171}
172
173/// Convenience function for parsing a value from the environment, if present
174/// and non-empty.
175pub fn parse_env_if_present<T: FromStr>(env_var: &str) -> Result<T, FromEnvErr<T::Err>> {
176 let s = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
177
178 if s.is_empty() {
179 Err(FromEnvErr::empty(env_var))
180 } else {
181 s.parse().map_err(Into::into)
182 }
183}
184
185/// Trait for loading from the environment.
186///
187/// This trait is for structs or other complex objects, that need to be loaded
188/// from the environment. It expects that
189///
190/// - The struct is [`Sized`] and `'static`.
191/// - The struct elements can be parsed from strings.
192/// - Struct elements are at fixed env vars, known by the type at compile time.
193///
194/// As such, unless the env is modified, these are essentially static runtime
195/// values. We do not recommend using dynamic env vars.
196///
197/// ## [`FromEnv`] vs [`FromEnvVar`]
198///
199/// While [`FromEnvVar`] deals with loading simple types from the environment,
200/// [`FromEnv`] is for loading complex types. It builds a struct from the
201/// environment, usually be delegating each field to a [`FromEnvVar`] or
202/// [`FromEnv`] implementation. [`FromEnv`] effectively defines a singleton
203/// configuration object, which is produced by loading many env vars, while
204/// [`FromEnvVar`] defines a procedure for loading data from a single
205/// environment variable.
206///
207/// ## Implementing [`FromEnv`]
208///
209/// Please use the [`FromEnv`](macro@FromEnv) derive macro to implement this
210/// trait.
211///
212/// ## Note on error types
213///
214/// [`FromEnv`] and [`FromEnvVar`] are often deeply nested. This means that
215/// error types are often nested as well. To avoid this, we use a single error
216/// type [`FromEnvVar`] that wraps an inner error type. This allows us to
217/// ensure that env-related errors (e.g. missing env vars) are not lost in the
218/// recursive structure of parsing errors. Environment errors are always at the
219/// top level, and should never be nested. **Do not use [`FromEnvErr<T>`] as
220/// the `Error` associated type in [`FromEnv`].**
221///
222/// ```no_compile
223/// // Do not do this
224/// impl FromEnv for MyType {
225/// type Error = FromEnvErr<MyTypeErr>;
226/// }
227///
228/// // Instead do this:
229/// impl FromEnv for MyType {
230/// type Error = MyTypeErr;
231/// }
232/// ```
233///
234pub trait FromEnv: core::fmt::Debug + Sized + 'static {
235 /// Error type produced when loading from the environment.
236 type Error: core::error::Error + Clone;
237
238 /// Get the required environment variable names for this type.
239 ///
240 /// ## Note
241 ///
242 /// This MUST include the environment variable names for all fields in the
243 /// struct, including optional vars.
244 fn inventory() -> Vec<&'static EnvItemInfo>;
245
246 /// Get a list of missing environment variables.
247 ///
248 /// This will check all environment variables in the inventory, and return
249 /// a list of those that are non-optional and missing. This is useful for
250 /// reporting missing environment variables.
251 fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
252 let mut missing = Vec::new();
253 for var in Self::inventory() {
254 if std::env::var(var.var).is_err() && !var.optional {
255 missing.push(var);
256 }
257 }
258 if missing.is_empty() {
259 Ok(())
260 } else {
261 Err(missing)
262 }
263 }
264
265 /// Load from the environment.
266 fn from_env() -> Result<Self, FromEnvErr<Self::Error>>;
267}
268
269/// Trait for loading primitives from the environment. These are simple types
270/// that should correspond to a single environment variable. It has been
271/// implemented for common integer types, [`String`], [`url::Url`],
272/// [`tracing::Level`], and [`std::time::Duration`].
273///
274/// It aims to make [`FromEnv`] implementations easier to write, by providing a
275/// default implementation for common types.
276///
277/// ## Note on error types
278///
279/// [`FromEnv`] and [`FromEnvVar`] are often deeply nested. This means that
280/// error types are often nested as well. To avoid this, we use a single error
281/// type [`FromEnvVar`] that wraps an inner error type. This allows us to
282/// ensure that env-related errors (e.g. missing env vars) are not lost in the
283/// recursive structure of parsing errors. Environment errors are always at the
284/// top level, and should never be nested. **Do not use [`FromEnvErr<T>`] as
285/// the `Error` associated type in [`FromEnv`].**
286///
287/// ```no_compile
288/// // Do not do this
289/// impl FromEnv for MyType {
290/// type Error = FromEnvErr<MyTypeErr>;
291/// }
292///
293/// // Instead do this:
294/// impl FromEnv for MyType {
295/// type Error = MyTypeErr;
296/// }
297/// ```
298///
299/// ## Implementing [`FromEnv`]
300///
301/// [`FromEnvVar`] is a trait for loading simple types from the environment. It
302/// represents a type that can be loaded from a single environment variable. It
303/// is similar to [`FromStr`] and will usually be using an existing [`FromStr`]
304/// impl.
305///
306/// ```
307/// # use init4_bin_base::utils::from_env::{FromEnvVar, FromEnvErr};
308/// # use std::str::FromStr;
309/// # #[derive(Debug)]
310/// # pub struct MyCoolType;
311/// # impl std::str::FromStr for MyCoolType {
312/// # type Err = std::convert::Infallible;
313/// # fn from_str(s: &str) -> Result<Self, Self::Err> {
314/// # Ok(MyCoolType)
315/// # }
316/// # }
317///
318/// // We can re-use the `FromStr` implementation for our `FromEnvVar` impl.
319/// impl FromEnvVar for MyCoolType {
320/// type Error = <MyCoolType as FromStr>::Err;
321///
322/// fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>>
323/// {
324/// String::from_env_var(env_var).unwrap().parse().map_err(Into::into)
325/// }
326/// }
327/// ```
328pub trait FromEnvVar: core::fmt::Debug + Sized + 'static {
329 /// Error type produced when parsing the primitive.
330 type Error: core::error::Error;
331
332 /// Load the primitive from the environment at the given variable.
333 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>>;
334}
335
336impl<T> FromEnvVar for Option<T>
337where
338 T: FromEnvVar,
339{
340 type Error = T::Error;
341
342 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
343 match std::env::var(env_var) {
344 Ok(s) if s.is_empty() => Ok(None),
345 Ok(_) => T::from_env_var(env_var).map(Some),
346 Err(_) => Ok(None),
347 }
348 }
349}
350
351impl FromEnvVar for String {
352 type Error = std::convert::Infallible;
353
354 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
355 std::env::var(env_var).map_err(|_| FromEnvErr::empty(env_var))
356 }
357}
358
359impl FromEnvVar for std::time::Duration {
360 type Error = ParseIntError;
361
362 fn from_env_var(s: &str) -> Result<Self, FromEnvErr<Self::Error>> {
363 u64::from_env_var(s).map(Self::from_millis)
364 }
365}
366
367impl<T> FromEnvVar for Vec<T>
368where
369 T: From<String> + core::fmt::Debug + 'static,
370{
371 type Error = Infallible;
372
373 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
374 let s = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
375 if s.is_empty() {
376 return Ok(vec![]);
377 }
378 Ok(s.split(',')
379 .map(str::to_string)
380 .map(Into::into)
381 .collect::<Vec<_>>())
382 }
383}
384
385macro_rules! impl_for_parseable {
386 ($($t:ty),*) => {
387 $(
388 impl FromEnvVar for $t {
389 type Error = <$t as FromStr>::Err;
390
391 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
392 parse_env_if_present(env_var)
393 }
394 }
395 )*
396 }
397}
398
399impl_for_parseable!(
400 u8,
401 u16,
402 u32,
403 u64,
404 u128,
405 usize,
406 i8,
407 i16,
408 i32,
409 i64,
410 i128,
411 isize,
412 url::Url,
413 tracing::Level
414);
415
416#[cfg(feature = "alloy")]
417impl_for_parseable!(
418 alloy::primitives::Address,
419 alloy::primitives::Bytes,
420 alloy::primitives::U256
421);
422
423#[cfg(feature = "alloy")]
424impl<const N: usize> FromEnvVar for alloy::primitives::FixedBytes<N> {
425 type Error = <alloy::primitives::FixedBytes<N> as FromStr>::Err;
426
427 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
428 parse_env_if_present(env_var)
429 }
430}
431
432impl FromEnvVar for bool {
433 type Error = std::str::ParseBoolError;
434
435 fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
436 let s: String = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
437 Ok(!s.is_empty())
438 }
439}
440
441#[cfg(test)]
442mod test {
443 use std::time::Duration;
444
445 use super::*;
446
447 fn set<T>(env: &str, val: &T)
448 where
449 T: ToString,
450 {
451 std::env::set_var(env, val.to_string());
452 }
453
454 fn load_expect_err<T>(env: &str, err: FromEnvErr<T::Error>)
455 where
456 T: FromEnvVar,
457 T::Error: PartialEq,
458 {
459 let res = T::from_env_var(env).unwrap_err();
460 assert_eq!(res, err);
461 }
462
463 fn test<T>(env: &str, val: T)
464 where
465 T: ToString + FromEnvVar + PartialEq + std::fmt::Debug,
466 {
467 set(env, &val);
468
469 let res = T::from_env_var(env).unwrap();
470 assert_eq!(res, val);
471 }
472
473 fn test_expect_err<T, U>(env: &str, value: U, err: FromEnvErr<T::Error>)
474 where
475 T: FromEnvVar,
476 U: ToString,
477 T::Error: PartialEq,
478 {
479 set(env, &value);
480 load_expect_err::<T>(env, err);
481 }
482
483 #[test]
484 fn test_primitives() {
485 test("U8", 42u8);
486 test("U16", 42u16);
487 test("U32", 42u32);
488 test("U64", 42u64);
489 test("U128", 42u128);
490 test("Usize", 42usize);
491 test("I8", 42i8);
492 test("I8-NEG", -42i16);
493 test("I16", 42i16);
494 test("I32", 42i32);
495 test("I64", 42i64);
496 test("I128", 42i128);
497 test("Isize", 42isize);
498 test("String", "hello".to_string());
499 test("Url", url::Url::parse("http://example.com").unwrap());
500 test("Level", tracing::Level::INFO);
501 }
502
503 #[test]
504 fn test_duration() {
505 let amnt = 42;
506 let val = Duration::from_millis(42);
507
508 set("Duration", &amnt);
509 let res = Duration::from_env_var("Duration").unwrap();
510
511 assert_eq!(res, val);
512 }
513
514 #[test]
515 fn test_a_few_errors() {
516 test_expect_err::<u8, _>(
517 "U8_",
518 30000u16,
519 FromEnvErr::parse_error("30000".parse::<u8>().unwrap_err()),
520 );
521
522 test_expect_err::<u8, _>("U8_", "", FromEnvErr::empty("U8_"));
523 }
524}