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