Skip to main content

orion_error/traits/
into_as.rs

1use std::{error::Error as StdError, fmt};
2
3use crate::{core::{DomainReason, OwnedDynStdStructError}, 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 `into_as(...)`.
14///
15/// Implement this trait only for genuine non-structured raw error types.
16/// Do not implement it for wrappers around `StructError<_>`.
17pub trait RawStdError: StdError + Send + Sync + 'static {}
18
19#[doc(hidden)]
20pub trait UnstructuredSource: private::Sealed {
21    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
22    where
23        R: DomainReason;
24}
25
26pub trait IntoAs<T, R: DomainReason>: Sized {
27    fn into_as(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>>;
28}
29
30#[derive(Debug)]
31pub struct RawSource<E>(E);
32
33/// Explicitly mark an opt-in raw `std::error::Error` as an unstructured source.
34///
35/// This is a narrow explicit escape hatch. It does **not** provide a blanket
36/// `E: StdError` path, and it must not be used for `StructError<_>`.
37///
38/// Downstream crates may opt in their own raw `StdError` types by implementing
39/// [`RawStdError`], instead of relying on a blanket `E: StdError` fallback.
40///
41/// ```rust
42/// use std::fmt;
43///
44/// use orion_error::{IntoAs, UvsReason};
45/// use orion_error::bridge::{raw_source, RawStdError};
46///
47/// #[derive(Debug)]
48/// struct ThirdPartyError;
49///
50/// impl fmt::Display for ThirdPartyError {
51///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52///         write!(f, "third-party failure")
53///     }
54/// }
55///
56/// impl std::error::Error for ThirdPartyError {}
57/// impl RawStdError for ThirdPartyError {}
58///
59/// let result: Result<(), ThirdPartyError> = Err(ThirdPartyError);
60/// let err = result
61///     .map_err(raw_source)
62///     .into_as(UvsReason::system_error(), "load failed")
63///     .expect_err("expected structured error");
64///
65/// assert_eq!(err.source_ref().unwrap().to_string(), "third-party failure");
66/// ```
67///
68/// ```compile_fail
69/// use orion_error::{StructError, UvsReason};
70/// use orion_error::bridge::{raw_source, RawStdError};
71///
72/// let structured = StructError::from(UvsReason::system_error());
73/// let _ = raw_source(structured);
74/// ```
75pub fn raw_source<E>(err: E) -> RawSource<E>
76where
77    E: RawStdError,
78{
79    RawSource(err)
80}
81
82impl<E> RawSource<E> {
83    pub fn into_inner(self) -> E {
84        self.0
85    }
86
87    pub fn inner(&self) -> &E {
88        &self.0
89    }
90}
91
92impl<E: fmt::Display> fmt::Display for RawSource<E> {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        self.0.fmt(f)
95    }
96}
97
98impl<E> StdError for RawSource<E>
99where
100    E: RawStdError,
101{
102    fn source(&self) -> Option<&(dyn StdError + 'static)> {
103        Some(&self.0)
104    }
105}
106
107impl<T, E, R> IntoAs<T, R> for Result<T, E>
108where
109    E: UnstructuredSource,
110    R: DomainReason,
111{
112    fn into_as(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>> {
113        let detail = detail.into();
114        self.map_err(|err| err.into_struct_error(reason, detail))
115    }
116}
117
118fn attach_std_source<E, R>(err: E, reason: R, detail: String) -> StructError<R>
119where
120    E: StdError + Send + Sync + 'static,
121    R: DomainReason,
122{
123    StructError::from(reason)
124        .with_detail(detail)
125        .with_std_source(err)
126}
127
128impl RawStdError for std::io::Error {}
129
130impl private::Sealed for std::io::Error {}
131
132impl UnstructuredSource for std::io::Error {
133    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
134    where
135        R: DomainReason,
136    {
137        attach_std_source(self, reason, detail)
138    }
139}
140
141impl<E> private::Sealed for RawSource<E> where E: RawStdError {}
142
143impl<E> UnstructuredSource for RawSource<E>
144where
145    E: RawStdError,
146{
147    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
148    where
149        R: DomainReason,
150    {
151        attach_std_source(self.0, reason, detail)
152    }
153}
154
155#[cfg(feature = "anyhow")]
156#[derive(Debug)]
157struct AnyhowStdSource(anyhow::Error);
158
159#[cfg(feature = "anyhow")]
160impl fmt::Display for AnyhowStdSource {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        self.0.fmt(f)
163    }
164}
165
166#[cfg(feature = "anyhow")]
167impl StdError for AnyhowStdSource {
168    fn source(&self) -> Option<&(dyn StdError + 'static)> {
169        self.0.source()
170    }
171}
172
173#[cfg(feature = "anyhow")]
174impl private::Sealed for anyhow::Error {}
175
176#[cfg(feature = "anyhow")]
177impl UnstructuredSource for anyhow::Error {
178    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
179    where
180        R: DomainReason,
181    {
182        match self.downcast::<OwnedDynStdStructError>() {
183            Ok(source) => StructError::from(reason)
184                .with_detail(detail)
185                .with_dyn_struct_source(source),
186            Err(err) => attach_std_source(AnyhowStdSource(err), reason, detail),
187        }
188    }
189}
190
191#[cfg(feature = "serde_json")]
192impl RawStdError for serde_json::Error {}
193
194#[cfg(feature = "serde_json")]
195impl private::Sealed for serde_json::Error {}
196
197#[cfg(feature = "serde_json")]
198impl UnstructuredSource for serde_json::Error {
199    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
200    where
201        R: DomainReason,
202    {
203        attach_std_source(self, reason, detail)
204    }
205}
206
207#[cfg(feature = "toml")]
208impl RawStdError for toml::de::Error {}
209
210#[cfg(feature = "toml")]
211impl private::Sealed for toml::de::Error {}
212
213#[cfg(feature = "toml")]
214impl UnstructuredSource for toml::de::Error {
215    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
216    where
217        R: DomainReason,
218    {
219        attach_std_source(self, reason, detail)
220    }
221}
222
223#[cfg(feature = "toml")]
224impl RawStdError for toml::ser::Error {}
225
226#[cfg(feature = "toml")]
227impl private::Sealed for toml::ser::Error {}
228
229#[cfg(feature = "toml")]
230impl UnstructuredSource for toml::ser::Error {
231    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
232    where
233        R: DomainReason,
234    {
235        attach_std_source(self, reason, detail)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use std::{fmt, io};
242
243    use super::{raw_source, IntoAs, RawStdError};
244    #[cfg(feature = "anyhow")]
245    use crate::StructError;
246    use crate::UvsReason;
247
248    #[derive(Debug)]
249    struct ThirdPartyError(&'static str);
250
251    impl fmt::Display for ThirdPartyError {
252        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253            write!(f, "{}", self.0)
254        }
255    }
256
257    impl std::error::Error for ThirdPartyError {}
258
259    impl RawStdError for ThirdPartyError {}
260
261    #[test]
262    fn test_into_as_for_io_error() {
263        let result: Result<(), io::Error> = Err(io::Error::other("disk offline"));
264
265        let err = result
266            .into_as(UvsReason::system_error(), "load config failed")
267            .expect_err("expected structured error");
268
269        assert_eq!(err.detail().as_deref(), Some("load config failed"));
270        assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
271    }
272
273    #[test]
274    fn test_into_as_for_raw_source_wrapper() {
275        let result: Result<(), ThirdPartyError> = Err(ThirdPartyError("parser aborted"));
276
277        let err = result
278            .map_err(raw_source)
279            .into_as(UvsReason::validation_error(), "parse config failed")
280            .expect_err("expected structured error");
281
282        assert_eq!(err.detail().as_deref(), Some("parse config failed"));
283        assert_eq!(err.source_ref().unwrap().to_string(), "parser aborted");
284    }
285
286    #[cfg(feature = "anyhow")]
287    #[test]
288    fn test_into_as_for_anyhow_defaults_to_unstructured_source() {
289        let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("network offline"));
290
291        let err = result
292            .into_as(UvsReason::system_error(), "load config failed")
293            .expect_err("expected structured error");
294
295        assert_eq!(err.detail().as_deref(), Some("load config failed"));
296        assert_eq!(err.source_ref().unwrap().to_string(), "network offline");
297        assert_eq!(err.source_frames()[0].message, "network offline");
298    }
299
300    #[cfg(feature = "anyhow")]
301    #[test]
302    fn test_into_as_for_anyhow_extracts_top_level_official_dyn_bridge() {
303        let structured = StructError::from(UvsReason::validation_error())
304            .with_detail("invalid port")
305            .with_std_source(io::Error::other("not a number"));
306        let structured_display = structured.to_string();
307        let result: Result<(), anyhow::Error> = Err(anyhow::Error::new(structured.into_dyn_std()));
308
309        let err = result
310            .into_as(UvsReason::system_error(), "load config failed")
311            .expect_err("expected structured error");
312
313        assert_eq!(err.detail().as_deref(), Some("load config failed"));
314        assert_eq!(err.source_ref().unwrap().to_string(), structured_display);
315        assert_eq!(err.source_frames()[0].message, "validation error");
316        assert_eq!(
317            err.source_frames()[0].reason.as_deref(),
318            Some("validation error")
319        );
320        assert_eq!(
321            err.source_frames()[0].detail.as_deref(),
322            Some("invalid port")
323        );
324        assert_eq!(err.root_cause().unwrap().to_string(), "not a number");
325    }
326}