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}