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// Allow `source_err` to work with already-structured `Result<T, StructError<R1>>`
298// sources. Callers no longer need to distinguish between raw std errors and
299// structured errors — `source_err` handles both.
300impl<T, R1, R2> SourceErr<T, R2> for Result<T, StructError<R1>>
301where
302    R1: DomainReason,
303    R2: DomainReason,
304{
305    fn source_err(self, reason: R2, detail: impl Into<String>) -> Result<T, StructError<R2>> {
306        let detail = detail.into();
307        self.map_err(|err| {
308            StructError::from(reason)
309                .with_detail(detail)
310                .with_struct_source(err)
311        })
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use std::{fmt, io};
318
319    use super::{raw_source, RawStdError, SourceErr};
320    use crate::StructError;
321    use crate::UnifiedReason;
322
323    #[derive(Debug)]
324    struct ThirdPartyError(&'static str);
325
326    impl fmt::Display for ThirdPartyError {
327        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328            write!(f, "{}", self.0)
329        }
330    }
331
332    impl std::error::Error for ThirdPartyError {}
333
334    impl RawStdError for ThirdPartyError {}
335
336    #[test]
337    fn test_source_err_for_io_error() {
338        let result: Result<(), io::Error> = Err(io::Error::other("disk offline"));
339
340        let err = result
341            .source_err(UnifiedReason::system_error(), "load config failed")
342            .expect_err("expected structured error");
343
344        assert_eq!(err.detail().as_deref(), Some("load config failed"));
345        assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
346    }
347
348    #[test]
349    fn test_source_err_for_raw_source_wrapper() {
350        let result: Result<(), ThirdPartyError> = Err(ThirdPartyError("parser aborted"));
351
352        let err = result
353            .map_err(raw_source)
354            .source_err(UnifiedReason::validation_error(), "parse config failed")
355            .expect_err("expected structured error");
356
357        assert_eq!(err.detail().as_deref(), Some("parse config failed"));
358        assert_eq!(err.source_ref().unwrap().to_string(), "parser aborted");
359    }
360
361    #[cfg(feature = "anyhow")]
362    #[test]
363    fn test_source_err_for_anyhow_defaults_to_unstructured_source() {
364        let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("network offline"));
365
366        let err = result
367            .source_err(UnifiedReason::system_error(), "load config failed")
368            .expect_err("expected structured error");
369
370        assert_eq!(err.detail().as_deref(), Some("load config failed"));
371        assert_eq!(err.source_ref().unwrap().to_string(), "network offline");
372        assert_eq!(err.source_frames()[0].message, "network offline");
373    }
374
375    #[cfg(feature = "anyhow")]
376    #[test]
377    fn test_source_err_for_anyhow_extracts_top_level_official_dyn_bridge() {
378        let structured = StructError::from(UnifiedReason::validation_error())
379            .with_detail("invalid port")
380            .with_std_source(io::Error::other("not a number"));
381        let structured_display = structured.to_string();
382        let result: Result<(), anyhow::Error> = Err(anyhow::Error::new(structured.into_dyn_std()));
383
384        let err = result
385            .source_err(UnifiedReason::system_error(), "load config failed")
386            .expect_err("expected structured error");
387
388        assert_eq!(err.detail().as_deref(), Some("load config failed"));
389        assert_eq!(err.source_ref().unwrap().to_string(), structured_display);
390        assert_eq!(err.source_frames()[0].message, "validation error");
391        assert_eq!(
392            err.source_frames()[0].reason.as_deref(),
393            Some("validation error")
394        );
395        assert_eq!(
396            err.source_frames()[0].detail.as_deref(),
397            Some("invalid port")
398        );
399        assert_eq!(err.root_cause().unwrap().to_string(), "not a number");
400    }
401
402    #[test]
403    fn test_source_err_for_struct_error_source() {
404        // source_err works on Result<T, StructError<R1>> too (wraps as struct source)
405        let inner: Result<(), StructError<UnifiedReason>> =
406            Err(StructError::from(UnifiedReason::validation_error()).with_detail("inner detail"));
407
408        let outer: Result<(), StructError<UnifiedReason>> =
409            inner.source_err(UnifiedReason::system_error(), "outer wrapper");
410
411        let err = outer.unwrap_err();
412        assert_eq!(err.detail().as_deref(), Some("outer wrapper"));
413
414        // The inner error becomes a structured source
415        let frames = err.source_frames();
416        assert_eq!(frames[0].message, "validation error");
417        assert_eq!(frames[0].detail.as_deref(), Some("inner detail"));
418    }
419}