confroid 0.0.1

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 exists; 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: e.to_string(),
                            }
                        }),
                    }
                }

                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::net::IpAddr,
    std::net::Ipv4Addr,
    std::net::Ipv6Addr,
    std::net::SocketAddr,
    std::path::PathBuf,
);

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

    fn is_present(ctx: &Ctx, env: &Env) -> bool {
        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());
        for seg in &segments {
            match seg.parse::<usize>() {
                Ok(i) => indices.push(i),
                Err(_) => {
                    return Err(ConfroidError::EnvVarInvalid {
                        var_name: format!("{}__{}", ctx.var, seg),
                        field: ctx.path.clone(),
                        value: seg.clone(),
                        parser_error: "expected a numeric vector index".to_string(),
                    });
                }
            }
        }
        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());
        for i in 0..indices.len() {
            out.push(T::from_env(&ctx.index(i), env)?);
        }
        Ok(out)
    }

    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());
        for key in &keys {
            map.insert(key.clone(), T::from_env(&ctx.key(key), env)?);
        }
        Ok(map)
    }

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