apollo-configuration 0.4.0

A typed, friendly configuration system for Apollo products.
Documentation
use apollo_errors::Error;
use miette::Diagnostic;
use miette::SourceSpan;
use std::sync::Arc;

#[derive(Debug, Diagnostic, Error)]
#[diagnostic()]
pub(crate) enum ExpandErrorKind {
    #[error("environment variable not present")]
    #[diagnostic(code(apollo::configuration::env_missing))]
    NotPresent {
        #[label("environment variable not present")]
        span: SourceSpan,
    },
    #[error("invalid expansion kind")]
    #[diagnostic(code(apollo::configuration::env_invalid))]
    #[diagnostic(help("only `env.` expansions are supported"))]
    InvalidExpansion {
        #[label("invalid expansion kind")]
        span: SourceSpan,
    },
    #[error("expanded value contains invalid characters")]
    #[diagnostic(code(apollo::configuration::env_invalid))]
    #[diagnostic(help(
        "environment variables must contain UTF-8 values to be used in configuration"
    ))]
    NotUnicode {
        #[label("could not expand this value")]
        span: SourceSpan,
    },
    #[error("invalid bare expansion")]
    #[diagnostic(code(apollo::configuration::env_invalid))]
    UnprefixedExpansion {
        #[label("change this to `${{env.{name}}}`")]
        span: SourceSpan,
        name: String,
    },
}

#[derive(Debug, Diagnostic, Error)]
#[error("could not expand configuration values")]
#[diagnostic(code(apollo::configuration::expansion))]
pub struct ExpandError {
    // fields are pub(crate) so we can construct these
    // and inspect the fields in unit tests
    #[source_code]
    pub(crate) source_code: Arc<str>,
    #[related]
    pub(crate) errors: Vec<ExpandErrorKind>,
}

/// A JSON schema validation error.
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
#[diagnostic(code(apollo::configuration::validation))]
pub struct ValidationError {
    #[label("{message}")]
    pub(crate) label: SourceSpan,
    pub(crate) message: String,
}

/// A list of JSON schema validation errors.
#[derive(Debug, Error, Diagnostic)]
#[error("schema validation error")]
#[diagnostic(code(apollo::configuration::schema))]
pub struct ValidationErrors {
    #[source_code]
    pub(crate) source_code: Arc<str>,
    #[related]
    pub(crate) errors: Vec<Box<dyn Diagnostic + Send + Sync + 'static>>,
}

/// Errors that can occur when transforming the YAML input to a JSON-compatible value.
///
/// Not all values that YAML can represent are supported in apollo-configuration.
#[derive(Debug, Error, Diagnostic)]
pub enum YamlToJsonError {
    /// Error raised when NaN/Infinity are used in YAML values.
    ///
    /// NaN/Infinity are supported in YAML, but not in JSON schema. apollo-configuration rejects those
    /// values.
    #[error("unsupported value")]
    #[diagnostic(code(apollo::configuration::invalid_number))]
    InvalidNumberError {
        #[source_code]
        source_code: Arc<str>,
        #[label("NaN/Infinity are not suported in configuration")]
        label: SourceSpan,
    },

    /// Error raised when an object with non-string keys appears in a YAML value.
    ///
    /// YAML supports any type of key value in mappings, but JSON schema does not. apollo-configuration
    /// rejects such values.
    #[error("unsupported key")]
    #[diagnostic(code(apollo::configuration::invalid_mapping))]
    InvalidMappingError {
        #[source_code]
        source_code: Arc<str>,
        #[label("non-string mapping keys are not supported in configuration")]
        label: SourceSpan,
    },
}

/// Top-level error type.
#[derive(Debug, Error, Diagnostic)]
#[non_exhaustive]
pub enum ConfigError {
    #[error("{error}")]
    #[diagnostic(code(apollo::configuration::parse))]
    ParseError {
        #[from]
        error: serde_yaml::Error,
    },

    #[error("{error}")]
    #[diagnostic(code(apollo::configuration::parse))]
    // TODO: Split this between parse error and JsonConversionError?
    ParseError2 {
        #[from]
        error: saphyr::ScanError,
    },

    // XXX(@goto-bus-stop): I don't love the way this looks in CLI error reports. There is a
    // tension between providing useful enough context in the main message in text formats like
    // JSON, and putting information in the most relevant place in CLI diagnostics.
    #[error("failed to parse value: {error}")]
    #[diagnostic(code(apollo::configuration::parse))]
    InvalidValue {
        #[source_code]
        source_code: Arc<str>,
        #[label("this value could not be parsed")]
        label: Option<SourceSpan>,
        // XXX(@goto-bus-stop): this should really be annotated with `#[source]`, but then it
        // looks a bit crap in CLI error reports!
        error: serde_json::Error,
    },

    #[error(transparent)]
    #[diagnostic(transparent)]
    ValidationError(#[from] ValidationErrors),

    #[error(transparent)]
    #[diagnostic(transparent)]
    JsonConversionError(#[from] YamlToJsonError),

    #[error(transparent)]
    #[diagnostic(transparent)]
    ExpansionError(#[from] ExpandError),
}