1use std::{error::Error, fmt};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum ErrorKind {
7 InvalidArgument,
8 NotFound,
9 Conflict,
10 Business,
11 Unavailable,
12 Infrastructure,
13 Internal,
14}
15
16pub struct SaddleError {
21 kind: ErrorKind,
22 code: &'static str,
23 message: String,
24 diagnostic: Option<Box<crate::Diagnostic>>,
25}
26
27impl SaddleError {
28 pub fn new(kind: ErrorKind, code: &'static str, message: impl Into<String>) -> Self {
29 Self {
30 kind,
31 code,
32 message: message.into(),
33 diagnostic: None,
34 }
35 }
36
37 pub const fn kind(&self) -> ErrorKind {
38 self.kind
39 }
40
41 pub const fn code(&self) -> &'static str {
42 self.code
43 }
44
45 pub fn message(&self) -> &str {
46 &self.message
47 }
48
49 pub fn with_diagnostic(mut self, diagnostic: crate::Diagnostic) -> Self {
50 self.diagnostic = Some(Box::new(diagnostic));
51 self
52 }
53
54 pub fn diagnostic(&self) -> Option<&crate::Diagnostic> {
55 self.diagnostic.as_deref()
56 }
57 pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
59 if let Some(parent) = primary.diagnostic()
60 && let Some(diagnostic) = self.diagnostic.take()
61 {
62 self.diagnostic = Some(Box::new(diagnostic.during_cleanup_of(parent)));
63 }
64 self
65 }
66 pub fn wrap_diagnostic(mut self, cause: crate::DiagnosticCause) -> Self {
69 if let Some(diagnostic) = self.diagnostic.take() {
70 self.diagnostic = Some(Box::new(diagnostic.wrap(cause)));
71 }
72 self
73 }
74}
75
76impl fmt::Display for SaddleError {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 if let Some(diagnostic) = &self.diagnostic {
79 fmt::Display::fmt(diagnostic, formatter)
81 } else {
82 write!(formatter, "{}: {}", self.code, self.message)
83 }
84 }
85}
86
87impl fmt::Debug for SaddleError {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 if self.diagnostic.is_some() {
90 fmt::Display::fmt(self, formatter)
91 } else {
92 formatter
93 .debug_struct("SaddleError")
94 .field("kind", &self.kind)
95 .field("code", &self.code)
96 .field("message", &self.message)
97 .finish()
98 }
99 }
100}
101
102impl Error for SaddleError {
103 fn source(&self) -> Option<&(dyn Error + 'static)> {
104 self.diagnostic
105 .as_deref()
106 .map(|d| d as &(dyn Error + 'static))
107 }
108}
109
110pub type Result<T> = std::result::Result<T, SaddleError>;
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn cleanup_link_keeps_existing_origin_and_missing_primary() {
118 use crate::{
119 CaptureSite, Diagnostic, DiagnosticCategory, DiagnosticCause, DiagnosticCode,
120 DiagnosticStage,
121 };
122 fn error(code: &'static str) -> SaddleError {
123 SaddleError::new(ErrorKind::Internal, code, "safe").with_diagnostic(
124 Diagnostic::capture(
125 DiagnosticCategory::UnexpectedError,
126 CaptureSite::FirstObserved,
127 DiagnosticCause::new(
128 DiagnosticStage::FinalizerResource,
129 DiagnosticCode::new(code).unwrap(),
130 ),
131 ),
132 )
133 }
134 let primary = error("test.primary");
135 let cleanup = error("test.cleanup");
136 let id = cleanup.diagnostic().unwrap().id();
137 let cleanup = cleanup.during_cleanup_of(&SaddleError::new(
138 ErrorKind::Internal,
139 "test.no_diagnostic",
140 "safe",
141 ));
142 assert_eq!(cleanup.diagnostic().unwrap().id(), id);
143 let before = serde_json::to_value(cleanup.diagnostic().unwrap()).unwrap();
144 let cleanup = cleanup.during_cleanup_of(&primary);
145 let after = serde_json::to_value(cleanup.diagnostic().unwrap()).unwrap();
146 assert_eq!(after["origin"], before["origin"]);
147 assert_eq!(after["causes"], before["causes"]);
148 assert_eq!(
149 after["primary_diagnostic_id"],
150 primary.diagnostic().unwrap().id()
151 );
152 assert_eq!(cleanup.diagnostic().unwrap().id(), id);
153 }
154
155 #[test]
156 fn error_exposes_stable_classification() {
157 let error = SaddleError::new(ErrorKind::NotFound, "user.not_found", "user not found");
158
159 assert_eq!(error.kind(), ErrorKind::NotFound);
160 assert_eq!(error.code(), "user.not_found");
161 assert_eq!(error.to_string(), "user.not_found: user not found");
162 }
163}