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 Into<String>) -> Self {
41 self.attachments.push((key.into(), value.into()));
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(mut self, key: impl Into<String>, value: impl FnOnce() -> String) -> Self {
58 self.attachments.push((key.into(), value()));
59 self
60 }
61}
62
63impl<T: StdError + Send + Sync + 'static> Debug for Report<T> {
64 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
65 Display::fmt(self, f)?;
66 let mut source = self.source();
67 while let Some(err) = source {
68 write!(f, "\n Caused by: {err}")?;
69 source = err.source();
70 }
71 Ok(())
72 }
73}
74
75impl<T: Display> Display for Report<T> {
76 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
77 Display::fmt(&self.context, f)?;
78 for (key, value) in &self.attachments {
79 write!(f, "\n▷ {key}: {value}")?;
80 }
81 Ok(())
82 }
83}
84
85impl<T: StdError + Send + Sync + 'static> StdError for Report<T> {
86 #[expect(
87 clippy::as_conversions,
88 reason = "cast from boxed trait object to trait reference"
89 )]
90 fn source(&self) -> Option<&(dyn StdError + 'static)> {
91 self.source
92 .as_ref()
93 .map(|s| s.as_ref() as &(dyn StdError + 'static))
94 }
95}
96
97impl<T: StdError + Send + Sync + 'static> Diagnostic for Report<T> {
98 fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
99 Some(Box::new(short_code(&self.context)))
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn report_display() {
109 let report = Report::new(OuterError::Operation);
111 let display = report.to_string();
113 assert_snapshot!(display, @"Outer operation failed");
115 }
116
117 #[test]
118 fn report_display__with_attach() {
119 let report = Report::new(OuterError::Operation)
121 .attach("path", "/tmp/file.txt")
122 .attach("retries", "3");
123 let display = report.to_string();
125 assert_snapshot!(display);
127 }
128
129 #[test]
130 fn report_display__with_attach_with() {
131 let report = Report::new(OuterError::Operation).attach_with("count", || String::from("42"));
133 let display = report.to_string();
135 assert_snapshot!(display);
137 }
138
139 #[test]
140 fn report_debug() {
141 let report = Report::new(OuterError::Operation);
143 let debug = format!("{report:?}");
145 assert_snapshot!(debug, @"Outer operation failed");
147 }
148
149 #[test]
150 fn report_debug__with_source() {
151 let inner = Report::new(InnerError::Operation);
153 let outer = inner.change_context(OuterError::Operation);
154 let debug = format!("{outer:?}");
156 assert_snapshot!(debug);
158 }
159
160 #[test]
161 fn report_debug__with_additional_and_source() {
162 let inner = Report::new(InnerError::Operation).attach("key", "inner_val");
164 let outer = inner
165 .change_context(OuterError::Operation)
166 .attach("key", "outer_val");
167 let debug = format!("{outer:?}");
169 assert_snapshot!(debug);
171 }
172
173 #[test]
174 fn report_source__none_without_wrapping() {
175 let report = Report::new(OuterError::Operation);
177 let source = report.source();
179 assert!(source.is_none());
181 }
182
183 #[test]
184 fn report_source__set_after_change_context() {
185 let inner = Report::new(InnerError::Operation);
187 let outer = inner.change_context(OuterError::Operation);
188 let source = outer.source().expect("should have source");
190 assert_eq!(source.to_string(), "Inner operation failed");
192 }
193
194 #[test]
195 fn report_change_context__preserves_context() {
196 let inner = Report::new(InnerError::Operation);
198 let outer = inner.change_context(OuterError::Operation);
200 assert_eq!(*outer.current_context(), OuterError::Operation);
202 }
203
204 #[test]
205 fn report_change_context__clears_additional() {
206 let inner = Report::new(InnerError::Operation).attach("key", "value");
208 let outer = inner.change_context(OuterError::Operation);
210 assert!(outer.attachments.is_empty());
212 }
213
214 #[test]
215 fn report_attach_path() {
216 let report = Report::new(OuterError::Operation).attach_path("/tmp/data.bin");
218 let display = report.to_string();
220 assert_snapshot!(display);
222 }
223}