1use std::{error::Error as StdError, fmt};
2
3use crate::{core::DomainReason, StructError};
4
5mod private {
6 pub trait Sealed {}
7}
8
9pub trait RawStdError: StdError + Send + Sync + 'static {}
46
47#[doc(hidden)]
48pub trait UnstructuredSource: private::Sealed {
49 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
50 where
51 R: DomainReason;
52}
53
54pub trait SourceErr<T, R: DomainReason>: Sized {
79 fn source_err(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>>;
80}
81
82#[derive(Debug)]
83pub struct RawSource<E>(E);
84
85pub fn raw_source<E>(err: E) -> RawSource<E>
129where
130 E: RawStdError,
131{
132 RawSource(err)
133}
134
135impl<E> RawSource<E> {
136 pub fn into_inner(self) -> E {
137 self.0
138 }
139
140 pub fn inner(&self) -> &E {
141 &self.0
142 }
143}
144
145impl<E: fmt::Display> fmt::Display for RawSource<E> {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 self.0.fmt(f)
148 }
149}
150
151impl<E> StdError for RawSource<E>
152where
153 E: RawStdError,
154{
155 fn source(&self) -> Option<&(dyn StdError + 'static)> {
156 Some(&self.0)
157 }
158}
159
160impl<T, E, R> SourceErr<T, R> for Result<T, E>
161where
162 E: UnstructuredSource,
163 R: DomainReason,
164{
165 fn source_err(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>> {
166 let detail = detail.into();
167 self.map_err(|err| err.into_struct_error(reason, detail))
168 }
169}
170
171fn attach_std_source<E, R>(err: E, reason: R, detail: String) -> StructError<R>
172where
173 E: StdError + Send + Sync + 'static,
174 R: DomainReason,
175{
176 StructError::from(reason)
177 .with_detail(detail)
178 .with_std_source(err)
179}
180
181impl RawStdError for std::io::Error {}
182
183impl private::Sealed for std::io::Error {}
184
185impl UnstructuredSource for std::io::Error {
186 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
187 where
188 R: DomainReason,
189 {
190 attach_std_source(self, reason, detail)
191 }
192}
193
194impl<E> private::Sealed for RawSource<E> where E: RawStdError {}
195
196impl<E> UnstructuredSource for RawSource<E>
197where
198 E: RawStdError,
199{
200 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
201 where
202 R: DomainReason,
203 {
204 attach_std_source(self.0, reason, detail)
205 }
206}
207
208#[cfg(feature = "anyhow")]
209#[derive(Debug)]
210struct AnyhowStdSource(anyhow::Error);
211
212#[cfg(feature = "anyhow")]
213impl fmt::Display for AnyhowStdSource {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 self.0.fmt(f)
216 }
217}
218
219#[cfg(feature = "anyhow")]
220impl StdError for AnyhowStdSource {
221 fn source(&self) -> Option<&(dyn StdError + 'static)> {
222 self.0.source()
223 }
224}
225
226#[cfg(feature = "anyhow")]
227impl private::Sealed for anyhow::Error {}
228
229#[cfg(feature = "anyhow")]
230impl UnstructuredSource for anyhow::Error {
231 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
232 where
233 R: DomainReason,
234 {
235 use crate::core::OwnedDynStdStructError;
236
237 match self.downcast::<OwnedDynStdStructError>() {
238 Ok(source) => StructError::from(reason)
239 .with_detail(detail)
240 .with_dyn_struct_source(source),
241 Err(err) => attach_std_source(AnyhowStdSource(err), reason, detail),
242 }
243 }
244}
245
246#[cfg(feature = "serde_json")]
248impl RawStdError for serde_json::Error {}
249
250#[cfg(feature = "serde_json")]
252impl private::Sealed for serde_json::Error {}
253
254#[cfg(feature = "serde_json")]
256impl UnstructuredSource for serde_json::Error {
257 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
258 where
259 R: DomainReason,
260 {
261 attach_std_source(self, reason, detail)
262 }
263}
264
265#[cfg(feature = "toml")]
266impl RawStdError for toml::de::Error {}
267
268#[cfg(feature = "toml")]
269impl private::Sealed for toml::de::Error {}
270
271#[cfg(feature = "toml")]
272impl UnstructuredSource for toml::de::Error {
273 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
274 where
275 R: DomainReason,
276 {
277 attach_std_source(self, reason, detail)
278 }
279}
280
281#[cfg(feature = "toml")]
282impl RawStdError for toml::ser::Error {}
283
284#[cfg(feature = "toml")]
285impl private::Sealed for toml::ser::Error {}
286
287#[cfg(feature = "toml")]
288impl UnstructuredSource for toml::ser::Error {
289 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
290 where
291 R: DomainReason,
292 {
293 attach_std_source(self, reason, detail)
294 }
295}
296
297pub struct AnyErr<E: StdError + Send + Sync + 'static>(pub E);
303
304impl<E: StdError + Send + Sync + 'static> fmt::Display for AnyErr<E> {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 fmt::Display::fmt(&self.0, f)
307 }
308}
309
310impl<E: StdError + Send + Sync + 'static> fmt::Debug for AnyErr<E> {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 fmt::Debug::fmt(&self.0, f)
313 }
314}
315
316impl<E: StdError + Send + Sync + 'static> StdError for AnyErr<E> {
317 fn source(&self) -> Option<&(dyn StdError + 'static)> {
318 self.0.source()
319 }
320}
321
322impl<E: StdError + Send + Sync + 'static> RawStdError for AnyErr<E> {}
323
324impl<E: StdError + Send + Sync + 'static> private::Sealed for AnyErr<E> {}
325
326impl<E: StdError + Send + Sync + 'static> UnstructuredSource for AnyErr<E> {
327 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
328 where
329 R: DomainReason,
330 {
331 attach_std_source(self.0, reason, detail)
332 }
333}
334
335pub fn any_err<E: StdError + Send + Sync + 'static>(e: E) -> AnyErr<E> {
337 AnyErr(e)
338}
339
340pub trait SourceRawErr<T, R: DomainReason>: Sized {
346 fn source_raw_err(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>>;
347}
348
349impl<T, E, R> SourceRawErr<T, R> for Result<T, E>
350where
351 E: StdError + Send + Sync + 'static,
352 R: DomainReason,
353{
354 fn source_raw_err(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>> {
355 self.map_err(any_err).source_err(reason, detail)
356 }
357}
358
359impl<T, R1, R2> SourceErr<T, R2> for Result<T, StructError<R1>>
363where
364 R1: DomainReason,
365 R2: DomainReason,
366{
367 fn source_err(self, reason: R2, detail: impl Into<String>) -> Result<T, StructError<R2>> {
368 let detail = detail.into();
369 self.map_err(|err| {
370 StructError::from(reason)
371 .with_detail(detail)
372 .with_struct_source(err)
373 })
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use std::{fmt, io};
380
381 use super::{raw_source, RawStdError, SourceErr};
382 use crate::StructError;
383 use crate::UnifiedReason;
384
385 #[derive(Debug)]
386 struct ThirdPartyError(&'static str);
387
388 impl fmt::Display for ThirdPartyError {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 write!(f, "{}", self.0)
391 }
392 }
393
394 impl std::error::Error for ThirdPartyError {}
395
396 impl RawStdError for ThirdPartyError {}
397
398 #[test]
399 fn test_source_err_for_io_error() {
400 let result: Result<(), io::Error> = Err(io::Error::other("disk offline"));
401
402 let err = result
403 .source_err(UnifiedReason::system_error(), "load config failed")
404 .expect_err("expected structured error");
405
406 assert_eq!(err.detail().as_deref(), Some("load config failed"));
407 assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
408 }
409
410 #[test]
411 fn test_source_err_for_raw_source_wrapper() {
412 let result: Result<(), ThirdPartyError> = Err(ThirdPartyError("parser aborted"));
413
414 let err = result
415 .map_err(raw_source)
416 .source_err(UnifiedReason::validation_error(), "parse config failed")
417 .expect_err("expected structured error");
418
419 assert_eq!(err.detail().as_deref(), Some("parse config failed"));
420 assert_eq!(err.source_ref().unwrap().to_string(), "parser aborted");
421 }
422
423 #[cfg(feature = "anyhow")]
424 #[test]
425 fn test_source_err_for_anyhow_defaults_to_unstructured_source() {
426 let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("network offline"));
427
428 let err = result
429 .source_err(UnifiedReason::system_error(), "load config failed")
430 .expect_err("expected structured error");
431
432 assert_eq!(err.detail().as_deref(), Some("load config failed"));
433 assert_eq!(err.source_ref().unwrap().to_string(), "network offline");
434 assert_eq!(err.source_frames()[0].message, "network offline");
435 }
436
437 #[cfg(feature = "anyhow")]
438 #[test]
439 fn test_source_err_for_anyhow_extracts_top_level_official_dyn_bridge() {
440 let structured = StructError::from(UnifiedReason::validation_error())
441 .with_detail("invalid port")
442 .with_std_source(io::Error::other("not a number"));
443 let structured_display = structured.to_string();
444 let result: Result<(), anyhow::Error> = Err(anyhow::Error::new(structured.into_dyn_std()));
445
446 let err = result
447 .source_err(UnifiedReason::system_error(), "load config failed")
448 .expect_err("expected structured error");
449
450 assert_eq!(err.detail().as_deref(), Some("load config failed"));
451 assert_eq!(err.source_ref().unwrap().to_string(), structured_display);
452 assert_eq!(err.source_frames()[0].message, "validation error");
453 assert_eq!(
454 err.source_frames()[0].reason.as_deref(),
455 Some("validation error")
456 );
457 assert_eq!(
458 err.source_frames()[0].detail.as_deref(),
459 Some("invalid port")
460 );
461 assert_eq!(err.root_cause().unwrap().to_string(), "not a number");
462 }
463
464 #[test]
465 fn test_source_err_for_struct_error_source() {
466 let inner: Result<(), StructError<UnifiedReason>> =
468 Err(StructError::from(UnifiedReason::validation_error()).with_detail("inner detail"));
469
470 let outer: Result<(), StructError<UnifiedReason>> =
471 inner.source_err(UnifiedReason::system_error(), "outer wrapper");
472
473 let err = outer.unwrap_err();
474 assert_eq!(err.detail().as_deref(), Some("outer wrapper"));
475
476 let frames = err.source_frames();
478 assert_eq!(frames[0].message, "validation error");
479 assert_eq!(frames[0].detail.as_deref(), Some("inner detail"));
480 }
481}