studiole_report/
report.rs1use crate::prelude::*;
3
4pub struct Report<T> {
6 pub(crate) context: T,
8 pub(crate) attachments: Vec<(String, String)>,
10 pub(crate) source: Option<Box<dyn StdError + Send + Sync>>,
12}
13
14impl<T: StdError + Send + Sync + 'static> Report<T> {
15 pub fn new(context: T) -> Self {
17 Self {
18 context,
19 attachments: Vec::new(),
20 source: None,
21 }
22 }
23
24 pub fn current_context(&self) -> &T {
26 &self.context
27 }
28
29 pub fn change_context<U: StdError + Send + Sync + 'static>(self, new_context: U) -> Report<U> {
31 Report {
32 context: new_context,
33 attachments: Vec::new(),
34 source: Some(Box::new(self)),
35 }
36 }
37
38 #[must_use]
40 pub fn attach(mut self, key: impl Into<String>, value: impl Display) -> Self {
41 self.attachments.push((key.into(), value.to_string()));
42 self
43 }
44
45 #[must_use]
47 pub fn attach_path(mut self, value: impl AsRef<Path>) -> Self {
48 self.attachments.push((
49 "path".to_owned(),
50 value.as_ref().to_string_lossy().to_string(),
51 ));
52 self
53 }
54
55 #[must_use]
57 pub fn attach_with<D: Display>(
58 mut self,
59 key: impl Into<String>,
60 value: impl FnOnce() -> D,
61 ) -> Self {
62 self.attachments.push((key.into(), value().to_string()));
63 self
64 }
65}
66
67impl<T: StdError + Send + Sync + 'static> From<T> for Report<T> {
68 fn from(error: T) -> Self {
69 Self::new(error)
70 }
71}
72
73impl<T: StdError + Send + Sync + 'static> Debug for Report<T> {
74 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
75 Display::fmt(self, f)?;
76 let mut source = self.source();
77 while let Some(err) = source {
78 write!(f, "\n Caused by: {err}")?;
79 source = err.source();
80 }
81 Ok(())
82 }
83}
84
85impl<T: Display> Display for Report<T> {
86 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
87 Display::fmt(&self.context, f)?;
88 for (key, value) in &self.attachments {
89 write!(f, "\n▷ {key}: {value}")?;
90 }
91 Ok(())
92 }
93}
94
95impl<T: StdError + Send + Sync + 'static> StdError for Report<T> {
96 #[expect(
97 clippy::as_conversions,
98 reason = "cast from boxed trait object to trait reference"
99 )]
100 fn source(&self) -> Option<&(dyn StdError + 'static)> {
101 self.source
102 .as_ref()
103 .map(|s| s.as_ref() as &(dyn StdError + 'static))
104 }
105}
106
107impl<T: StdError + Send + Sync + 'static> Diagnostic for Report<T> {
108 fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
109 Some(Box::new(short_code(&self.context)))
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn report_display() {
119 let report = Report::new(OuterError::Operation);
121 let display = report.to_string();
123 assert_snapshot!(display, @"Outer operation failed");
125 }
126
127 #[test]
128 fn report_display__with_attach() {
129 let report = Report::new(OuterError::Operation)
131 .attach("path", "/tmp/file.txt")
132 .attach("retries", 3);
133 let display = report.to_string();
135 assert_snapshot!(display);
137 }
138
139 #[test]
140 fn report_display__with_attach_with() {
141 let report = Report::new(OuterError::Operation).attach_with("count", || 42);
143 let display = report.to_string();
145 assert_snapshot!(display);
147 }
148
149 #[test]
150 fn report_debug() {
151 let report = Report::new(OuterError::Operation);
153 let debug = format!("{report:?}");
155 assert_snapshot!(debug, @"Outer operation failed");
157 }
158
159 #[test]
160 fn report_debug__with_source() {
161 let inner = Report::new(InnerError::Operation);
163 let outer = inner.change_context(OuterError::Operation);
164 let debug = format!("{outer:?}");
166 assert_snapshot!(debug);
168 }
169
170 #[test]
171 fn report_debug__with_additional_and_source() {
172 let inner = Report::new(InnerError::Operation).attach("key", "inner_val");
174 let outer = inner
175 .change_context(OuterError::Operation)
176 .attach("key", "outer_val");
177 let debug = format!("{outer:?}");
179 assert_snapshot!(debug);
181 }
182
183 #[test]
184 fn report_source__none_without_wrapping() {
185 let report = Report::new(OuterError::Operation);
187 let source = report.source();
189 assert!(source.is_none());
191 }
192
193 #[test]
194 fn report_source__set_after_change_context() {
195 let inner = Report::new(InnerError::Operation);
197 let outer = inner.change_context(OuterError::Operation);
198 let source = outer.source().expect("should have source");
200 assert_eq!(source.to_string(), "Inner operation failed");
202 }
203
204 #[test]
205 fn report_change_context__preserves_context() {
206 let inner = Report::new(InnerError::Operation);
208 let outer = inner.change_context(OuterError::Operation);
210 assert_eq!(*outer.current_context(), OuterError::Operation);
212 }
213
214 #[test]
215 fn report_change_context__clears_additional() {
216 let inner = Report::new(InnerError::Operation).attach("key", "value");
218 let outer = inner.change_context(OuterError::Operation);
220 assert!(outer.attachments.is_empty());
222 }
223
224 #[test]
225 fn report_attach_path() {
226 let report = Report::new(OuterError::Operation).attach_path("/tmp/data.bin");
228 let display = report.to_string();
230 assert_snapshot!(display);
232 }
233
234 #[test]
235 fn report_from__converts_error_via_question_mark() {
236 fn fallible() -> Result<(), OuterError> {
238 Err(OuterError::Operation)
239 }
240 fn wrapper() -> Result<(), Report<OuterError>> {
241 fallible()?;
242 Ok(())
243 }
244 let report = wrapper().expect_err("should be err");
246 assert_eq!(*report.current_context(), OuterError::Operation);
248 assert!(report.source().is_none());
249 }
250}