1use std::{error::Error as StdError, fmt};
2
3use crate::{core::{DomainReason, OwnedDynStdStructError}, 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 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}