Skip to main content

orion_error/traits/
into_as.rs

1use std::{error::Error as StdError, fmt};
2
3use crate::{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 V1 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 V1 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::{raw_source, IntoAs, RawStdError, UvsReason};
45///
46/// #[derive(Debug)]
47/// struct ThirdPartyError;
48///
49/// impl fmt::Display for ThirdPartyError {
50///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51///         write!(f, "third-party failure")
52///     }
53/// }
54///
55/// impl std::error::Error for ThirdPartyError {}
56/// impl RawStdError for ThirdPartyError {}
57///
58/// let result: Result<(), ThirdPartyError> = Err(ThirdPartyError);
59/// let err = result
60///     .map_err(raw_source)
61///     .into_as(UvsReason::system_error(), "load failed")
62///     .expect_err("expected structured error");
63///
64/// assert_eq!(err.source_ref().unwrap().to_string(), "third-party failure");
65/// ```
66///
67/// ```compile_fail
68/// use orion_error::{raw_source, RawStdError, StructError, UvsReason};
69///
70/// let structured = StructError::from(UvsReason::system_error());
71/// let _ = raw_source(structured);
72/// ```
73pub fn raw_source<E>(err: E) -> RawSource<E>
74where
75    E: RawStdError,
76{
77    RawSource(err)
78}
79
80impl<E> RawSource<E> {
81    pub fn into_inner(self) -> E {
82        self.0
83    }
84
85    pub fn inner(&self) -> &E {
86        &self.0
87    }
88}
89
90impl<E: fmt::Display> fmt::Display for RawSource<E> {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        self.0.fmt(f)
93    }
94}
95
96impl<E> StdError for RawSource<E>
97where
98    E: RawStdError,
99{
100    fn source(&self) -> Option<&(dyn StdError + 'static)> {
101        Some(&self.0)
102    }
103}
104
105impl<T, E, R> IntoAs<T, R> for Result<T, E>
106where
107    E: UnstructuredSource,
108    R: DomainReason,
109{
110    fn into_as(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>> {
111        let detail = detail.into();
112        self.map_err(|err| err.into_struct_error(reason, detail))
113    }
114}
115
116fn attach_std_source<E, R>(err: E, reason: R, detail: String) -> StructError<R>
117where
118    E: StdError + Send + Sync + 'static,
119    R: DomainReason,
120{
121    StructError::from(reason)
122        .with_detail(detail)
123        .with_std_source(err)
124}
125
126impl RawStdError for std::io::Error {}
127
128impl private::Sealed for std::io::Error {}
129
130impl UnstructuredSource for std::io::Error {
131    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
132    where
133        R: DomainReason,
134    {
135        attach_std_source(self, reason, detail)
136    }
137}
138
139impl<E> private::Sealed for RawSource<E> where E: RawStdError {}
140
141impl<E> UnstructuredSource for RawSource<E>
142where
143    E: RawStdError,
144{
145    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
146    where
147        R: DomainReason,
148    {
149        attach_std_source(self.0, reason, detail)
150    }
151}
152
153#[cfg(feature = "anyhow")]
154#[derive(Debug)]
155struct AnyhowStdSource(anyhow::Error);
156
157#[cfg(feature = "anyhow")]
158impl fmt::Display for AnyhowStdSource {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        self.0.fmt(f)
161    }
162}
163
164#[cfg(feature = "anyhow")]
165impl StdError for AnyhowStdSource {
166    fn source(&self) -> Option<&(dyn StdError + 'static)> {
167        self.0.source()
168    }
169}
170
171#[cfg(feature = "anyhow")]
172impl private::Sealed for anyhow::Error {}
173
174#[cfg(feature = "anyhow")]
175impl UnstructuredSource for anyhow::Error {
176    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
177    where
178        R: DomainReason,
179    {
180        attach_std_source(AnyhowStdSource(self), reason, detail)
181    }
182}
183
184#[cfg(feature = "serde_json")]
185impl RawStdError for serde_json::Error {}
186
187#[cfg(feature = "serde_json")]
188impl private::Sealed for serde_json::Error {}
189
190#[cfg(feature = "serde_json")]
191impl UnstructuredSource for serde_json::Error {
192    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
193    where
194        R: DomainReason,
195    {
196        attach_std_source(self, reason, detail)
197    }
198}
199
200#[cfg(feature = "toml")]
201impl RawStdError for toml::de::Error {}
202
203#[cfg(feature = "toml")]
204impl private::Sealed for toml::de::Error {}
205
206#[cfg(feature = "toml")]
207impl UnstructuredSource for toml::de::Error {
208    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
209    where
210        R: DomainReason,
211    {
212        attach_std_source(self, reason, detail)
213    }
214}
215
216#[cfg(feature = "toml")]
217impl RawStdError for toml::ser::Error {}
218
219#[cfg(feature = "toml")]
220impl private::Sealed for toml::ser::Error {}
221
222#[cfg(feature = "toml")]
223impl UnstructuredSource for toml::ser::Error {
224    fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
225    where
226        R: DomainReason,
227    {
228        attach_std_source(self, reason, detail)
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use std::{fmt, io};
235
236    use super::{raw_source, IntoAs, RawStdError};
237    use crate::UvsReason;
238
239    #[derive(Debug)]
240    struct ThirdPartyError(&'static str);
241
242    impl fmt::Display for ThirdPartyError {
243        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244            write!(f, "{}", self.0)
245        }
246    }
247
248    impl std::error::Error for ThirdPartyError {}
249
250    impl RawStdError for ThirdPartyError {}
251
252    #[test]
253    fn test_into_as_for_io_error() {
254        let result: Result<(), io::Error> = Err(io::Error::other("disk offline"));
255
256        let err = result
257            .into_as(UvsReason::system_error(), "load config failed")
258            .expect_err("expected structured error");
259
260        assert_eq!(err.detail().as_deref(), Some("load config failed"));
261        assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
262    }
263
264    #[test]
265    fn test_into_as_for_raw_source_wrapper() {
266        let result: Result<(), ThirdPartyError> = Err(ThirdPartyError("parser aborted"));
267
268        let err = result
269            .map_err(raw_source)
270            .into_as(UvsReason::validation_error(), "parse config failed")
271            .expect_err("expected structured error");
272
273        assert_eq!(err.detail().as_deref(), Some("parse config failed"));
274        assert_eq!(err.source_ref().unwrap().to_string(), "parser aborted");
275    }
276}