Skip to main content

orion_error/traits/
into_as.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 `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        use crate::core::OwnedDynStdStructError;
183
184        match self.downcast::<OwnedDynStdStructError>() {
185            Ok(source) => StructError::from(reason)
186                .with_detail(detail)
187                .with_dyn_struct_source(source),
188            Err(err) => attach_std_source(AnyhowStdSource(err), reason, detail),
189        }
190    }
191}
192
193#[cfg(feature = "serde_json")]
194impl RawStdError for serde_json::Error {}
195
196#[cfg(feature = "serde_json")]
197impl private::Sealed for serde_json::Error {}
198
199#[cfg(feature = "serde_json")]
200impl UnstructuredSource for serde_json::Error {
201    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
202    where
203        R: DomainReason,
204    {
205        attach_std_source(self, reason, detail)
206    }
207}
208
209#[cfg(feature = "toml")]
210impl RawStdError for toml::de::Error {}
211
212#[cfg(feature = "toml")]
213impl private::Sealed for toml::de::Error {}
214
215#[cfg(feature = "toml")]
216impl UnstructuredSource for toml::de::Error {
217    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
218    where
219        R: DomainReason,
220    {
221        attach_std_source(self, reason, detail)
222    }
223}
224
225#[cfg(feature = "toml")]
226impl RawStdError for toml::ser::Error {}
227
228#[cfg(feature = "toml")]
229impl private::Sealed for toml::ser::Error {}
230
231#[cfg(feature = "toml")]
232impl UnstructuredSource for toml::ser::Error {
233    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
234    where
235        R: DomainReason,
236    {
237        attach_std_source(self, reason, detail)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use std::{fmt, io};
244
245    use super::{raw_source, IntoAs, RawStdError};
246    #[cfg(feature = "anyhow")]
247    use crate::StructError;
248    use crate::UvsReason;
249
250    #[derive(Debug)]
251    struct ThirdPartyError(&'static str);
252
253    impl fmt::Display for ThirdPartyError {
254        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255            write!(f, "{}", self.0)
256        }
257    }
258
259    impl std::error::Error for ThirdPartyError {}
260
261    impl RawStdError for ThirdPartyError {}
262
263    #[test]
264    fn test_into_as_for_io_error() {
265        let result: Result<(), io::Error> = Err(io::Error::other("disk offline"));
266
267        let err = result
268            .into_as(UvsReason::system_error(), "load config failed")
269            .expect_err("expected structured error");
270
271        assert_eq!(err.detail().as_deref(), Some("load config failed"));
272        assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
273    }
274
275    #[test]
276    fn test_into_as_for_raw_source_wrapper() {
277        let result: Result<(), ThirdPartyError> = Err(ThirdPartyError("parser aborted"));
278
279        let err = result
280            .map_err(raw_source)
281            .into_as(UvsReason::validation_error(), "parse config failed")
282            .expect_err("expected structured error");
283
284        assert_eq!(err.detail().as_deref(), Some("parse config failed"));
285        assert_eq!(err.source_ref().unwrap().to_string(), "parser aborted");
286    }
287
288    #[cfg(feature = "anyhow")]
289    #[test]
290    fn test_into_as_for_anyhow_defaults_to_unstructured_source() {
291        let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("network offline"));
292
293        let err = result
294            .into_as(UvsReason::system_error(), "load config failed")
295            .expect_err("expected structured error");
296
297        assert_eq!(err.detail().as_deref(), Some("load config failed"));
298        assert_eq!(err.source_ref().unwrap().to_string(), "network offline");
299        assert_eq!(err.source_frames()[0].message, "network offline");
300    }
301
302    #[cfg(feature = "anyhow")]
303    #[test]
304    fn test_into_as_for_anyhow_extracts_top_level_official_dyn_bridge() {
305        let structured = StructError::from(UvsReason::validation_error())
306            .with_detail("invalid port")
307            .with_std_source(io::Error::other("not a number"));
308        let structured_display = structured.to_string();
309        let result: Result<(), anyhow::Error> = Err(anyhow::Error::new(structured.into_dyn_std()));
310
311        let err = result
312            .into_as(UvsReason::system_error(), "load config failed")
313            .expect_err("expected structured error");
314
315        assert_eq!(err.detail().as_deref(), Some("load config failed"));
316        assert_eq!(err.source_ref().unwrap().to_string(), structured_display);
317        assert_eq!(err.source_frames()[0].message, "validation error");
318        assert_eq!(
319            err.source_frames()[0].reason.as_deref(),
320            Some("validation error")
321        );
322        assert_eq!(
323            err.source_frames()[0].detail.as_deref(),
324            Some("invalid port")
325        );
326        assert_eq!(err.root_cause().unwrap().to_string(), "not a number");
327    }
328}