confroid 0.0.4

The n+1-st config reader for your environment-based configs.
Documentation
//! Error types produced while reading configuration from the environment.

use std::error::Error;
use std::fmt;

/// A boxed parser error retained as the source of an invalid-value error.
pub type ParserError = Box<dyn Error + Send + Sync + 'static>;

/// The error type returned by [`from_env`](crate::from_env) and the
/// [`FromEnv`](crate::FromEnv) trait.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfroidError {
    /// Multiple independent configuration errors collected in one pass.
    ///
    /// Errors are ordered by struct field declaration, vector index, and map
    /// key. Nested aggregates are flattened.
    #[error("configuration contains multiple errors:\n{errors}", errors = ErrorList(.errors))]
    Multiple {
        /// The individual configuration errors.
        errors: Vec<ConfroidError>,
    },

    /// A required environment variable was not set.
    ///
    /// For a scalar field this means the exact variable is missing. For a
    /// collection (`Vec`/`HashMap`) it means none of its indexed/keyed
    /// variables were present and the field has no default.
    #[error("environment variable `{var_name}` (field `{field}`) is not set")]
    EnvVarNotFound {
        /// The environment variable name, e.g. `HTTP__PORT`.
        var_name: String,
        /// The dotted field path, e.g. `http.port`.
        field: String,
    },

    /// An environment variable was set but its value could not be parsed into
    /// the target type.
    #[error(
        "environment variable `{var_name}` (field `{field}`) has invalid value `{value}`: {parser_error}"
    )]
    EnvVarInvalid {
        /// The environment variable name, e.g. `HTTP__PORT`.
        var_name: String,
        /// The dotted field path, e.g. `http.port`.
        field: String,
        /// The raw value that failed to parse.
        value: String,
        /// The underlying parser error.
        #[source]
        parser_error: ParserError,
    },

    /// A `Vec` field had a non-numeric index.
    #[error(
        "vector `{field}` has invalid index `{index}` in variable `{var_name}`; expected a number"
    )]
    VecIndexInvalid {
        /// The full malformed environment variable name.
        var_name: String,
        /// The dotted field path, e.g. `names`.
        field: String,
        /// The invalid index segment.
        index: String,
    },

    /// A `Vec` field had non-contiguous indices (e.g. `FOO__0` and `FOO__2`
    /// with no `FOO__1`).
    #[error(
        "vector `{field}` (var `{var_name}`) is missing index {missing}; indices must be contiguous starting at 0"
    )]
    VecIndexGap {
        /// The environment variable prefix, e.g. `NAMES`.
        var_name: String,
        /// The dotted field path, e.g. `names`.
        field: String,
        /// The first missing index.
        missing: usize,
    },
}

impl ConfroidError {
    /// Combine one or more errors, flattening nested aggregates.
    ///
    /// A single error is returned directly; multiple errors are wrapped in
    /// [`ConfroidError::Multiple`].
    #[doc(hidden)]
    pub fn from_errors(errors: Vec<Self>) -> Self {
        let mut flattened = Vec::new();
        for error in errors {
            flattened.extend(error.into_errors());
        }

        match flattened.len() {
            0 => panic!("ConfroidError::from_errors requires at least one error"),
            1 => flattened.pop().expect("length was checked"),
            _ => Self::Multiple { errors: flattened },
        }
    }

    /// Return the individual errors represented by this value.
    ///
    /// Leaf errors produce a one-element slice. Aggregates expose their
    /// flattened children.
    pub fn errors(&self) -> &[Self] {
        match self {
            Self::Multiple { errors } => errors,
            error => std::slice::from_ref(error),
        }
    }

    /// Consume this value and return its individual errors.
    #[doc(hidden)]
    pub fn into_errors(self) -> Vec<Self> {
        match self {
            Self::Multiple { errors } => errors,
            error => vec![error],
        }
    }
}

struct ErrorList<'a>(&'a [ConfroidError]);

impl fmt::Display for ErrorList<'_> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (index, error) in self.0.iter().enumerate() {
            if index > 0 {
                formatter.write_str("\n")?;
            }
            write!(formatter, "- {error}")?;
        }
        Ok(())
    }
}

/// Convenient result alias.
pub type Result<T> = core::result::Result<T, ConfroidError>;