1use std::fmt;
8
9#[derive(Debug)]
10pub enum TriggerError {
11 Malformed { what: String },
13 UnresolvedWorkflow { hash: String, why: String },
15 NoConnector { trigger: String, connector: String },
21 ConnectorFailed { trigger: String, detail: String },
23 ClaimLost { trigger: String },
25 BadSchedule { expr: String, why: String },
28 UnknownTarget { target: String },
30 UnknownMember { trigger: String, member: String },
32 EgressRefused { trigger: String, host: String },
34 Storage { detail: String },
36 BlobContract { trigger: String, detail: String },
42}
43
44impl TriggerError {
45 pub fn code(&self) -> &'static str {
46 match self {
47 TriggerError::Malformed { .. } => "TRG-E001",
48 TriggerError::UnresolvedWorkflow { .. } => "TRG-E002",
49 TriggerError::NoConnector { .. } => "TRG-E003",
50 TriggerError::ConnectorFailed { .. } => "TRG-E004",
51 TriggerError::ClaimLost { .. } => "TRG-E005",
52 TriggerError::BadSchedule { .. } => "TRG-E006",
53 TriggerError::UnknownTarget { .. } => "TRG-E007",
54 TriggerError::UnknownMember { .. } => "TRG-E008",
55 TriggerError::EgressRefused { .. } => "TRG-E009",
56 TriggerError::Storage { .. } => "TRG-E010",
57 TriggerError::BlobContract { .. } => "TRG-E011",
58 }
59 }
60}
61
62impl fmt::Display for TriggerError {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 TriggerError::Malformed { what } => {
66 write!(f, "TRG-E001: malformed trigger declaration: {what}")
67 }
68 TriggerError::UnresolvedWorkflow { hash, why } => {
69 write!(f, "TRG-E002: workflow {hash} does not resolve: {why}")
70 }
71 TriggerError::NoConnector { trigger, connector } => write!(
72 f,
73 "TRG-E003: trigger {trigger} is due but no connector command is configured \
74 for '{connector}' — pass --connector-cmd, or the poll would silently do nothing"
75 ),
76 TriggerError::ConnectorFailed { trigger, detail } => {
77 write!(f, "TRG-E004: connector for trigger {trigger} failed: {detail}")
78 }
79 TriggerError::ClaimLost { trigger } => write!(
80 f,
81 "TRG-E005: claim on trigger {trigger} was lost — its lease expired and another \
82 evaluator took over; this firing's release was refused"
83 ),
84 TriggerError::BadSchedule { expr, why } => {
85 write!(f, "TRG-E006: schedule {expr:?} is unusable: {why}")
86 }
87 TriggerError::UnknownTarget { target } => {
88 write!(f, "TRG-E007: unknown render target '{target}'")
89 }
90 TriggerError::UnknownMember { trigger, member } => write!(
91 f,
92 "TRG-E008: composite trigger {trigger} references member '{member}' \
93 which it does not declare"
94 ),
95 TriggerError::EgressRefused { trigger, host } => write!(
96 f,
97 "TRG-E009: connector for trigger {trigger} tried to reach '{host}', \
98 which is outside its declared allowed_outbound_hosts"
99 ),
100 TriggerError::Storage { detail } => write!(f, "TRG-E010: store: {detail}"),
101 TriggerError::BlobContract { trigger, detail } => write!(
102 f,
103 "TRG-E011: connector blob contract violated for trigger {trigger}: {detail} — \
104 the poll was refused whole and the cursor left unmoved"
105 ),
106 }
107 }
108}
109
110impl std::error::Error for TriggerError {}
111
112pub type Result<T> = std::result::Result<T, TriggerError>;
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn all() -> Vec<TriggerError> {
119 vec![
120 TriggerError::Malformed { what: "x".into() },
121 TriggerError::UnresolvedWorkflow { hash: "h".into(), why: "w".into() },
122 TriggerError::NoConnector { trigger: "t".into(), connector: "c".into() },
123 TriggerError::ConnectorFailed { trigger: "t".into(), detail: "d".into() },
124 TriggerError::ClaimLost { trigger: "t".into() },
125 TriggerError::BadSchedule { expr: "e".into(), why: "w".into() },
126 TriggerError::UnknownTarget { target: "t".into() },
127 TriggerError::UnknownMember { trigger: "t".into(), member: "m".into() },
128 TriggerError::EgressRefused { trigger: "t".into(), host: "h".into() },
129 TriggerError::Storage { detail: "d".into() },
130 TriggerError::BlobContract { trigger: "t".into(), detail: "d".into() },
131 ]
132 }
133
134 #[test]
135 fn the_code_leads_the_display_string() {
136 for e in all() {
139 let s = e.to_string();
140 assert!(s.starts_with(e.code()), "{s} should start with {}", e.code());
141 }
142 }
143
144 #[test]
145 fn codes_are_unique_and_contiguous() {
146 let codes: Vec<&str> = all().iter().map(|e| e.code()).collect();
147 let mut sorted = codes.clone();
148 sorted.sort_unstable();
149 sorted.dedup();
150 assert_eq!(sorted.len(), codes.len(), "codes must be unique");
151 for (i, c) in codes.iter().enumerate() {
152 assert_eq!(*c, format!("TRG-E{:03}", i + 1), "codes are append-only and dense");
153 }
154 }
155}