Skip to main content

studiole_report/
report.rs

1//! Typed error report wrapping [`StructuredError`].
2use crate::prelude::*;
3use std::marker::PhantomData;
4use std::ops::Deref;
5
6/// Typed error report that wraps a [`StructuredError`] with compile-time type information.
7pub struct Report<T> {
8    /// The underlying type-erased error.
9    pub(crate) inner: StructuredError,
10    /// Marker for the typed context.
11    _marker: PhantomData<T>,
12}
13
14impl<T: StdError + Send + Sync + 'static> Report<T> {
15    /// Create a report from the given error context with no source.
16    pub fn new(context: T) -> Self {
17        Self {
18            inner: StructuredError::new(context),
19            _marker: PhantomData,
20        }
21    }
22
23    /// Create a report from a pre-built [`StructuredError`].
24    pub(crate) fn from_inner(inner: StructuredError) -> Self {
25        Self {
26            inner,
27            _marker: PhantomData,
28        }
29    }
30
31    /// Get the current context.
32    #[must_use]
33    pub fn current_context(&self) -> &T {
34        self.inner
35            .context
36            .downcast_ref::<T>()
37            .expect("Report<T> inner context should be of type T")
38    }
39
40    /// Wrap this report as the source of a new context.
41    pub fn change_context<U: StdError + Send + Sync + 'static>(self, new_context: U) -> Report<U> {
42        Report {
43            inner: self.inner.change_context(new_context),
44            _marker: PhantomData,
45        }
46    }
47}
48
49impl<T: StdError + Send + Sync + 'static> From<Report<T>> for StructuredError {
50    fn from(report: Report<T>) -> Self {
51        report.inner
52    }
53}
54
55impl<T: StdError + Send + Sync + 'static> Deref for Report<T> {
56    type Target = StructuredError;
57
58    fn deref(&self) -> &StructuredError {
59        &self.inner
60    }
61}
62
63impl<T: StdError + Send + Sync + 'static> From<T> for Report<T> {
64    fn from(error: T) -> Self {
65        Self::new(error)
66    }
67}
68
69impl<T: StdError + Send + Sync + 'static> Debug for Report<T> {
70    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
71        Debug::fmt(&self.inner, f)
72    }
73}
74
75impl<T: StdError + Send + Sync + 'static> Display for Report<T> {
76    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
77        Display::fmt(&self.inner, f)
78    }
79}
80
81impl<T: StdError + Send + Sync + 'static> StdError for Report<T> {
82    fn source(&self) -> Option<&(dyn StdError + 'static)> {
83        self.inner.source()
84    }
85}
86
87impl<T: StdError + Send + Sync + 'static> Diagnostic for Report<T> {
88    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
89        self.inner.code()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn report_display() {
99        // Arrange
100        let report = Report::new(OuterError::Operation);
101        // Act
102        let display = report.to_string();
103        // Assert
104        assert_snapshot!(display, @"Outer operation failed");
105    }
106
107    #[test]
108    fn report_display__with_attach() {
109        // Arrange
110        let report = Report::new(OuterError::Operation)
111            .attach("path", "/tmp/file.txt")
112            .attach("retries", 3);
113        // Act
114        let display = report.to_string();
115        // Assert
116        assert_snapshot!(display);
117    }
118
119    #[test]
120    fn report_display__with_attach_with() {
121        // Arrange
122        let report = Report::new(OuterError::Operation).attach_with("count", || 42);
123        // Act
124        let display = report.to_string();
125        // Assert
126        assert_snapshot!(display);
127    }
128
129    #[test]
130    fn report_debug() {
131        // Arrange
132        let report = Report::new(OuterError::Operation);
133        // Act
134        let debug = format!("{report:?}");
135        // Assert
136        assert_snapshot!(debug, @"Outer operation failed");
137    }
138
139    #[test]
140    fn report_debug__with_source() {
141        // Arrange
142        let inner = Report::new(InnerError::Operation);
143        let outer = inner.change_context(OuterError::Operation);
144        // Act
145        let debug = format!("{outer:?}");
146        // Assert
147        assert_snapshot!(debug);
148    }
149
150    #[test]
151    fn report_debug__with_additional_and_source() {
152        // Arrange
153        let inner = Report::new(InnerError::Operation).attach("key", "inner_val");
154        let outer = inner
155            .change_context(OuterError::Operation)
156            .attach("key", "outer_val");
157        // Act
158        let debug = format!("{outer:?}");
159        // Assert
160        assert_snapshot!(debug);
161    }
162
163    #[test]
164    fn report_source__none_without_wrapping() {
165        // Arrange
166        let report = Report::new(OuterError::Operation);
167        // Act
168        let source = report.source();
169        // Assert
170        assert!(source.is_none());
171    }
172
173    #[test]
174    fn report_source__set_after_change_context() {
175        // Arrange
176        let inner = Report::new(InnerError::Operation);
177        let outer = inner.change_context(OuterError::Operation);
178        // Act
179        let source = outer.source().expect("should have source");
180        // Assert
181        assert_eq!(source.to_string(), "Inner operation failed");
182    }
183
184    #[test]
185    fn report_change_context__preserves_context() {
186        // Arrange
187        let inner = Report::new(InnerError::Operation);
188        // Act
189        let outer = inner.change_context(OuterError::Operation);
190        // Assert
191        assert_eq!(*outer.current_context(), OuterError::Operation);
192    }
193
194    #[test]
195    fn report_change_context__clears_additional() {
196        // Arrange
197        let inner = Report::new(InnerError::Operation).attach("key", "value");
198        // Act
199        let outer = inner.change_context(OuterError::Operation);
200        // Assert
201        assert_eq!(outer.inner.attached.len(), 0);
202    }
203
204    #[test]
205    fn report_attach_path() {
206        // Arrange
207        let report = Report::new(OuterError::Operation).attach_path("/tmp/data.bin");
208        // Act
209        let display = report.to_string();
210        // Assert
211        assert_snapshot!(display);
212    }
213
214    #[test]
215    fn report_from__converts_error_via_question_mark() {
216        // Arrange
217        fn fallible() -> Result<(), OuterError> {
218            Err(OuterError::Operation)
219        }
220        fn wrapper() -> Result<(), Report<OuterError>> {
221            fallible()?;
222            Ok(())
223        }
224        // Act
225        let report = wrapper().expect_err("should be err");
226        // Assert
227        assert_eq!(*report.current_context(), OuterError::Operation);
228        assert!(report.source().is_none());
229    }
230
231    #[test]
232    fn structured_error_from() {
233        // Arrange
234        let report = Report::new(OuterError::Operation).attach("key", "value");
235        // Act
236        let error = StructuredError::from(report);
237        // Assert
238        assert_eq!(error.to_string(), "Outer operation failed\n▷ key: value");
239    }
240}