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 { .. } => {
91                "compare the failing key against the schema reference in ARCHITECTURE.md §1; \
92                 remove or rename keys the schema does not define"
93                    .into()
94            }
95            Self::NameInvalid { kind, .. } => format!(
96                "rename the {kind} to lowercase letters, digits, and hyphens, starting with a \
97                 letter (it becomes hostnames and cloud service names)"
98            ),
99            Self::NoServices => {
100                "declare at least one [services.<name>] table in stackless.toml".into()
101            }
102            Self::UnknownKey {
103                key,
104                known_substrates,
105                ..
106            } => format!(
107                "{key:?} is neither a schema field nor a registered substrate; known substrates \
108                 are {known_substrates:?} — fix the typo or remove the key"
109            ),
110            Self::DependsOnRejected { .. } => {
111                "express the dependency in wiring instead: reference the dependency from this \
112                 service's env (e.g. CORS_ALLOWED_ORIGINS = \"${services.web.origin}\"); the \
113                 dependency graph is derived from wiring, never declared separately"
114                    .into()
115            }
116            Self::SubstrateBlockInvalid { location, .. } => {
117                format!("make {location} a TOML table, e.g. [{location}]")
118            }
119            Self::SubstrateConfigMissing { service, substrate } => format!(
120                "add a [services.{service}.{substrate}] block with the config that substrate \
121                 requires, or bring the instance up on a substrate this service supports"
122            ),
123            Self::RootOriginConflict { services } => format!(
124                "keep root_origin = true on exactly one of {services:?} and remove it from the \
125                 others"
126            ),
127            Self::ReferenceSyntax { .. } => {
128                "valid references are ${stack.name}, ${instance.name}, \
129                 ${services.<name>.origin}, ${secrets.<KEY>}, and \
130                 ${integrations.<name>.<output>}"
131                    .into()
132            }
133            Self::UndeclaredReference { kind, name, .. } => format!(
134                "declare [{kind}s.{name}] in stackless.toml, or fix the reference to name a \
135                 declared {kind}"
136            ),
137            Self::SecretNotRequired { key, .. } => format!(
138                "add {key:?} to [secrets].required so it is resolved and validated before \
139                 anything provisions"
140            ),
141            Self::IntegrationInvalid { integration, .. } => format!(
142                "fix [integrations.{integration}]; declare provider and provider-specific \
143                 config (see `stackless check` / SCHEMA.md)"
144            ),
145            Self::WiringCycle { .. } => {
146                "break the cycle: at least one of these references must be removed or replaced \
147                 with one that does not require its target to start first"
148                    .into()
149            }
150            Self::EnvNotStrings { location } => {
151                format!("every value under {location} must be a TOML string")
152            }
153        }
154    }
155}