device_driver_diagnostics/
lib.rs1use std::{
2 borrow::Cow,
3 error::Error,
4 fmt::{Debug, Display, Write},
5 ops::Deref,
6};
7
8use annotate_snippets::{Group, Level, Renderer, Title, renderer::DecorStyle};
9use device_driver_common::span::Span;
10
11pub mod errors;
12
13#[derive(Debug)]
14pub struct Diagnostics {
15 diagnostics: Vec<Box<dyn Diagnostic>>,
16}
17
18impl Default for Diagnostics {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl Diagnostics {
25 #[must_use]
26 pub fn new() -> Self {
27 Self {
28 diagnostics: Vec::new(),
29 }
30 }
31
32 pub fn add(&mut self, diagnostic: impl Diagnostic + 'static) {
33 self.diagnostics.push(Box::new(diagnostic));
34 }
35
36 #[must_use]
37 pub fn has_error(&self) -> bool {
38 self.diagnostics
39 .iter()
40 .any(|diagnostic| diagnostic.severity() == Severity::Error)
41 }
42
43 #[must_use]
44 pub fn is_empty(&self) -> bool {
45 self.diagnostics.is_empty()
46 }
47
48 pub fn iter(&self) -> impl Iterator<Item = &dyn Diagnostic> {
49 self.diagnostics.iter().map(Box::deref)
50 }
51
52 pub fn render_for_each<E>(
53 &self,
54 metadata: Metadata<'_>,
55 mut f: impl FnMut(&dyn Diagnostic, String) -> Result<(), E>,
56 ) -> Result<(), E> {
57 let renderer = metadata.get_renderer();
58
59 for diagnostic in self.iter() {
60 let mut rendered =
61 renderer.render(&diagnostic.as_report(metadata.source, metadata.source_path));
62
63 if !metadata.ansi {
64 rendered = strip_ansi_urls(&rendered);
65 }
66
67 f(diagnostic, rendered)?;
68 }
69
70 Ok(())
71 }
72
73 pub fn print_to<W: std::io::Write>(
74 self,
75 mut writer: W,
76 metadata: Metadata<'_>,
77 ) -> std::io::Result<()> {
78 self.render_for_each(metadata, |_, rendered| writeln!(writer, "{rendered}\n"))
79 }
80
81 pub fn print_to_fmt<W: std::fmt::Write>(
82 self,
83 mut writer: W,
84 metadata: Metadata<'_>,
85 ) -> std::fmt::Result {
86 self.render_for_each(metadata, |_, rendered| writeln!(writer, "{rendered}\n"))
87 }
88}
89
90pub struct Metadata<'s> {
91 pub source: &'s str,
93 pub source_path: &'s str,
95 pub term_width: Option<usize>,
97 pub ansi: bool,
99 pub unicode: bool,
101 pub anonymized_line_numbers: bool,
103}
104
105impl Metadata<'_> {
106 fn get_renderer(&self) -> Renderer {
107 if self.ansi {
108 Renderer::styled()
109 } else {
110 Renderer::plain()
111 }
112 .term_width(
113 self.term_width
114 .unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
115 )
116 .decor_style(if self.unicode {
117 DecorStyle::Unicode
118 } else {
119 DecorStyle::Ascii
120 })
121 .anonymized_snippet_line_numbers(self.anonymized_line_numbers)
122 .anonymized_origin_line_numbers(self.anonymized_line_numbers)
123 }
124}
125
126pub fn encode_ansi_url(link: &str, name: &str) -> String {
128 format!("\x1b]8;;{link}\x1b\\{name}\x1b]8;;\x1b\\")
129}
130
131fn strip_ansi_urls(text: &str) -> String {
133 enum UrlStage {
134 None,
135 Start,
136 End,
137 }
138
139 let mut output = String::new();
140
141 let mut url_stage = UrlStage::None;
142
143 for split in text.split("\x1b]8;;") {
144 match url_stage {
145 UrlStage::None => {
146 output += split;
147 url_stage = UrlStage::Start;
148 }
149 UrlStage::Start => {
150 if let Some((link, name)) = split.split_once("\x1b\\") {
152 output += name;
153 let _ = write!(output, " ({link})");
154 url_stage = UrlStage::End;
155 } else {
156 return text.into();
159 }
160 }
161 UrlStage::End => {
162 output += split.trim_start_matches("\x1b\\");
164 url_stage = UrlStage::Start;
165 }
166 }
167 }
168
169 output
170}
171
172pub trait Diagnostic: Debug + Send {
173 fn severity(&self) -> Severity;
174 fn as_report<'a>(&'a self, source: &'a str, path: &'a str) -> Vec<Group<'a>>;
175
176 fn primary_span(&self) -> Span;
177 fn title(&self) -> Cow<'static, str>;
178
179 fn title_snippet(&self) -> Title<'static> {
180 match self.severity() {
181 Severity::Error => Level::ERROR,
182 Severity::Warning => Level::WARNING,
183 Severity::Info => Level::INFO,
184 Severity::Note => Level::NOTE,
185 Severity::Help => Level::HELP,
186 }
187 .primary_title(self.title())
188 }
189}
190
191impl<E: Error + Send> Diagnostic for E {
192 fn severity(&self) -> Severity {
193 Severity::Error
194 }
195
196 fn as_report<'a>(&'a self, _source: &'a str, _file_path: &'a str) -> Vec<Group<'a>> {
197 let mut sources = Vec::new();
198 let mut source = self.source();
199
200 while let Some(current_source) = source {
201 sources.push(Level::NOTE.message(current_source.to_string()));
202 source = current_source.source();
203 }
204
205 vec![Group::with_title(Level::ERROR.primary_title(self.to_string())).elements(sources)]
206 }
207
208 fn primary_span(&self) -> Span {
209 Span::empty()
210 }
211
212 fn title(&self) -> Cow<'static, str> {
213 self.to_string().into()
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
218pub enum Severity {
219 Error,
220 Warning,
221 Info,
222 Note,
223 Help,
224}
225
226#[derive(Debug)]
227pub struct DynError {
228 source: Option<Box<dyn Error + Send + 'static>>,
229 message: String,
230}
231
232impl DynError {
233 pub fn new(message: impl Display) -> Self {
234 Self {
235 source: None,
236 message: message.to_string(),
237 }
238 }
239
240 pub fn to_report_string(&self) -> String {
241 let mut string = String::new();
242 let report = self.as_report("", "");
243 let output = annotate_snippets::Renderer::styled().render(&report);
244 write!(&mut string, "{output}").unwrap();
245 string
246 }
247}
248
249impl Display for DynError {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 match (self.message.as_str(), self.source()) {
252 ("", Some(source)) => write!(f, "{source}"),
253 ("", None) => unreachable!(),
254 (message, _) if !f.alternate() => write!(f, "{message}"),
255 (message, None) => write!(f, "{message}"),
256 (message, Some(source)) => write!(f, "{message}\n| {source:#}"),
257 }
258 }
259}
260
261impl Error for DynError {
262 fn source(&self) -> Option<&(dyn Error + 'static)> {
263 if self.message.is_empty() {
264 None
266 } else {
267 self.source.as_ref().map(|e| {
268 let x: &(dyn Error + Send) = Box::as_ref(e);
269 let y: &dyn Error = x;
270 y
271 })
272 }
273 }
274}
275
276pub trait ErrorExt: Error + Sized + Send + 'static {
277 fn with_message(self, message: impl Display) -> DynError {
278 DynError {
279 source: Some(Box::new(self)),
280 message: message.to_string(),
281 }
282 }
283
284 fn into_dyn_error(self) -> DynError {
285 DynError {
286 source: Some(Box::new(self)),
287 message: String::new(),
288 }
289 }
290}
291
292impl<E: Error + Send + Sized + 'static> ErrorExt for E {}
293
294pub trait ResultExt<T, E: ErrorExt> {
295 fn with_message<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError>;
296 fn into_dyn_result(self) -> Result<T, DynError>;
297}
298
299impl<T, E: ErrorExt> ResultExt<T, E> for Result<T, E> {
300 fn with_message<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError> {
301 self.map_err(|e| e.with_message(f()))
302 }
303
304 fn into_dyn_result(self) -> Result<T, DynError> {
305 self.map_err(ErrorExt::into_dyn_error)
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use std::fmt::Display;
312
313 use super::*;
314
315 #[derive(Debug)]
316 struct DummyError;
317
318 impl Display for DummyError {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 write!(f, "Something went wrong!")
321 }
322 }
323
324 impl Error for DummyError {}
325
326 #[derive(Debug)]
327 struct DummyErrorWithSource(usize);
328
329 impl Display for DummyErrorWithSource {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 write!(f, "@level {} - Something deep down went wrong!", self.0)
332 }
333 }
334
335 impl Error for DummyErrorWithSource {
336 fn source(&self) -> Option<&(dyn Error + 'static)> {
337 if self.0 == 0 {
338 Some(&DummyError)
339 } else {
340 Some(Box::leak(Box::new(DummyErrorWithSource(self.0 - 1))))
341 }
342 }
343 }
344
345 #[test]
346 fn error_into_report() {
347 let report = DummyError.as_report("", "");
348 let output = annotate_snippets::Renderer::plain().render(&report);
349 pretty_assertions::assert_str_eq!("error: Something went wrong!", output);
350
351 let report = DummyErrorWithSource(5).as_report("", "");
352 let output = annotate_snippets::Renderer::plain().render(&report);
353 pretty_assertions::assert_str_eq!(
354 "error: @level 5 - Something deep down went wrong!
355 |
356 = note: @level 4 - Something deep down went wrong!
357 = note: @level 3 - Something deep down went wrong!
358 = note: @level 2 - Something deep down went wrong!
359 = note: @level 1 - Something deep down went wrong!
360 = note: @level 0 - Something deep down went wrong!
361 = note: Something went wrong!",
362 output
363 );
364 }
365
366 #[test]
367 fn dyn_error_context() {
368 let error = DummyError.with_message("Here's some context!");
369 let report = error.as_report("", "");
370 let output = annotate_snippets::Renderer::plain().render(&report);
371 pretty_assertions::assert_str_eq!(
372 "error: Here's some context!
373 |
374 = note: Something went wrong!",
375 output
376 );
377 }
378}