stackless-core 0.1.7

Definition model, state store, and lifecycle engine for stackless
Documentation
//! Definition-layer errors. Every variant carries a stable code and a
//! remediation (ARCHITECTURE.md §2, §8).

use crate::fault::{Fault, codes};

#[derive(Debug, thiserror::Error)]
pub enum DefError {
    #[error("stackless.toml is not valid TOML: {message}")]
    Syntax { message: String },

    #[error("stackless.toml does not match the schema: {message}")]
    Schema { message: String },

    #[error("{kind} name {name:?} is not DNS-safe")]
    NameInvalid { kind: &'static str, name: String },

    #[error("the stack declares no services")]
    NoServices,

    #[error("unknown key {key:?} under {location}")]
    UnknownKey {
        location: String,
        key: String,
        known_substrates: Vec<String>,
    },

    #[error("`depends_on` under {location} is not part of the schema")]
    DependsOnRejected { location: String },

    #[error("substrate block {location} must be a table, found {found}")]
    SubstrateBlockInvalid { location: String, found: String },

    #[error("service {service:?} has no [services.{service}.{substrate}] config")]
    SubstrateConfigMissing { service: String, substrate: String },

    #[error("multiple services declare root_origin: {services:?}")]
    RootOriginConflict { services: Vec<String> },

    #[error("invalid interpolation reference {reference:?} in {location}: {detail}")]
    ReferenceSyntax {
        location: String,
        reference: String,
        detail: String,
    },

    #[error("{location} references undeclared {kind} {name:?}")]
    UndeclaredReference {
        location: String,
        kind: &'static str,
        name: String,
    },

    #[error("{location} uses secret {key:?} which is not in [secrets].required")]
    SecretNotRequired { location: String, key: String },

    #[error("[integrations.{integration}] is invalid: {detail}")]
    IntegrationInvalid { integration: String, detail: String },

    #[error("the wiring graph has a dependency cycle through: {nodes}")]
    WiringCycle { nodes: String },

    #[error("env under {location} must be a table of string values")]
    EnvNotStrings { location: String },
}

impl Fault for DefError {
    fn code(&self) -> &'static str {
        match self {
            Self::Syntax { .. } => codes::DEF_PARSE_SYNTAX,
            Self::Schema { .. } => codes::DEF_PARSE_SCHEMA,
            Self::NameInvalid { .. } => codes::DEF_NAME_INVALID,
            Self::NoServices => codes::DEF_NO_SERVICES,
            Self::UnknownKey { .. } => codes::DEF_UNKNOWN_KEY,
            Self::DependsOnRejected { .. } => codes::DEF_DEPENDS_ON_REJECTED,
            Self::SubstrateBlockInvalid { .. } => codes::DEF_SUBSTRATE_BLOCK_INVALID,
            Self::SubstrateConfigMissing { .. } => codes::DEF_SUBSTRATE_CONFIG_MISSING,
            Self::RootOriginConflict { .. } => codes::DEF_ROOT_ORIGIN_CONFLICT,
            Self::ReferenceSyntax { .. } => codes::DEF_REFERENCE_SYNTAX,
            Self::UndeclaredReference { .. } => codes::DEF_UNDECLARED_REFERENCE,
            Self::SecretNotRequired { .. } => codes::DEF_SECRET_NOT_REQUIRED,
            Self::IntegrationInvalid { .. } => codes::DEF_INTEGRATION_INVALID,
            Self::WiringCycle { .. } => codes::DEF_WIRING_CYCLE,
            Self::EnvNotStrings { .. } => codes::DEF_ENV_NOT_STRINGS,
        }
    }

    fn remediation(&self) -> String {
        match self {
            Self::Syntax { .. } => "fix the TOML syntax at the location shown, then re-run".into(),
            Self::Schema { .. } => {
                "compare the failing key against the schema reference in ARCHITECTURE.md §1; \
                 remove or rename keys the schema does not define"
                    .into()
            }
            Self::NameInvalid { kind, .. } => format!(
                "rename the {kind} to lowercase letters, digits, and hyphens, starting with a \
                 letter (it becomes hostnames and cloud service names)"
            ),
            Self::NoServices => {
                "declare at least one [services.<name>] table in stackless.toml".into()
            }
            Self::UnknownKey {
                key,
                known_substrates,
                ..
            } => format!(
                "{key:?} is neither a schema field nor a registered substrate; known substrates \
                 are {known_substrates:?} — fix the typo or remove the key"
            ),
            Self::DependsOnRejected { .. } => {
                "express the dependency in wiring instead: reference the dependency from this \
                 service's env (e.g. CORS_ALLOWED_ORIGINS = \"${services.web.origin}\"); the \
                 dependency graph is derived from wiring, never declared separately"
                    .into()
            }
            Self::SubstrateBlockInvalid { location, .. } => {
                format!("make {location} a TOML table, e.g. [{location}]")
            }
            Self::SubstrateConfigMissing { service, substrate } => format!(
                "add a [services.{service}.{substrate}] block with the config that substrate \
                 requires, or bring the instance up on a substrate this service supports"
            ),
            Self::RootOriginConflict { services } => format!(
                "keep root_origin = true on exactly one of {services:?} and remove it from the \
                 others"
            ),
            Self::ReferenceSyntax { .. } => {
                "valid references are ${stack.name}, ${instance.name}, \
                 ${services.<name>.origin}, ${secrets.<KEY>}, and \
                 ${integrations.<name>.<output>}"
                    .into()
            }
            Self::UndeclaredReference { kind, name, .. } => format!(
                "declare [{kind}s.{name}] in stackless.toml, or fix the reference to name a \
                 declared {kind}"
            ),
            Self::SecretNotRequired { key, .. } => format!(
                "add {key:?} to [secrets].required so it is resolved and validated before \
                 anything provisions"
            ),
            Self::IntegrationInvalid { integration, .. } => format!(
                "fix [integrations.{integration}]; declare provider and provider-specific \
                 config (see `stackless check` / SCHEMA.md)"
            ),
            Self::WiringCycle { .. } => {
                "break the cycle: at least one of these references must be removed or replaced \
                 with one that does not require its target to start first"
                    .into()
            }
            Self::EnvNotStrings { location } => {
                format!("every value under {location} must be a TOML string")
            }
        }
    }
}