use std::{error::Error, fmt};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
InvalidArgument,
NotFound,
Conflict,
Business,
Unavailable,
Infrastructure,
Internal,
}
pub struct SaddleError {
kind: ErrorKind,
code: &'static str,
message: String,
diagnostic: Option<Box<crate::Diagnostic>>,
}
impl SaddleError {
pub fn new(kind: ErrorKind, code: &'static str, message: impl Into<String>) -> Self {
Self {
kind,
code,
message: message.into(),
diagnostic: None,
}
}
pub const fn kind(&self) -> ErrorKind {
self.kind
}
pub const fn code(&self) -> &'static str {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub fn with_diagnostic(mut self, diagnostic: crate::Diagnostic) -> Self {
self.diagnostic = Some(Box::new(diagnostic));
self
}
pub fn diagnostic(&self) -> Option<&crate::Diagnostic> {
self.diagnostic.as_deref()
}
pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
if let Some(parent) = primary.diagnostic()
&& let Some(diagnostic) = self.diagnostic.take()
{
self.diagnostic = Some(Box::new(diagnostic.during_cleanup_of(parent)));
}
self
}
pub fn wrap_diagnostic(mut self, cause: crate::DiagnosticCause) -> Self {
if let Some(diagnostic) = self.diagnostic.take() {
self.diagnostic = Some(Box::new(diagnostic.wrap(cause)));
}
self
}
}
impl fmt::Display for SaddleError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(diagnostic) = &self.diagnostic {
fmt::Display::fmt(diagnostic, formatter)
} else {
write!(formatter, "{}: {}", self.code, self.message)
}
}
}
impl fmt::Debug for SaddleError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.diagnostic.is_some() {
fmt::Display::fmt(self, formatter)
} else {
formatter
.debug_struct("SaddleError")
.field("kind", &self.kind)
.field("code", &self.code)
.field("message", &self.message)
.finish()
}
}
}
impl Error for SaddleError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.diagnostic
.as_deref()
.map(|d| d as &(dyn Error + 'static))
}
}
pub type Result<T> = std::result::Result<T, SaddleError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cleanup_link_keeps_existing_origin_and_missing_primary() {
use crate::{
CaptureSite, Diagnostic, DiagnosticCategory, DiagnosticCause, DiagnosticCode,
DiagnosticStage,
};
fn error(code: &'static str) -> SaddleError {
SaddleError::new(ErrorKind::Internal, code, "safe").with_diagnostic(
Diagnostic::capture(
DiagnosticCategory::UnexpectedError,
CaptureSite::FirstObserved,
DiagnosticCause::new(
DiagnosticStage::FinalizerResource,
DiagnosticCode::new(code).unwrap(),
),
),
)
}
let primary = error("test.primary");
let cleanup = error("test.cleanup");
let id = cleanup.diagnostic().unwrap().id();
let cleanup = cleanup.during_cleanup_of(&SaddleError::new(
ErrorKind::Internal,
"test.no_diagnostic",
"safe",
));
assert_eq!(cleanup.diagnostic().unwrap().id(), id);
let before = serde_json::to_value(cleanup.diagnostic().unwrap()).unwrap();
let cleanup = cleanup.during_cleanup_of(&primary);
let after = serde_json::to_value(cleanup.diagnostic().unwrap()).unwrap();
assert_eq!(after["origin"], before["origin"]);
assert_eq!(after["causes"], before["causes"]);
assert_eq!(
after["primary_diagnostic_id"],
primary.diagnostic().unwrap().id()
);
assert_eq!(cleanup.diagnostic().unwrap().id(), id);
}
#[test]
fn error_exposes_stable_classification() {
let error = SaddleError::new(ErrorKind::NotFound, "user.not_found", "user not found");
assert_eq!(error.kind(), ErrorKind::NotFound);
assert_eq!(error.code(), "user.not_found");
assert_eq!(error.to_string(), "user.not_found: user not found");
}
}