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