1use thiserror::Error;
2use uuid::Uuid;
3
4#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum OutboxError {
11 #[error("failed to (de)serialize event payload as JSON")]
13 Serialization(#[from] serde_json::Error),
14
15 #[error("database error")]
20 Database(#[source] Box<dyn std::error::Error + Send + Sync>),
21
22 #[error("no handler registered for event type `{event_type}`")]
24 MissingHandler {
25 event_type: String,
27 },
28
29 #[error("event {event_id} reached max retries after {attempts} attempts")]
31 MaxRetries {
32 event_id: Uuid,
34 attempts: u32,
36 },
37
38 #[error("envelope carries event_type `{actual}` but decode requested `{expected}`")]
45 TypeMismatch {
46 expected: &'static str,
48 actual: String,
50 },
51
52 #[error("internal outbox error: {0}")]
57 Internal(String),
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn serialization_error_is_built_from_serde_json() {
66 let invalid_json = b"not json";
67 let serde_error: serde_json::Error =
68 serde_json::from_slice::<serde_json::Value>(invalid_json).unwrap_err();
69 let error: OutboxError = serde_error.into();
70 assert!(matches!(error, OutboxError::Serialization(_)));
71 }
72
73 #[test]
74 fn database_error_preserves_source_chain() {
75 let inner = std::io::Error::other("disk on fire");
76 let error = OutboxError::Database(Box::new(inner));
77 let source = std::error::Error::source(&error).expect("source must be set");
78 assert_eq!(source.to_string(), "disk on fire");
79 }
80
81 #[test]
82 fn missing_handler_message_includes_event_type() {
83 let error = OutboxError::MissingHandler {
84 event_type: "users.registered".to_owned(),
85 };
86 assert!(error.to_string().contains("users.registered"));
87 }
88
89 #[test]
90 fn max_retries_message_includes_event_id_and_count() {
91 let event_id = Uuid::from_u128(7);
92 let error = OutboxError::MaxRetries {
93 event_id,
94 attempts: 5,
95 };
96 let message = error.to_string();
97 assert!(message.contains(&event_id.to_string()));
98 assert!(message.contains('5'));
99 }
100}