use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum OutboxError {
#[error("failed to (de)serialize event payload as JSON")]
Serialization(#[from] serde_json::Error),
#[error("database error")]
Database(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("no handler registered for event type `{event_type}`")]
MissingHandler {
event_type: String,
},
#[error("event {event_id} reached max retries after {attempts} attempts")]
MaxRetries {
event_id: Uuid,
attempts: u32,
},
#[error("envelope carries event_type `{actual}` but decode requested `{expected}`")]
TypeMismatch {
expected: &'static str,
actual: String,
},
#[error("internal outbox error: {0}")]
Internal(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialization_error_is_built_from_serde_json() {
let invalid_json = b"not json";
let serde_error: serde_json::Error =
serde_json::from_slice::<serde_json::Value>(invalid_json).unwrap_err();
let error: OutboxError = serde_error.into();
assert!(matches!(error, OutboxError::Serialization(_)));
}
#[test]
fn database_error_preserves_source_chain() {
let inner = std::io::Error::other("disk on fire");
let error = OutboxError::Database(Box::new(inner));
let source = std::error::Error::source(&error).expect("source must be set");
assert_eq!(source.to_string(), "disk on fire");
}
#[test]
fn missing_handler_message_includes_event_type() {
let error = OutboxError::MissingHandler {
event_type: "users.registered".to_owned(),
};
assert!(error.to_string().contains("users.registered"));
}
#[test]
fn max_retries_message_includes_event_id_and_count() {
let event_id = Uuid::from_u128(7);
let error = OutboxError::MaxRetries {
event_id,
attempts: 5,
};
let message = error.to_string();
assert!(message.contains(&event_id.to_string()));
assert!(message.contains('5'));
}
}