Skip to main content

stackless_core/def/
error.rs

1//! Definition-layer errors. Every variant carries a stable code and a
2//! remediation (ARCHITECTURE.md §2, §8).
3
4use crate::fault::{Fault, codes};
5
6#[derive(Debug, thiserror::Error)]
7pub enum DefError {
8    #[error("stackless.toml is not valid TOML: {message}")]
9    Syntax { message: String },
10
11    #[error("stackless.toml does not match the schema: {message}")]
12    Schema { message: String },
13
14    #[error("{kind} name {name:?} is not DNS-safe")]
15    NameInvalid { kind: &'static str, name: String },
16
17    #[error("the stack declares no services")]
18    NoServices,
19
20    #[error("unknown key {key:?} under {location}")]
21    UnknownKey {
22        location: String,
23        key: String,
24        known_substrates: Vec<String>,
25    },
26
27    #[error("`depends_on` under {location} is not part of the schema")]
28    DependsOnRejected { location: String },
29
30    #[error("substrate block {location} must be a table, found {found}")]
31    SubstrateBlockInvalid { location: String, found: String },
32
33    #[error("service {service:?} has no [services.{service}.{substrate}] config")]
34    SubstrateConfigMissing { service: String, substrate: String },
35
36    #[error("multiple services declare root_origin: {services:?}")]
37    RootOriginConflict { services: Vec<String> },
38
39    #[error("invalid interpolation reference {reference:?} in {location}: {detail}")]
40    ReferenceSyntax {
41        location: String,
42        reference: String,
43        detail: String,
44    },
45
46    #[error("{location} references undeclared {kind} {name:?}")]
47    UndeclaredReference {
48        location: String,
49        kind: &'static str,
50        name: String,
51    },
52
53    #[error("{location} uses secret {key:?} which is not in [secrets].required")]
54    SecretNotRequired { location: String, key: String },
55
56    #[error("[integrations.{integration}] is invalid: {detail}")]
57    IntegrationInvalid { integration: String, detail: String },
58
59    #[error("the wiring graph has a dependency cycle through: {nodes}")]
60    WiringCycle { nodes: String },
61
62    #[error("env under {location} must be a table of string values")]
63    EnvNotStrings { location: String },
64}
65
66impl Fault for DefError {
67    fn code(&self) -> &'static str {
68        match self {
69            Self::Syntax { .. } => codes::DEF_PARSE_SYNTAX,
70            Self::Schema { .. } => codes::DEF_PARSE_SCHEMA,
71            Self::NameInvalid { .. } => codes::DEF_NAME_INVALID,
72            Self::NoServices => codes::DEF_NO_SERVICES,
73            Self::UnknownKey { .. } => codes::DEF_UNKNOWN_KEY,
74            Self::DependsOnRejected { .. } => codes::DEF_DEPENDS_ON_REJECTED,
75            Self::SubstrateBlockInvalid { .. } => codes::DEF_SUBSTRATE_BLOCK_INVALID,
76            Self::SubstrateConfigMissing { .. } => codes::DEF_SUBSTRATE_CONFIG_MISSING,
77            Self::RootOriginConflict { .. } => codes::DEF_ROOT_ORIGIN_CONFLICT,
78            Self::ReferenceSyntax { .. } => codes::DEF_REFERENCE_SYNTAX,
79            Self::UndeclaredReference { .. } => codes::DEF_UNDECLARED_REFERENCE,
80            Self::SecretNotRequired { .. } => codes::DEF_SECRET_NOT_REQUIRED,
81            Self::IntegrationInvalid { .. } => codes::DEF_INTEGRATION_INVALID,
82            Self::WiringCycle { .. } => codes::DEF_WIRING_CYCLE,
83            Self::EnvNotStrings { .. } => codes::DEF_ENV_NOT_STRINGS,
84        }
85    }
86
87    fn remediation(&self) -> String {
88        match self {
89            Self::Syntax { .. } => "fix the TOML syntax at the location shown, then re-run".into(),
90            Self::Schema { .. } => "compare the failing key against docs/SCHEMA.md; \
91                 remove or rename keys the schema does not define"
92                .into(),
93            Self::NameInvalid { kind, .. } => format!(
94                "rename the {kind} to lowercase letters, digits, and hyphens, starting with a \
95                 letter (it becomes hostnames and cloud service names)"
96            ),
97            Self::NoServices => {
98                "declare at least one [services.<name>] table in stackless.toml".into()
99            }
100            Self::UnknownKey {
101                key,
102                known_substrates,
103                ..
104            } => format!(
105                "{key:?} is neither a schema field nor a registered substrate; known substrates \
106                 are {known_substrates:?} — fix the typo or remove the key"
107            ),
108            Self::DependsOnRejected { .. } => {
109                "express the dependency in wiring instead: reference the dependency from this \
110                 service's env (e.g. CORS_ALLOWED_ORIGINS = \"${services.web.origin}\"); the \
111                 dependency graph is derived from wiring, never declared separately"
112                    .into()
113            }
114            Self::SubstrateBlockInvalid { location, .. } => {
115                format!("make {location} a TOML table, e.g. [{location}]")
116            }
117            Self::SubstrateConfigMissing { service, substrate } => format!(
118                "add a [services.{service}.{substrate}] block with the config that substrate \
119                 requires, or bring the instance up on a substrate this service supports"
120            ),
121            Self::RootOriginConflict { services } => format!(
122                "keep root_origin = true on exactly one of {services:?} and remove it from the \
123                 others"
124            ),
125            Self::ReferenceSyntax { .. } => {
126                "valid references are ${stack.name}, ${instance.name}, \
127                 ${services.<name>.origin}, ${secrets.<KEY>}, and \
128                 ${integrations.<name>.<output>}"
129                    .into()
130            }
131            Self::UndeclaredReference { kind, name, .. } => format!(
132                "declare [{kind}s.{name}] in stackless.toml, or fix the reference to name a \
133                 declared {kind}"
134            ),
135            Self::SecretNotRequired { key, .. } => format!(
136                "add {key:?} to [secrets].required so it is resolved and validated before \
137                 anything provisions"
138            ),
139            Self::IntegrationInvalid { integration, .. } => format!(
140                "fix [integrations.{integration}]; declare provider and provider-specific \
141                 config (see `stackless check` / SCHEMA.md)"
142            ),
143            Self::WiringCycle { .. } => {
144                "break the cycle: at least one of these references must be removed or replaced \
145                 with one that does not require its target to start first"
146                    .into()
147            }
148            Self::EnvNotStrings { location } => {
149                format!("every value under {location} must be a TOML string")
150            }
151        }
152    }
153}