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 {}
46
47#[doc(hidden)]
48pub trait UnstructuredSource: private::Sealed {
49 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
50 where
51 R: DomainReason;
52}
53
54pub trait SourceErr<T, R: DomainReason>: Sized {
79 fn source_err(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>>;
80}
81
82#[derive(Debug)]
83pub struct RawSource<E>(E);
84
85pub fn raw_source<E>(err: E) -> RawSource<E>
129where
130 E: RawStdError,
131{
132 RawSource(err)
133}
134
135impl<E> RawSource<E> {
136 pub fn into_inner(self) -> E {
137 self.0
138 }
139
140 pub fn inner(&self) -> &E {
141 &self.0
142 }
143}
144
145impl<E: fmt::Display> fmt::Display for RawSource<E> {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 self.0.fmt(f)
148 }
149}
150
151impl<E> StdError for RawSource<E>
152where
153 E: RawStdError,
154{
155 fn source(&self) -> Option<&(dyn StdError + 'static)> {
156 Some(&self.0)
157 }
158}
159
160impl<T, E, R> SourceErr<T, R> for Result<T, E>
161where
162 E: UnstructuredSource,
163 R: DomainReason,
164{
165 fn source_err(self, reason: R, detail: impl Into<String>) -> Result<T, StructError<R>> {
166 let detail = detail.into();
167 self.map_err(|err| err.into_struct_error(reason, detail))
168 }
169}
170
171fn attach_std_source<E, R>(err: E, reason: R, detail: String) -> StructError<R>
172where
173 E: StdError + Send + Sync + 'static,
174 R: DomainReason,
175{
176 StructError::from(reason)
177 .with_detail(detail)
178 .with_std_source(err)
179}
180
181impl RawStdError for std::io::Error {}
182
183impl private::Sealed for std::io::Error {}
184
185impl UnstructuredSource for std::io::Error {
186 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
187 where
188 R: DomainReason,
189 {
190 attach_std_source(self, reason, detail)
191 }
192}
193
194impl<E> private::Sealed for RawSource<E> where E: RawStdError {}
195
196impl<E> UnstructuredSource for RawSource<E>
197where
198 E: RawStdError,
199{
200 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
201 where
202 R: DomainReason,
203 {
204 attach_std_source(self.0, reason, detail)
205 }
206}
207
208#[cfg(feature = "anyhow")]
209#[derive(Debug)]
210struct AnyhowStdSource(anyhow::Error);
211
212#[cfg(feature = "anyhow")]
213impl fmt::Display for AnyhowStdSource {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 self.0.fmt(f)
216 }
217}
218
219#[cfg(feature = "anyhow")]
220impl StdError for AnyhowStdSource {
221 fn source(&self) -> Option<&(dyn StdError + 'static)> {
222 self.0.source()
223 }
224}
225
226#[cfg(feature = "anyhow")]
227impl private::Sealed for anyhow::Error {}
228
229#[cfg(feature = "anyhow")]
230impl UnstructuredSource for anyhow::Error {
231 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
232 where
233 R: DomainReason,
234 {
235 use crate::core::OwnedDynStdStructError;
236
237 match self.downcast::<OwnedDynStdStructError>() {
238 Ok(source) => StructError::from(reason)
239 .with_detail(detail)
240 .with_dyn_struct_source(source),
241 Err(err) => attach_std_source(AnyhowStdSource(err), reason, detail),
242 }
243 }
244}
245
246#[cfg(feature = "serde_json")]
248impl RawStdError for serde_json::Error {}
249
250#[cfg(feature = "serde_json")]
252impl private::Sealed for serde_json::Error {}
253
254#[cfg(feature = "serde_json")]
256impl UnstructuredSource for serde_json::Error {
257 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
258 where
259 R: DomainReason,
260 {
261 attach_std_source(self, reason, detail)
262 }
263}
264
265#[cfg(feature = "toml")]
266impl RawStdError for toml::de::Error {}
267
268#[cfg(feature = "toml")]
269impl private::Sealed for toml::de::Error {}
270
271#[cfg(feature = "toml")]
272impl UnstructuredSource for toml::de::Error {
273 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
274 where
275 R: DomainReason,
276 {
277 attach_std_source(self, reason, detail)
278 }
279}
280
281#[cfg(feature = "toml")]
282impl RawStdError for toml::ser::Error {}
283
284#[cfg(feature = "toml")]
285impl private::Sealed for toml::ser::Error {}
286
287#[cfg(feature = "toml")]
288impl UnstructuredSource for toml::ser::Error {
289 fn into_struct_error<R>(self, reason: R, detail: String) -> StructError<R>
290 where
291 R: DomainReason,
292 {
293 attach_std_source(self, reason, detail)
294 }
295}
296
297impl<T, R1, R2> SourceErr<T, R2> for Result<T, StructError<R1>>
301where
302 R1: DomainReason,
303 R2: DomainReason,
304{
305 fn source_err(self, reason: R2, detail: impl Into<String>) -> Result<T, StructError<R2>> {
306 let detail = detail.into();
307 self.map_err(|err| {
308 StructError::from(reason)
309 .with_detail(detail)
310 .with_struct_source(err)
311 })
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use std::{fmt, io};
318
319 use super::{raw_source, RawStdError, SourceErr};
320 use crate::StructError;
321 use crate::UnifiedReason;
322
323 #[derive(Debug)]
324 struct ThirdPartyError(&'static str);
325
326 impl fmt::Display for ThirdPartyError {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 write!(f, "{}", self.0)
329 }
330 }
331
332 impl std::error::Error for ThirdPartyError {}
333
334 impl RawStdError for ThirdPartyError {}
335
336 #[test]
337 fn test_source_err_for_io_error() {
338 let result: Result<(), io::Error> = Err(io::Error::other("disk offline"));
339
340 let err = result
341 .source_err(UnifiedReason::system_error(), "load config failed")
342 .expect_err("expected structured error");
343
344 assert_eq!(err.detail().as_deref(), Some("load config failed"));
345 assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
346 }
347
348 #[test]
349 fn test_source_err_for_raw_source_wrapper() {
350 let result: Result<(), ThirdPartyError> = Err(ThirdPartyError("parser aborted"));
351
352 let err = result
353 .map_err(raw_source)
354 .source_err(UnifiedReason::validation_error(), "parse config failed")
355 .expect_err("expected structured error");
356
357 assert_eq!(err.detail().as_deref(), Some("parse config failed"));
358 assert_eq!(err.source_ref().unwrap().to_string(), "parser aborted");
359 }
360
361 #[cfg(feature = "anyhow")]
362 #[test]
363 fn test_source_err_for_anyhow_defaults_to_unstructured_source() {
364 let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("network offline"));
365
366 let err = result
367 .source_err(UnifiedReason::system_error(), "load config failed")
368 .expect_err("expected structured error");
369
370 assert_eq!(err.detail().as_deref(), Some("load config failed"));
371 assert_eq!(err.source_ref().unwrap().to_string(), "network offline");
372 assert_eq!(err.source_frames()[0].message, "network offline");
373 }
374
375 #[cfg(feature = "anyhow")]
376 #[test]
377 fn test_source_err_for_anyhow_extracts_top_level_official_dyn_bridge() {
378 let structured = StructError::from(UnifiedReason::validation_error())
379 .with_detail("invalid port")
380 .with_std_source(io::Error::other("not a number"));
381 let structured_display = structured.to_string();
382 let result: Result<(), anyhow::Error> = Err(anyhow::Error::new(structured.into_dyn_std()));
383
384 let err = result
385 .source_err(UnifiedReason::system_error(), "load config failed")
386 .expect_err("expected structured error");
387
388 assert_eq!(err.detail().as_deref(), Some("load config failed"));
389 assert_eq!(err.source_ref().unwrap().to_string(), structured_display);
390 assert_eq!(err.source_frames()[0].message, "validation error");
391 assert_eq!(
392 err.source_frames()[0].reason.as_deref(),
393 Some("validation error")
394 );
395 assert_eq!(
396 err.source_frames()[0].detail.as_deref(),
397 Some("invalid port")
398 );
399 assert_eq!(err.root_cause().unwrap().to_string(), "not a number");
400 }
401
402 #[test]
403 fn test_source_err_for_struct_error_source() {
404 let inner: Result<(), StructError<UnifiedReason>> =
406 Err(StructError::from(UnifiedReason::validation_error()).with_detail("inner detail"));
407
408 let outer: Result<(), StructError<UnifiedReason>> =
409 inner.source_err(UnifiedReason::system_error(), "outer wrapper");
410
411 let err = outer.unwrap_err();
412 assert_eq!(err.detail().as_deref(), Some("outer wrapper"));
413
414 let frames = err.source_frames();
416 assert_eq!(frames[0].message, "validation error");
417 assert_eq!(frames[0].detail.as_deref(), Some("inner detail"));
418 }
419}