confroid 0.0.4

The n+1-st config reader for your environment-based configs.
Documentation
//! The [`FromEnv`] trait and its built-in implementations for scalars,
//! `Option`, `Vec` and `HashMap`.

use std::collections::HashMap;
use std::hash::BuildHasher;

use crate::env::{Ctx, Env};
use crate::error::{ConfroidError, Result};

/// A type that can be read from the environment at a given [`Ctx`].
///
/// This is the single dispatch point that lets confroid decide behaviour from
/// a field's *type* rather than from attributes: scalars read one variable,
/// structs read their fields under a prefix, and the collection wrappers below
/// build on those. Users rarely implement this by hand — `#[derive(Config)]`
/// generates it for structs — but manual impls are the escape hatch for custom
/// scalar types.
pub trait FromEnv: Sized {
    /// Read `Self` from `env` using the names in `ctx`.
    fn from_env(ctx: &Ctx, env: &Env) -> Result<Self>;

    /// Whether any environment variable relevant to this value is present.
    ///
    /// This is what makes optionality uniform: `Option<T>` is `None` exactly
    /// when `T::is_present` is `false`. For a scalar that means the exact
    /// variable has a non-empty value; for a struct/collection it means at
    /// least one child variable exists (so a *partially* specified struct is still "present"
    /// and therefore surfaces a missing-field error rather than silently
    /// becoming `None`).
    fn is_present(ctx: &Ctx, env: &Env) -> bool;
}

/// Implement [`FromEnv`] for a scalar type via its [`FromStr`](std::str::FromStr)
/// impl. A blanket impl over `FromStr` is impossible (it would overlap with the
/// derived struct impls), so scalars are enumerated explicitly.
macro_rules! impl_scalar {
    ($($ty:ty),+ $(,)?) => {
        $(
            impl FromEnv for $ty {
                fn from_env(ctx: &Ctx, env: &Env) -> Result<Self> {
                    match env.get(&ctx.var) {
                        None => Err(ConfroidError::EnvVarNotFound {
                            var_name: ctx.var.clone(),
                            field: ctx.path.clone(),
                        }),
                        Some(raw) => raw.parse::<$ty>().map_err(|e| {
                            ConfroidError::EnvVarInvalid {
                                var_name: ctx.var.clone(),
                                field: ctx.path.clone(),
                                value: raw.to_string(),
                                parser_error: Box::new(e),
                            }
                        }),
                    }
                }

                fn is_present(ctx: &Ctx, env: &Env) -> bool {
                    env.get(&ctx.var).is_some()
                }
            }
        )+
    };
}

impl_scalar!(
    u8,
    u16,
    u32,
    u64,
    u128,
    usize,
    i8,
    i16,
    i32,
    i64,
    i128,
    isize,
    f32,
    f64,
    bool,
    char,
    String,
    std::num::NonZeroU16,
    std::net::IpAddr,
    std::net::Ipv4Addr,
    std::net::Ipv6Addr,
    std::net::SocketAddr,
    std::net::SocketAddrV4,
    std::net::SocketAddrV6,
    std::path::PathBuf,
);

macro_rules! impl_string_scalar {
    ($ty:ty, $convert:expr) => {
        impl FromEnv for $ty {
            fn from_env(ctx: &Ctx, env: &Env) -> Result<Self> {
                env.get(&ctx.var)
                    .map($convert)
                    .ok_or_else(|| ConfroidError::EnvVarNotFound {
                        var_name: ctx.var.clone(),
                        field: ctx.path.clone(),
                    })
            }

            fn is_present(ctx: &Ctx, env: &Env) -> bool {
                env.get(&ctx.var).is_some()
            }
        }
    };
}

impl_string_scalar!(Box<str>, |raw: &str| Box::<str>::from(raw));
impl_string_scalar!(std::sync::Arc<str>, |raw: &str| {
    std::sync::Arc::<str>::from(raw)
});

#[cfg(feature = "url")]
impl_scalar!(url::Url);

