1use std::{any::Any, 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 source_receipt: Option<Box<dyn Any + Send + Sync>>,
26 cleanup_diagnostic: Option<Box<crate::Diagnostic>>,
27 cleanup_source_receipt: Option<Box<dyn Any + Send + Sync>>,
28 unconfirmed_original: Option<Box<dyn Error + Send + Sync>>,
29 source_unavailable: bool,
30 cleanup_source_unavailable: bool,
31}
32
33impl SaddleError {
34 pub fn new(kind: ErrorKind, code: &'static str, message: impl Into<String>) -> Self {
35 Self {
36 kind,
37 code,
38 message: message.into(),
39 diagnostic: None,
40 source_receipt: None,
41 cleanup_diagnostic: None,
42 cleanup_source_receipt: None,
43 unconfirmed_original: None,
44 source_unavailable: false,
45 cleanup_source_unavailable: false,
46 }
47 }
48
49 pub const fn kind(&self) -> ErrorKind {
50 self.kind
51 }
52
53 pub const fn code(&self) -> &'static str {
54 self.code
55 }
56
57 pub fn message(&self) -> &str {
58 &self.message
59 }
60
61 pub fn with_diagnostic(mut self, diagnostic: crate::Diagnostic) -> Self {
62 self.diagnostic = Some(Box::new(diagnostic));
63 self
64 }
65
66 pub fn diagnostic(&self) -> Option<&crate::Diagnostic> {
67 self.diagnostic.as_deref()
68 }
69 pub fn with_source_receipt<T: Any + Send + Sync>(mut self, receipt: T) -> Self {
72 self.source_receipt = Some(Box::new(receipt));
73 self
74 }
75 pub fn source_receipt<T: Any + Send + Sync>(&self) -> Option<&T> {
76 self.source_receipt.as_ref()?.downcast_ref::<T>()
77 }
78 pub fn with_unconfirmed_original(
81 mut self, original: Box<dyn Error + Send + Sync>,
82 ) -> Self {
83 self.source_unavailable = true;
84 self.unconfirmed_original = Some(original);
85 self
86 }
87 pub fn unconfirmed_original(&self) -> Option<&(dyn Error + Send + Sync)> {
88 self.unconfirmed_original.as_deref()
89 }
90 pub fn take_unconfirmed_original(&mut self) -> Option<Box<dyn Error + Send + Sync>> {
91 self.unconfirmed_original.take()
92 }
93 pub fn with_cleanup_source_record<T: Any + Send + Sync>(
96 mut self,
97 diagnostic: crate::Diagnostic,
98 receipt: T,
99 ) -> Self {
100 self.cleanup_diagnostic = Some(Box::new(diagnostic));
101 self.cleanup_source_receipt = Some(Box::new(receipt));
102 self
103 }
104 pub fn cleanup_diagnostic(&self) -> Option<&crate::Diagnostic> {
105 self.cleanup_diagnostic.as_deref()
106 }
107 pub fn cleanup_source_receipt<T: Any + Send + Sync>(&self) -> Option<&T> {
108 self.cleanup_source_receipt.as_ref()?.downcast_ref::<T>()
109 }
110 pub fn with_unconfirmed_cleanup_diagnostic(mut self, diagnostic: crate::Diagnostic) -> Self {
113 self.cleanup_diagnostic = Some(Box::new(diagnostic));
114 self.cleanup_source_receipt = None;
115 self.cleanup_source_unavailable = true;
116 self
117 }
118 pub fn with_unconfirmed_source(mut self) -> Self {
120 self.source_unavailable = true;
121 self
122 }
123 pub const fn source_unavailable(&self) -> bool {
124 self.source_unavailable
125 }
126 pub fn with_unconfirmed_cleanup_source(mut self) -> Self {
129 self.cleanup_source_unavailable = true;
130 self
131 }
132 pub const fn cleanup_source_unavailable(&self) -> bool {
133 self.cleanup_source_unavailable
134 }
135 pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
137 if let Some(parent) = primary.diagnostic()
138 && let Some(diagnostic) = self.diagnostic.take()
139 {
140 self.diagnostic = Some(Box::new(diagnostic.during_cleanup_of(parent)));
141 }
142 self
143 }
144 pub fn during_cleanup_of_occurrence(mut self, primary: &crate::DiagnosticOccurrence) -> Self {
146 if let Some(diagnostic) = self.diagnostic.take() {
147 self.diagnostic = Some(Box::new(diagnostic.during_cleanup_of_occurrence(primary)));
148 }
149 self
150 }
151 pub fn wrap_diagnostic(mut self, cause: crate::DiagnosticCause) -> Self {
154 if let Some(diagnostic) = self.diagnostic.take() {
155 self.diagnostic = Some(Box::new(diagnostic.wrap(cause)));
156 }
157 self
158 }
159}
160
161impl fmt::Display for SaddleError {
162 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 if let Some(diagnostic) = &self.diagnostic {
164 fmt::Display::fmt(diagnostic, formatter)?;
166 } else {
167 write!(formatter, "{}: {}", self.code, self.message)?;
168 }
169 if self.source_unavailable {
170 write!(formatter, "; source=unavailable")?;
171 }
172 if self.cleanup_source_unavailable {
173 write!(formatter, "; cleanup_source=unavailable")?;
174 }
175 Ok(())
176 }
177}
178
179impl fmt::Debug for SaddleError {
180 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181 if self.diagnostic.is_some() {
182 fmt::Display::fmt(self, formatter)
183 } else {
184 formatter
185 .debug_struct("SaddleError")
186 .field("kind", &self.kind)
187 .field("code", &self.code)
188 .field("message", &self.message)
189 .field("source_unavailable", &self.source_unavailable)
190 .field("cleanup_source_unavailable", &self.cleanup_source_unavailable)
191 .finish()
192 }
193 }
194}
195
196impl Error for SaddleError {
197 fn source(&self) -> Option<&(dyn Error + 'static)> {
198 self.diagnostic
199 .as_deref()
200 .map(|d| d as &(dyn Error + 'static))
201 }
202}
203
204pub type Result<T> = std::result::Result<T, SaddleError>;
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210
211 #[test]
212 fn cleanup_link_keeps_existing_origin_and_missing_primary() {
213 use crate::{
214 CaptureSite, Diagnostic, DiagnosticCategory, DiagnosticCause, DiagnosticCode,
215 DiagnosticStage,
216 };
217 fn error(code: &'static str) -> SaddleError {
218 SaddleError::new(ErrorKind::Internal, code, "safe").with_diagnostic(
219 Diagnostic::capture(
220 DiagnosticCategory::UnexpectedError,
221 CaptureSite::FirstObserved,
222 DiagnosticCause::new(
223 DiagnosticStage::FinalizerResource,
224 DiagnosticCode::new(code).unwrap(),
225 ),
226 ),
227 )
228 }
229 let primary = error("test.primary");
230 let cleanup = error("test.cleanup");
231 let id = cleanup.diagnostic().unwrap().id();
232 let cleanup = cleanup.during_cleanup_of(&SaddleError::new(
233 ErrorKind::Internal,
234 "test.no_diagnostic",
235 "safe",
236 ));
237 assert_eq!(cleanup.diagnostic().unwrap().id(), id);
238 let before = serde_json::to_value(cleanup.diagnostic().unwrap()).unwrap();
239 let cleanup = cleanup.during_cleanup_of(&primary);
240 let after = serde_json::to_value(cleanup.diagnostic().unwrap()).unwrap();
241 assert_eq!(after["origin"], before["origin"]);
242 assert_eq!(after["causes"], before["causes"]);
243 assert_eq!(
244 after["primary_diagnostic_id"],
245 primary.diagnostic().unwrap().id()
246 );
247 assert_eq!(cleanup.diagnostic().unwrap().id(), id);
248 }
249
250 #[test]
251 fn error_exposes_stable_classification() {
252 let error = SaddleError::new(ErrorKind::NotFound, "user.not_found", "user not found");
253
254 assert_eq!(error.kind(), ErrorKind::NotFound);
255 assert_eq!(error.code(), "user.not_found");
256 assert_eq!(error.to_string(), "user.not_found: user not found");
257 }
258}