Skip to main content

areev_trigger/
error.rs

1//! `TRG-Ennn` error codes.
2//!
3//! Every user-facing error carries a stable code as the **leading token of its
4//! `Display` string**, plus a `code()` method, so a reported code alone locates
5//! the variant. Codes are **append-only** — never renumber or reuse one.
6
7use std::fmt;
8
9#[derive(Debug)]
10pub enum TriggerError {
11    /// TRG-E001 — the declaration cannot fire as written.
12    Malformed { what: String },
13    /// TRG-E002 — `workflow` does not resolve to a Workflow grain.
14    UnresolvedWorkflow { hash: String, why: String },
15    /// TRG-E003 — a trigger is due but the host configured no connector command.
16    ///
17    /// Loud on purpose, matching how a host tool with no `--tool-cmd` refuses
18    /// rather than quietly succeeding: a poll that silently does nothing looks
19    /// exactly like a source with no new items.
20    NoConnector { trigger: String, connector: String },
21    /// TRG-E004 — the connector failed: non-zero exit, timeout, or non-JSON.
22    ConnectorFailed { trigger: String, detail: String },
23    /// TRG-E005 — claim lost: the lease expired and another evaluator took over.
24    ClaimLost { trigger: String },
25    /// TRG-E006 — the cron expression is invalid, or names a timezone this
26    /// release refuses.
27    BadSchedule { expr: String, why: String },
28    /// TRG-E007 — unknown `trigger render` target.
29    UnknownTarget { target: String },
30    /// TRG-E008 — a composite predicate references a member it does not declare.
31    UnknownMember { trigger: String, member: String },
32    /// TRG-E009 — the connector tried to reach a host outside its allowlist.
33    EgressRefused { trigger: String, host: String },
34    /// TRG-E010 — the store refused or failed underneath us.
35    Storage { detail: String },
36}
37
38impl TriggerError {
39    pub fn code(&self) -> &'static str {
40        match self {
41            TriggerError::Malformed { .. } => "TRG-E001",
42            TriggerError::UnresolvedWorkflow { .. } => "TRG-E002",
43            TriggerError::NoConnector { .. } => "TRG-E003",
44            TriggerError::ConnectorFailed { .. } => "TRG-E004",
45            TriggerError::ClaimLost { .. } => "TRG-E005",
46            TriggerError::BadSchedule { .. } => "TRG-E006",
47            TriggerError::UnknownTarget { .. } => "TRG-E007",
48            TriggerError::UnknownMember { .. } => "TRG-E008",
49            TriggerError::EgressRefused { .. } => "TRG-E009",
50            TriggerError::Storage { .. } => "TRG-E010",
51        }
52    }
53}
54
55impl fmt::Display for TriggerError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            TriggerError::Malformed { what } => {
59                write!(f, "TRG-E001: malformed trigger declaration: {what}")
60            }
61            TriggerError::UnresolvedWorkflow { hash, why } => {
62                write!(f, "TRG-E002: workflow {hash} does not resolve: {why}")
63            }
64            TriggerError::NoConnector { trigger, connector } => write!(
65                f,
66                "TRG-E003: trigger {trigger} is due but no connector command is configured \
67                 for '{connector}' — pass --connector-cmd, or the poll would silently do nothing"
68            ),
69            TriggerError::ConnectorFailed { trigger, detail } => {
70                write!(f, "TRG-E004: connector for trigger {trigger} failed: {detail}")
71            }
72            TriggerError::ClaimLost { trigger } => write!(
73                f,
74                "TRG-E005: claim on trigger {trigger} was lost — its lease expired and another \
75                 evaluator took over; this firing's release was refused"
76            ),
77            TriggerError::BadSchedule { expr, why } => {
78                write!(f, "TRG-E006: schedule {expr:?} is unusable: {why}")
79            }
80            TriggerError::UnknownTarget { target } => {
81                write!(f, "TRG-E007: unknown render target '{target}'")
82            }
83            TriggerError::UnknownMember { trigger, member } => write!(
84                f,
85                "TRG-E008: composite trigger {trigger} references member '{member}' \
86                 which it does not declare"
87            ),
88            TriggerError::EgressRefused { trigger, host } => write!(
89                f,
90                "TRG-E009: connector for trigger {trigger} tried to reach '{host}', \
91                 which is outside its declared allowed_outbound_hosts"
92            ),
93            TriggerError::Storage { detail } => write!(f, "TRG-E010: store: {detail}"),
94        }
95    }
96}
97
98impl std::error::Error for TriggerError {}
99
100pub type Result<T> = std::result::Result<T, TriggerError>;
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    fn all() -> Vec<TriggerError> {
107        vec![
108            TriggerError::Malformed { what: "x".into() },
109            TriggerError::UnresolvedWorkflow { hash: "h".into(), why: "w".into() },
110            TriggerError::NoConnector { trigger: "t".into(), connector: "c".into() },
111            TriggerError::ConnectorFailed { trigger: "t".into(), detail: "d".into() },
112            TriggerError::ClaimLost { trigger: "t".into() },
113            TriggerError::BadSchedule { expr: "e".into(), why: "w".into() },
114            TriggerError::UnknownTarget { target: "t".into() },
115            TriggerError::UnknownMember { trigger: "t".into(), member: "m".into() },
116            TriggerError::EgressRefused { trigger: "t".into(), host: "h".into() },
117            TriggerError::Storage { detail: "d".into() },
118        ]
119    }
120
121    #[test]
122    fn the_code_leads_the_display_string() {
123        // A reported code alone has to locate the variant, so it must be the
124        // first token — not buried mid-sentence where a log grep misses it.
125        for e in all() {
126            let s = e.to_string();
127            assert!(s.starts_with(e.code()), "{s} should start with {}", e.code());
128        }
129    }
130
131    #[test]
132    fn codes_are_unique_and_contiguous() {
133        let codes: Vec<&str> = all().iter().map(|e| e.code()).collect();
134        let mut sorted = codes.clone();
135        sorted.sort_unstable();
136        sorted.dedup();
137        assert_eq!(sorted.len(), codes.len(), "codes must be unique");
138        for (i, c) in codes.iter().enumerate() {
139            assert_eq!(*c, format!("TRG-E{:03}", i + 1), "codes are append-only and dense");
140        }
141    }
142}