#[cfg(feature = "humantime")]
#[doc(hidden)]
pub fn parse_humantime_duration(ctx: &Ctx, env: &Env) -> Result<std::time::Duration> {
    match env.get(&ctx.var) {
        None => Err(ConfroidError::EnvVarNotFound {
            var_name: ctx.var.clone(),
            field: ctx.path.clone(),
        }),
        Some(raw) => humantime::parse_duration(raw).map_err(|error| ConfroidError::EnvVarInvalid {
            var_name: ctx.var.clone(),
            field: ctx.path.clone(),
            value: raw.to_string(),
            parser_error: Box::new(error),
        }),
    }
}

impl<T: FromEnv> FromEnv for Option<T> {
    fn from_env(ctx: &Ctx, env: &Env) -> Result<Self> {
        if Self::is_present(ctx, env) {
            Ok(Some(T::from_env(ctx, env)?))
        } else {
            Ok(None)
        }
    }

    fn is_present(ctx: &Ctx, env: &Env) -> bool {
        // Deployment tools commonly materialize unset variables as empty
        // strings. Treat an exact empty leaf as absent, but do not let an empty
        // parent marker hide populated children of an optional struct.
        let is_empty_leaf = env.get(&ctx.var).is_some_and(str::is_empty)
            && env.immediate_children(&ctx.var).is_empty();
        !is_empty_leaf && T::is_present(ctx, env)
    }
}

impl<T: FromEnv> FromEnv for Vec<T> {
    fn from_env(ctx: &Ctx, env: &Env) -> Result<Self> {
        let segments = env.immediate_children(&ctx.var);
        if segments.is_empty() {
            return Err(ConfroidError::EnvVarNotFound {
                var_name: ctx.var.clone(),
                field: ctx.path.clone(),
            });
        }

        // Every immediate child of a vector must be a numeric index.
        let mut indices = Vec::with_capacity(segments.len());
        let mut errors = Vec::new();
        for seg in &segments {
            match seg.parse::<usize>() {
                Ok(i) => indices.push(i),
                Err(_) => errors.push(ConfroidError::VecIndexInvalid {
                    var_name: format!("{}__{}", ctx.var, seg),
                    field: ctx.path.clone(),
                    index: seg.clone(),
                }),
            }
        }
        if !errors.is_empty() {
            return Err(ConfroidError::from_errors(errors));
        }
        indices.sort_unstable();

        // Indices must be contiguous starting at 0.
        for (expected, &got) in indices.iter().enumerate() {
            if expected != got {
                return Err(ConfroidError::VecIndexGap {
                    var_name: ctx.var.clone(),
                    field: ctx.path.clone(),
                    missing: expected,
                });
            }
        }

        let mut out = Vec::with_capacity(indices.len());
        let mut errors = Vec::new();
        for i in 0..indices.len() {
            match T::from_env(&ctx.index(i), env) {
                Ok(value) => out.push(value),
                Err(error) => errors.extend(error.into_errors()),
            }
        }
        if errors.is_empty() {
            Ok(out)
        } else {
            Err(ConfroidError::from_errors(errors))
        }
    }

    fn is_present(ctx: &Ctx, env: &Env) -> bool {
        !env.immediate_children(&ctx.var).is_empty()
    }
}

impl<T: FromEnv, S: BuildHasher + Default> FromEnv for HashMap<String, T, S> {
    fn from_env(ctx: &Ctx, env: &Env) -> Result<Self> {
        let keys = env.immediate_children(&ctx.var);
        if keys.is_empty() {
            return Err(ConfroidError::EnvVarNotFound {
                var_name: ctx.var.clone(),
                field: ctx.path.clone(),
            });
        }

        let mut map = HashMap::with_hasher(S::default());
        let mut errors = Vec::new();
        for key in &keys {
            match T::from_env(&ctx.key(key), env) {
                Ok(value) => {
                    map.insert(key.clone(), value);
                }
                Err(error) => errors.extend(error.into_errors()),
            }
        }
        if errors.is_empty() {
            Ok(map)
        } else {
            Err(ConfroidError::from_errors(errors))
        }
    }

    fn is_present(ctx: &Ctx, env: &Env) -> bool {
        !env.immediate_children(&ctx.var).is_empty()
    }
}