1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct UnknownAtmErrorCode(pub String);
10
11impl fmt::Display for UnknownAtmErrorCode {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "unknown ATM error code: {}", self.0)
14 }
15}
16
17impl std::error::Error for UnknownAtmErrorCode {}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum AtmErrorCode {
21 ConfigHomeUnavailable,
22 AtmHomeUnresolved,
23 ConfigParseFailed,
24 ConfigRetiredHookMembersKey,
25 ConfigRetiredLegacyHookKeys,
26 ConfigTeamParseFailed,
27 ConfigTeamMissing,
28 IdentityUnavailable,
29 IdentityInvalid,
30 IdentityConflict,
31 MemberAlreadyExists,
32 MemberNotFound,
33 DaemonUnavailable,
34 RuntimeRootInvalid,
35 RuntimeBootstrapRefused,
36 SocketOverrideForbidden,
37 DaemonMayHaveExecuted,
38 DaemonLifecycleWedge,
39 DaemonLaunchGateRejected,
40 DaemonServingStateRejected,
41 DaemonStaleOwnerRecoveryFailed,
42 DaemonAutoStartFailed,
43 DaemonConnectionSaturated,
44 ClientDaemonVersionIncompatible,
45 DaemonAdvisorySessionAlreadyRegistered,
46 DaemonAdvisorySessionNotRegistered,
47 DaemonAdvisorySessionCleanupFailed,
48 RemoteDeliveryOutcomeUnknown,
49 AddressParseFailed,
50 TeamUnavailable,
51 TeamInvalid,
52 TeamNotFound,
53 AgentNotFound,
54 MailboxReadFailed,
55 MailboxWriteFailed,
56 MailboxLockFailed,
57 MailboxLockReadOnlyFilesystem,
58 MailboxLockTimeout,
59 InternalError,
60 MessageValidationFailed,
61 SelfAddressedSendInvalid,
62 EmptyNudgeTemplateBody,
63 CallerContextRequestInvalid,
64 SerializationFailed,
65 FilePolicyRejected,
66 FileReferenceRewriteFailed,
67 WaitTimeout,
68 AckInvalidState,
69 ClearInvalidState,
70 ObservabilityEmitFailed,
71 ObservabilityQueryFailed,
72 ObservabilityFollowFailed,
73 ObservabilityHealthFailed,
74 ObservabilityBootstrapFailed,
75 ObservabilityHealthOk,
76 WarningInvalidTeamMemberSkipped,
77 WarningMailboxRecordSkipped,
78 WarningMalformedAtmFieldIgnored,
79 WarningObservabilityHealthDegraded,
80 WarningSqliteHealthDegraded,
81 WarningOriginInboxEntrySkipped,
82 WarningMissingTeamConfigFallback,
83 WarningSendAlertStateDegraded,
84 WarningIdentityDrift,
85 WarningRosterDrift,
86 WarningBaselineMemberMissing,
87 WarningRestoreInProgress,
88 WarningStaleMailboxLock,
89 WarningHookSkipped,
90 WarningHookExecutionFailed,
91 PostSendPaneMissing,
92 PostSendTmuxSendFailed,
93 PostSendGraftUnavailable,
94 PostSendAdvisoryDeliveryFailed,
95 TestFakeTransportInjectionFailed,
96 HelpTopicNotFound,
97}
98
99impl AtmErrorCode {
100 pub fn as_str(self) -> &'static str {
101 if let Some(value) = self.config_or_identity_str() {
102 return value;
103 }
104 if let Some(value) = self.daemon_or_address_str() {
105 return value;
106 }
107 if let Some(value) = self.mailbox_or_validation_str() {
108 return value;
109 }
110 if let Some(value) = self.observability_or_warning_str() {
111 return value;
112 }
113 self.post_send_or_misc_str()
114 }
115
116 fn config_or_identity_str(self) -> Option<&'static str> {
117 Some(match self {
118 Self::ConfigHomeUnavailable => "ATM_CONFIG_HOME_UNAVAILABLE",
119 Self::AtmHomeUnresolved => "ATM_HOME_UNRESOLVED",
120 Self::ConfigParseFailed => "ATM_CONFIG_PARSE_FAILED",
121 Self::ConfigRetiredHookMembersKey => "ATM_CONFIG_RETIRED_HOOK_MEMBERS_KEY",
122 Self::ConfigRetiredLegacyHookKeys => "ATM_CONFIG_RETIRED_LEGACY_HOOK_KEYS",
123 Self::ConfigTeamParseFailed => "ATM_CONFIG_TEAM_PARSE_FAILED",
124 Self::ConfigTeamMissing => "ATM_CONFIG_TEAM_MISSING",
125 Self::IdentityUnavailable => "ATM_IDENTITY_UNAVAILABLE",
126 Self::IdentityInvalid => "ATM_IDENTITY_INVALID",
127 Self::IdentityConflict => "ATM_IDENTITY_CONFLICT",
128 Self::MemberAlreadyExists => "ATM_MEMBER_ALREADY_EXISTS",
129 Self::MemberNotFound => "ATM_MEMBER_NOT_FOUND",
130 _ => return None,
131 })
132 }
133
134 fn daemon_or_address_str(self) -> Option<&'static str> {
135 Some(match self {
136 Self::DaemonUnavailable => "ATM_DAEMON_UNAVAILABLE",
137 Self::RuntimeRootInvalid => "ATM_RUNTIME_ROOT_INVALID",
138 Self::RuntimeBootstrapRefused => "ATM_RUNTIME_BOOTSTRAP_REFUSED",
139 Self::SocketOverrideForbidden => "ATM_SOCKET_OVERRIDE_FORBIDDEN",
140 Self::DaemonMayHaveExecuted => "ATM_DAEMON_MAY_HAVE_EXECUTED",
141 Self::DaemonLifecycleWedge => "ATM_DAEMON_LIFECYCLE_WEDGE",
142 Self::DaemonLaunchGateRejected => "ATM_DAEMON_LAUNCH_GATE_REJECTED",
143 Self::DaemonServingStateRejected => "ATM_DAEMON_SERVING_STATE_REJECTED",
144 Self::DaemonStaleOwnerRecoveryFailed => "ATM_DAEMON_STALE_OWNER_RECOVERY_FAILED",
145 Self::DaemonAutoStartFailed => "ATM_DAEMON_AUTO_START_FAILED",
146 Self::DaemonConnectionSaturated => "ATM_DAEMON_CONNECTION_SATURATED",
147 Self::ClientDaemonVersionIncompatible => "ATM_CLIENT_DAEMON_VERSION_INCOMPATIBLE",
148 Self::DaemonAdvisorySessionAlreadyRegistered => {
149 "ATM_DAEMON_ADVISORY_SESSION_ALREADY_REGISTERED"
150 }
151 Self::DaemonAdvisorySessionNotRegistered => {
152 "ATM_DAEMON_ADVISORY_SESSION_NOT_REGISTERED"
153 }
154 Self::DaemonAdvisorySessionCleanupFailed => {
155 "ATM_DAEMON_ADVISORY_SESSION_CLEANUP_FAILED"
156 }
157 Self::RemoteDeliveryOutcomeUnknown => "ATM_REMOTE_OUTCOME_UNKNOWN",
158 Self::AddressParseFailed => "ATM_ADDRESS_PARSE_FAILED",
159 Self::TeamUnavailable => "ATM_TEAM_UNAVAILABLE",
160 Self::TeamInvalid => "ATM_TEAM_INVALID",
161 Self::TeamNotFound => "ATM_TEAM_NOT_FOUND",
162 Self::AgentNotFound => "ATM_AGENT_NOT_FOUND",
163 _ => return None,
164 })
165 }
166
167 fn mailbox_or_validation_str(self) -> Option<&'static str> {
168 Some(match self {
169 Self::MailboxReadFailed => "ATM_MAILBOX_READ_FAILED",
170 Self::MailboxWriteFailed => "ATM_MAILBOX_WRITE_FAILED",
171 Self::MailboxLockFailed => "ATM_MAILBOX_LOCK_FAILED",
172 Self::MailboxLockReadOnlyFilesystem => "ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM",
173 Self::MailboxLockTimeout => "ATM_MAILBOX_LOCK_TIMEOUT",
174 Self::InternalError => "ATM_INTERNAL_ERROR",
175 Self::MessageValidationFailed => "ATM_MESSAGE_VALIDATION_FAILED",
176 Self::SelfAddressedSendInvalid => "ATM_SELF_ADDRESSED_SEND_INVALID",
177 Self::EmptyNudgeTemplateBody => "ATM_NUDGE_TEMPLATE_BODY_EMPTY",
178 Self::CallerContextRequestInvalid => "ATM_CALLER_CONTEXT_REQUEST_INVALID",
179 Self::SerializationFailed => "ATM_SERIALIZATION_FAILED",
180 Self::FilePolicyRejected => "ATM_FILE_POLICY_REJECTED",
181 Self::FileReferenceRewriteFailed => "ATM_FILE_REFERENCE_REWRITE_FAILED",
182 Self::WaitTimeout => "ATM_WAIT_TIMEOUT",
183 Self::AckInvalidState => "ATM_ACK_INVALID_STATE",
184 Self::ClearInvalidState => "ATM_CLEAR_INVALID_STATE",
185 _ => return None,
186 })
187 }
188
189 fn observability_or_warning_str(self) -> Option<&'static str> {
190 Some(match self {
191 Self::ObservabilityEmitFailed => "ATM_OBSERVABILITY_EMIT_FAILED",
192 Self::ObservabilityQueryFailed => "ATM_OBSERVABILITY_QUERY_FAILED",
193 Self::ObservabilityFollowFailed => "ATM_OBSERVABILITY_FOLLOW_FAILED",
194 Self::ObservabilityHealthFailed => "ATM_OBSERVABILITY_HEALTH_FAILED",
195 Self::ObservabilityBootstrapFailed => "ATM_OBSERVABILITY_BOOTSTRAP_FAILED",
196 Self::ObservabilityHealthOk => "ATM_OBSERVABILITY_HEALTH_OK",
197 Self::WarningInvalidTeamMemberSkipped => "ATM_WARNING_INVALID_TEAM_MEMBER_SKIPPED",
198 Self::WarningMailboxRecordSkipped => "ATM_WARNING_MAILBOX_RECORD_SKIPPED",
199 Self::WarningMalformedAtmFieldIgnored => "ATM_WARNING_MALFORMED_ATM_FIELD_IGNORED",
200 Self::WarningObservabilityHealthDegraded => "ATM_WARNING_OBSERVABILITY_HEALTH_DEGRADED",
201 Self::WarningSqliteHealthDegraded => "ATM_WARNING_SQLITE_HEALTH_DEGRADED",
202 Self::WarningOriginInboxEntrySkipped => "ATM_WARNING_ORIGIN_INBOX_ENTRY_SKIPPED",
203 Self::WarningMissingTeamConfigFallback => "ATM_WARNING_MISSING_TEAM_CONFIG_FALLBACK",
204 Self::WarningSendAlertStateDegraded => "ATM_WARNING_SEND_ALERT_STATE_DEGRADED",
205 Self::WarningIdentityDrift => "ATM_WARNING_IDENTITY_DRIFT",
206 Self::WarningRosterDrift => "ATM_WARNING_ROSTER_DRIFT",
207 Self::WarningBaselineMemberMissing => "ATM_WARNING_BASELINE_MEMBER_MISSING",
208 Self::WarningRestoreInProgress => "ATM_WARNING_RESTORE_IN_PROGRESS",
209 Self::WarningStaleMailboxLock => "ATM_WARNING_STALE_MAILBOX_LOCK",
210 Self::WarningHookSkipped => "ATM_WARNING_HOOK_SKIPPED",
211 Self::WarningHookExecutionFailed => "ATM_WARNING_HOOK_EXECUTION_FAILED",
212 _ => return None,
213 })
214 }
215
216 fn post_send_or_misc_str(self) -> &'static str {
217 match self {
218 Self::PostSendPaneMissing => "ATM_POST_SEND_PANE_MISSING",
219 Self::PostSendTmuxSendFailed => "ATM_POST_SEND_TMUX_SEND_FAILED",
220 Self::PostSendGraftUnavailable => "ATM_POST_SEND_GRAFT_UNAVAILABLE",
221 Self::PostSendAdvisoryDeliveryFailed => "ATM_POST_SEND_ADVISORY_DELIVERY_FAILED",
222 Self::TestFakeTransportInjectionFailed => "ATM_TEST_FAKE_TRANSPORT_INJECTION_FAILED",
223 Self::HelpTopicNotFound => "ATM_HELP_TOPIC_NOT_FOUND",
224 _ => unreachable!("all AtmErrorCode variants must be covered by as_str helpers"),
225 }
226 }
227}
228
229impl FromStr for AtmErrorCode {
230 type Err = UnknownAtmErrorCode;
231
232 fn from_str(value: &str) -> Result<Self, Self::Err> {
233 parse_known_error_code(value).ok_or_else(|| UnknownAtmErrorCode(value.to_owned()))
234 }
235}
236
237fn parse_known_error_code(value: &str) -> Option<AtmErrorCode> {
238 parse_config_or_identity_code(value)
239 .or_else(|| parse_daemon_or_address_code(value))
240 .or_else(|| parse_mailbox_or_validation_code(value))
241 .or_else(|| parse_observability_or_warning_code(value))
242 .or_else(|| parse_post_send_or_misc_code(value))
243}
244
245fn parse_config_or_identity_code(value: &str) -> Option<AtmErrorCode> {
246 Some(match value {
247 "ATM_CONFIG_HOME_UNAVAILABLE" => AtmErrorCode::ConfigHomeUnavailable,
248 "ATM_HOME_UNRESOLVED" => AtmErrorCode::AtmHomeUnresolved,
249 "ATM_CONFIG_PARSE_FAILED" => AtmErrorCode::ConfigParseFailed,
250 "ATM_CONFIG_RETIRED_HOOK_MEMBERS_KEY" => AtmErrorCode::ConfigRetiredHookMembersKey,
251 "ATM_CONFIG_RETIRED_LEGACY_HOOK_KEYS" => AtmErrorCode::ConfigRetiredLegacyHookKeys,
252 "ATM_CONFIG_TEAM_PARSE_FAILED" => AtmErrorCode::ConfigTeamParseFailed,
253 "ATM_CONFIG_TEAM_MISSING" => AtmErrorCode::ConfigTeamMissing,
254 "ATM_IDENTITY_UNAVAILABLE" => AtmErrorCode::IdentityUnavailable,
255 "ATM_IDENTITY_INVALID" => AtmErrorCode::IdentityInvalid,
256 "ATM_IDENTITY_CONFLICT" => AtmErrorCode::IdentityConflict,
257 "ATM_MEMBER_ALREADY_EXISTS" => AtmErrorCode::MemberAlreadyExists,
258 "ATM_MEMBER_NOT_FOUND" => AtmErrorCode::MemberNotFound,
259 _ => return None,
260 })
261}
262
263fn parse_daemon_or_address_code(value: &str) -> Option<AtmErrorCode> {
264 Some(match value {
265 "ATM_DAEMON_UNAVAILABLE" => AtmErrorCode::DaemonUnavailable,
266 "ATM_RUNTIME_ROOT_INVALID" => AtmErrorCode::RuntimeRootInvalid,
267 "ATM_RUNTIME_BOOTSTRAP_REFUSED" => AtmErrorCode::RuntimeBootstrapRefused,
268 "ATM_SOCKET_OVERRIDE_FORBIDDEN" => AtmErrorCode::SocketOverrideForbidden,
269 "ATM_DAEMON_MAY_HAVE_EXECUTED" => AtmErrorCode::DaemonMayHaveExecuted,
270 "ATM_DAEMON_LIFECYCLE_WEDGE" => AtmErrorCode::DaemonLifecycleWedge,
271 "ATM_DAEMON_LAUNCH_GATE_REJECTED" => AtmErrorCode::DaemonLaunchGateRejected,
272 "ATM_DAEMON_SERVING_STATE_REJECTED" => AtmErrorCode::DaemonServingStateRejected,
273 "ATM_DAEMON_STALE_OWNER_RECOVERY_FAILED" => AtmErrorCode::DaemonStaleOwnerRecoveryFailed,
274 "ATM_DAEMON_AUTO_START_FAILED" => AtmErrorCode::DaemonAutoStartFailed,
275 "ATM_DAEMON_CONNECTION_SATURATED" => AtmErrorCode::DaemonConnectionSaturated,
276 "ATM_CLIENT_DAEMON_VERSION_INCOMPATIBLE" => AtmErrorCode::ClientDaemonVersionIncompatible,
277 "ATM_DAEMON_ADVISORY_SESSION_ALREADY_REGISTERED" => {
278 AtmErrorCode::DaemonAdvisorySessionAlreadyRegistered
279 }
280 "ATM_DAEMON_ADVISORY_SESSION_NOT_REGISTERED" => {
281 AtmErrorCode::DaemonAdvisorySessionNotRegistered
282 }
283 "ATM_DAEMON_ADVISORY_SESSION_CLEANUP_FAILED" => {
284 AtmErrorCode::DaemonAdvisorySessionCleanupFailed
285 }
286 "ATM_REMOTE_OUTCOME_UNKNOWN" => AtmErrorCode::RemoteDeliveryOutcomeUnknown,
287 "ATM_ADDRESS_PARSE_FAILED" => AtmErrorCode::AddressParseFailed,
288 "ATM_TEAM_UNAVAILABLE" => AtmErrorCode::TeamUnavailable,
289 "ATM_TEAM_INVALID" => AtmErrorCode::TeamInvalid,
290 "ATM_TEAM_NOT_FOUND" => AtmErrorCode::TeamNotFound,
291 "ATM_AGENT_NOT_FOUND" => AtmErrorCode::AgentNotFound,
292 _ => return None,
293 })
294}
295
296fn parse_mailbox_or_validation_code(value: &str) -> Option<AtmErrorCode> {
297 Some(match value {
298 "ATM_MAILBOX_READ_FAILED" => AtmErrorCode::MailboxReadFailed,
299 "ATM_MAILBOX_WRITE_FAILED" => AtmErrorCode::MailboxWriteFailed,
300 "ATM_MAILBOX_LOCK_FAILED" => AtmErrorCode::MailboxLockFailed,
301 "ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM" => AtmErrorCode::MailboxLockReadOnlyFilesystem,
302 "ATM_MAILBOX_LOCK_TIMEOUT" => AtmErrorCode::MailboxLockTimeout,
303 "ATM_INTERNAL_ERROR" => AtmErrorCode::InternalError,
304 "ATM_MESSAGE_VALIDATION_FAILED" => AtmErrorCode::MessageValidationFailed,
305 "ATM_SELF_ADDRESSED_SEND_INVALID" => AtmErrorCode::SelfAddressedSendInvalid,
306 "ATM_NUDGE_TEMPLATE_BODY_EMPTY" => AtmErrorCode::EmptyNudgeTemplateBody,
307 "ATM_CALLER_CONTEXT_REQUEST_INVALID" => AtmErrorCode::CallerContextRequestInvalid,
308 "ATM_SERIALIZATION_FAILED" => AtmErrorCode::SerializationFailed,
309 "ATM_FILE_POLICY_REJECTED" => AtmErrorCode::FilePolicyRejected,
310 "ATM_FILE_REFERENCE_REWRITE_FAILED" => AtmErrorCode::FileReferenceRewriteFailed,
311 "ATM_WAIT_TIMEOUT" => AtmErrorCode::WaitTimeout,
312 "ATM_ACK_INVALID_STATE" => AtmErrorCode::AckInvalidState,
313 "ATM_CLEAR_INVALID_STATE" => AtmErrorCode::ClearInvalidState,
314 _ => return None,
315 })
316}
317
318fn parse_observability_or_warning_code(value: &str) -> Option<AtmErrorCode> {
319 Some(match value {
320 "ATM_OBSERVABILITY_EMIT_FAILED" => AtmErrorCode::ObservabilityEmitFailed,
321 "ATM_OBSERVABILITY_QUERY_FAILED" => AtmErrorCode::ObservabilityQueryFailed,
322 "ATM_OBSERVABILITY_FOLLOW_FAILED" => AtmErrorCode::ObservabilityFollowFailed,
323 "ATM_OBSERVABILITY_HEALTH_FAILED" => AtmErrorCode::ObservabilityHealthFailed,
324 "ATM_OBSERVABILITY_BOOTSTRAP_FAILED" => AtmErrorCode::ObservabilityBootstrapFailed,
325 "ATM_OBSERVABILITY_HEALTH_OK" => AtmErrorCode::ObservabilityHealthOk,
326 "ATM_WARNING_INVALID_TEAM_MEMBER_SKIPPED" => AtmErrorCode::WarningInvalidTeamMemberSkipped,
327 "ATM_WARNING_MAILBOX_RECORD_SKIPPED" => AtmErrorCode::WarningMailboxRecordSkipped,
328 "ATM_WARNING_MALFORMED_ATM_FIELD_IGNORED" => AtmErrorCode::WarningMalformedAtmFieldIgnored,
329 "ATM_WARNING_OBSERVABILITY_HEALTH_DEGRADED" => {
330 AtmErrorCode::WarningObservabilityHealthDegraded
331 }
332 "ATM_WARNING_SQLITE_HEALTH_DEGRADED" => AtmErrorCode::WarningSqliteHealthDegraded,
333 "ATM_WARNING_ORIGIN_INBOX_ENTRY_SKIPPED" => AtmErrorCode::WarningOriginInboxEntrySkipped,
334 "ATM_WARNING_MISSING_TEAM_CONFIG_FALLBACK" => {
335 AtmErrorCode::WarningMissingTeamConfigFallback
336 }
337 "ATM_WARNING_SEND_ALERT_STATE_DEGRADED" => AtmErrorCode::WarningSendAlertStateDegraded,
338 "ATM_WARNING_IDENTITY_DRIFT" => AtmErrorCode::WarningIdentityDrift,
339 "ATM_WARNING_ROSTER_DRIFT" => AtmErrorCode::WarningRosterDrift,
340 "ATM_WARNING_BASELINE_MEMBER_MISSING" => AtmErrorCode::WarningBaselineMemberMissing,
341 "ATM_WARNING_RESTORE_IN_PROGRESS" => AtmErrorCode::WarningRestoreInProgress,
342 "ATM_WARNING_STALE_MAILBOX_LOCK" => AtmErrorCode::WarningStaleMailboxLock,
343 "ATM_WARNING_HOOK_SKIPPED" => AtmErrorCode::WarningHookSkipped,
344 "ATM_WARNING_HOOK_EXECUTION_FAILED" => AtmErrorCode::WarningHookExecutionFailed,
345 _ => return None,
346 })
347}
348
349fn parse_post_send_or_misc_code(value: &str) -> Option<AtmErrorCode> {
350 Some(match value {
351 "ATM_POST_SEND_PANE_MISSING" => AtmErrorCode::PostSendPaneMissing,
352 "ATM_POST_SEND_TMUX_SEND_FAILED" => AtmErrorCode::PostSendTmuxSendFailed,
353 "ATM_POST_SEND_GRAFT_UNAVAILABLE" => AtmErrorCode::PostSendGraftUnavailable,
354 "ATM_POST_SEND_ADVISORY_DELIVERY_FAILED" => AtmErrorCode::PostSendAdvisoryDeliveryFailed,
355 "ATM_TEST_FAKE_TRANSPORT_INJECTION_FAILED" => {
356 AtmErrorCode::TestFakeTransportInjectionFailed
357 }
358 "ATM_HELP_TOPIC_NOT_FOUND" => AtmErrorCode::HelpTopicNotFound,
359 _ => return None,
360 })
361}
362
363impl fmt::Display for AtmErrorCode {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 f.write_str(self.as_str())
366 }
367}
368
369impl Serialize for AtmErrorCode {
370 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
371 where
372 S: serde::Serializer,
373 {
374 serializer.serialize_str(self.as_str())
375 }
376}
377
378impl<'de> Deserialize<'de> for AtmErrorCode {
379 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
380 where
381 D: Deserializer<'de>,
382 {
383 let value = String::deserialize(deserializer)?;
384 value.parse().map_err(serde::de::Error::custom)
385 }
386}