Skip to main content

orion_error/traits/
source_err.rs

1use std::{error::Error as StdError, fmt};
2
3use crate::{core::DomainReason, StructError};
4
5mod private {
6    pub trait Sealed {}
7}
8
9/// Marker trait for explicitly opt-in raw `std::error::Error` sources.
10///
11/// This is the explicit escape hatch for downstream crates that have their own raw
12/// `StdError` types and want to route them through `raw_source(...)` before
13/// calling `source_err(...)`.
14///
15/// # Example
16///
17/// ```rust
18/// use orion_error::interop::{raw_source, RawStdError};
19/// use orion_error::prelude::*;
20/// use orion_error::UnifiedReason;
21///
22/// #[derive(Debug)]
23/// struct MyError;
24///
25/// impl std::fmt::Display for MyError {
26///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27///         write!(f, "my error")
28///     }
29/// }
30///
31/// impl std::error::Error for MyError {}
32/// impl RawStdError for MyError {}
33///
34/// let result: Result<(), MyError> = Err(MyError);
35/// let err = result
36///     .map_err(raw_source)
37///     .source_err(UnifiedReason::system_error(), "my operation failed")
38///     .unwrap_err();
39///
40/// assert_eq!(err.source_ref().unwrap().to_string(), "my error");
41/// ```
42///
43/// Implement this trait only for genuine non-structured raw error types.
44/// Do not implement it for wrappers around `StructError<_>`.
45pub 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
54/// Convert a `Result<T, E>` into `Result<T, StructError<R>>`, attaching the
55/// original error as the source of the new structured error.
56///
57/// Works for both raw `std::error::Error` types and already-structured
58/// `StructError` sources.
59///
60/// # Example
61///
62/// ```rust
63/// use orion_error::prelude::*;
64/// use orion_error::UnifiedReason;
65///
66/// let result: Result<(), std::io::Error> = Err(std::io::Error::other("disk offline"));
67/// let err: Result<(), StructError<UnifiedReason>> =
68///     result.source_err(UnifiedReason::system_error(), "read config");
69/// assert!(err.unwrap_err().detail().as_deref() == Some("read config"));
70/// ```
71///
72/// # Note on trait resolution
73///
74/// There are two blanket impls: one for raw `StdError` sources (via
75/// `UnstructuredSource`), and one for already-structured `StructError`
76/// sources. These do not conflict because `StructError<R>` does not
77/// implement `UnstructuredSource`.
78pub 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
85/// Explicitly mark an opt-in raw `std::error::Error` as an unstructured source.
86///
87/// This is a narrow explicit escape hatch. It does **not** provide a blanket
88/// `E: StdError` path, and it must not be used for `StructError<_>`.
89///
90/// Downstream crates may opt in their own raw `StdError` types by implementing
91/// [`RawStdError`], instead of relying on a blanket `E: StdError` fallback.
92///
93/// ```rust
94/// use std::fmt;
95///
96/// use orion_error::prelude::*;
97/// use orion_error::UnifiedReason;
98/// use orion_error::interop::{raw_source, RawStdError};
99///
100/// #[derive(Debug)]
101/// struct ThirdPartyError;
102///
103/// impl fmt::Display for ThirdPartyError {
104///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105///         write!(f, "third-party failure")
106///     }
107/// }
108///
109/// impl std::error::Error for ThirdPartyError {}
110/// impl RawStdError for ThirdPartyError {}
111///
112/// let result: Result<(), ThirdPartyError> = Err(ThirdPartyError);
113/// let err = result
114///     .map_err(raw_source)
115///     .source_err(UnifiedReason::system_error(), "load failed")
116///     .expect_err("expected structured error");
117///
118/// assert_eq!(err.source_ref().unwrap().to_string(), "third-party failure");
119/// ```
120///
121/// ```compile_fail
122/// use orion_error::{StructError, UnifiedReason};
123/// use orion_error::interop::{raw_source, RawStdError};
124///
125/// let structured = StructError::from(UnifiedReason::system_error());
126/// let _ = raw_source(structured);
127/// ```
128pub 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// Requires feature: "serde_json"
247#[cfg(feature = "serde_json")]
248impl RawStdError for serde_json::Error {}
249
250// Requires feature: "serde_json"
251#[cfg(feature = "serde_json")]
252impl private::Sealed for serde_json::Error {}
253
254// Requires feature: "serde_json"
255#[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
297/// Wrapper that implements `RawStdError` for any `StdError`.
298///
299/// This is the bridge for third-party error types that don't have a
300/// dedicated `UnstructuredSource` impl.  Downstream code rarely needs to
301/// name this type directly; use [`any_err`] instead.
302pub 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
335/// Wrap any `StdError` so it can enter `.source_err()`.
336pub fn any_err<E: StdError + Send + Sync + 'static>(e: E) -> AnyErr<E> {
337    AnyErr(e)
338}
339
340/// Like [`SourceErr::source_err`] but accepts **any** `StdError`.
341///
342/// Internally wraps the error with [`any_err`] so the sealed
343/// `UnstructuredSource` bound is satisfied without weakening the
344/// sealing contract.
345pub 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
359// Allow `source_err` to work with already-structured `Result<T, StructError<R1>>`
360// sources. Callers no longer need to distinguish between raw std errors and
361// structured errors — `source_err` handles both.
362impl<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        // source_err works on Result<T, StructError<R1>> too (wraps as struct source)
467        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        // The inner error becomes a structured source
477        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}