1use std::{error::Error as StdError, fmt};
2
3use crate::{core::DomainReason, StructError};
4
5mod private {
6 pub trait Sealed {}
7}
8
9pub 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
33pub 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}