Skip to main content

studiole_report/
structured_error.rs

1//! Type-erased error with chained sources, attachments, and diagnostics.
2
3use crate::prelude::*;
4
5/// Type-erased error with chained sources, attachments, and diagnostic rendering.
6pub struct StructuredError {
7    /// Boxed error context.
8    pub(crate) context: Box<dyn StdError + Send + Sync + 'static>,
9    /// Pre-captured diagnostic code from the original typed context.
10    code: String,
11    /// Key-value pairs of additional context.
12    pub(crate) attached: Attached,
13    /// Underlying error in the source chain.
14    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
15}
16
17impl StructuredError {
18    /// Create from any error, capturing its diagnostic code before type erasure.
19    pub fn new<T: StdError + Send + Sync + 'static>(context: T) -> Self {
20        let code = short_code(&context);
21        Self {
22            context: Box::new(context),
23            code,
24            attached: Attached::new(),
25            source: None,
26        }
27    }
28
29    /// Wrap this error as the source of a new context.
30    #[must_use]
31    pub fn change_context<T: StdError + Send + Sync + 'static>(
32        self,
33        new_context: T,
34    ) -> StructuredError {
35        StructuredError {
36            source: Some(Box::new(self)),
37            ..StructuredError::new(new_context)
38        }
39    }
40
41    /// Get the current context.
42    #[must_use]
43    pub fn current_context(&self) -> &(dyn StdError + Send + Sync + 'static) {
44        self.context.as_ref()
45    }
46}
47
48impl Display for StructuredError {
49    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
50        Display::fmt(&self.context, f)?;
51        write!(f, "{}", self.attached)?;
52        Ok(())
53    }
54}
55
56impl Debug for StructuredError {
57    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
58        Display::fmt(self, f)?;
59        let mut source = self.source();
60        while let Some(err) = source {
61            write!(f, "\n  Caused by: {err}")?;
62            source = err.source();
63        }
64        Ok(())
65    }
66}
67
68impl StdError for StructuredError {
69    #[expect(
70        clippy::as_conversions,
71        reason = "cast from boxed trait object to trait reference"
72    )]
73    fn source(&self) -> Option<&(dyn StdError + 'static)> {
74        self.source
75            .as_ref()
76            .map(|s| s.as_ref() as &(dyn StdError + 'static))
77    }
78}
79
80impl Diagnostic for StructuredError {
81    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
82        Some(Box::new(self.code.as_str()))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn structured_error_display() {
92        // Arrange
93        let error = StructuredError::new(OuterError::Operation);
94        // Act
95        let display = error.to_string();
96        // Assert
97        assert_snapshot!(display, @"Outer operation failed");
98    }
99
100    #[test]
101    fn structured_error_display__with_attach() {
102        // Arrange
103        let error = StructuredError::new(OuterError::Operation)
104            .attach("path", "/tmp/file.txt")
105            .attach("retries", 3);
106        // Act
107        let display = error.to_string();
108        // Assert
109        assert_snapshot!(display);
110    }
111
112    #[test]
113    fn structured_error_debug__with_source() {
114        // Arrange
115        let inner = StructuredError::new(InnerError::Operation);
116        let outer = inner.change_context(OuterError::Operation);
117        // Act
118        let debug = format!("{outer:?}");
119        // Assert
120        assert_snapshot!(debug);
121    }
122
123    #[test]
124    fn structured_error_diagnostic_code() {
125        // Arrange
126        let error = StructuredError::new(OuterError::Operation);
127        // Act
128        let code = error.code().expect("should have code").to_string();
129        // Assert
130        assert_eq!(code, "studiole_report::OuterError::Operation");
131    }
132
133    #[test]
134    fn structured_error_current_context() {
135        // Arrange
136        let error = StructuredError::new(OuterError::Operation);
137        // Act
138        let context = error.current_context();
139        // Assert
140        let downcast = context
141            .downcast_ref::<OuterError>()
142            .expect("should be OuterError");
143        assert_eq!(downcast, &OuterError::Operation);
144    }
145}