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}