ch-config 0.1.1

Typed configuration loading with validation and secret-file support
Documentation
use std::io;
use std::path::PathBuf;

use thiserror::Error;

/// Error returned while resolving or validating configuration.
#[derive(Debug, Error)]
pub enum ConfigError {
    /// A parsed or overridden value violates a configuration invariant.
    #[error("invalid configuration field `{field}`: {message}")]
    Validation {
        /// Stable dotted field path.
        field: &'static str,
        /// Value-independent validation explanation.
        message: &'static str,
    },

    /// An explicit environment file could not be loaded.
    #[error("failed to load environment file `{path}`")]
    Environment {
        /// File that failed to load.
        path: PathBuf,
        /// Sanitized dotenv failure that never retains a parsed line or value.
        #[source]
        source: EnvironmentFileError,
    },

    /// A process variable and its `_FILE` companion were both supplied.
    #[error("configuration variable `{variable}` conflicts with `{variable}_FILE`")]
    VariableConflict {
        /// Base environment-variable name.
        variable: &'static str,
    },

    /// A process variable is not valid Unicode.
    #[error("configuration variable `{variable}` must be valid Unicode")]
    VariableUnicode {
        /// Environment-variable name.
        variable: String,
    },

    /// A configured value is empty or cannot be parsed into its typed field.
    #[error("configuration variable `{variable}` {message}")]
    VariableValue {
        /// Environment-variable name.
        variable: &'static str,
        /// Value-independent explanation.
        message: &'static str,
    },

    /// A file selected through a `_FILE` variable could not be read.
    #[error("failed to read configuration value selected by `{variable}_FILE`")]
    VariableFile {
        /// Base environment-variable name.
        variable: &'static str,
        /// Underlying filesystem error.
        #[source]
        source: io::Error,
    },
}

/// Sanitized cause for an explicitly selected dotenv file.
#[derive(Debug, Error)]
pub enum EnvironmentFileError {
    /// The file could not be read.
    #[error("environment file could not be read")]
    Io(#[source] io::Error),
    /// A dotenv line has invalid syntax at the reported character index.
    #[error("invalid dotenv syntax at character index {index}")]
    Syntax {
        /// Zero-based character index reported within the invalid line.
        index: usize,
    },
    /// A referenced process variable was unavailable or not valid Unicode.
    #[error("referenced environment variable is unavailable")]
    Variable,
    /// A future dotenv error kind was returned by the non-exhaustive API.
    #[error("environment file could not be loaded")]
    Other,
}

impl EnvironmentFileError {
    pub(crate) fn from_dotenv(error: dotenvy::Error) -> Self {
        match error {
            dotenvy::Error::LineParse(_, index) => Self::Syntax { index },
            dotenvy::Error::Io(source) => Self::Io(source),
            dotenvy::Error::EnvVar(_) => Self::Variable,
            _ => Self::Other,
        }
    }
}

impl ConfigError {
    pub(crate) const fn validation(field: &'static str, message: &'static str) -> Self {
        Self::Validation { field, message }
    }

    pub(crate) fn environment(path: PathBuf, error: dotenvy::Error) -> Self {
        Self::Environment {
            path,
            source: EnvironmentFileError::from_dotenv(error),
        }
    }
}

/// Result type used by this crate.
pub type Result<T> = std::result::Result<T, ConfigError>;