1#![warn(missing_docs)]
3use super::types::ErrorReportFormat;
20use std::io::{self, Write};
21
22#[derive(Debug, Clone)]
24pub struct ErrorReportConfig {
25 pub include_message: bool,
27 pub include_source_chain: bool,
29 pub include_backtrace: bool,
31 pub include_rich_context: bool,
33 pub include_source_location: bool,
35 pub include_severity: bool,
37 pub format: ErrorReportFormat,
39 pub max_chain_depth: Option<usize>,
41 pub pretty_print_json: bool,
43 pub include_diagnostics: bool,
45}
46
47impl Default for ErrorReportConfig {
48 fn default() -> Self {
49 Self {
50 include_message: true,
51 include_source_chain: true,
52 include_backtrace: true,
53 include_rich_context: true,
54 include_source_location: true,
55 include_severity: true,
56 format: ErrorReportFormat::Plain,
57 max_chain_depth: None,
58 pretty_print_json: true,
59 include_diagnostics: true,
60 }
61 }
62}
63
64#[derive(Debug, Default)]
66pub struct ErrorReporter;
67
68impl ErrorReporter {
69 pub fn new() -> Self {
74 Self
75 }
76
77 pub fn report<W, E>(
79 &self,
80 error: &E,
81 config: &ErrorReportConfig,
82 writer: &mut W,
83 ) -> io::Result<()>
84 where
85 W: Write,
86 E: std::error::Error,
87 {
88 match config.format {
89 ErrorReportFormat::Plain => self.report_plain(error, config, writer),
90 ErrorReportFormat::Json => self.report_json(error, config, writer),
91 ErrorReportFormat::Markdown => self.report_markdown(error, config, writer),
92 ErrorReportFormat::Html => self.report_html(error, config, writer),
93 }
94 }
95
96 pub fn report_with_syntax<W, E>(
110 &self,
111 error: &E,
112 config: &ErrorReportConfig,
113 source_code: Option<&str>,
114 writer: &mut W,
115 ) -> io::Result<()>
116 where
117 W: Write,
118 E: std::error::Error,
119 {
120 self.report(error, config, writer)?;
122
123 if let Some(code) = source_code {
125 match config.format {
126 ErrorReportFormat::Plain => {
127 writeln!(writer, "\nSource Code Context:")?;
128 writeln!(writer, "-------------------")?;
129
130 for (i, line) in code.lines().enumerate() {
132 writeln!(writer, "{:4} | {}", i + 1, line)?;
133 }
134 }
135 ErrorReportFormat::Markdown => {
136 writeln!(writer, "\n### Source Code Context\n")?;
137 writeln!(writer, "```rust")?;
138 writeln!(writer, "{}", code)?;
139 writeln!(writer, "```")?;
140 }
141 ErrorReportFormat::Html => {
142 writeln!(writer, "<h3>Source Code Context</h3>")?;
143 writeln!(writer, "<pre class=\"code rust\">")?;
144
145 let escaped_code = code
147 .replace("&", "&")
148 .replace("<", "<")
149 .replace(">", ">");
150
151 writeln!(writer, "{}", escaped_code)?;
152 writeln!(writer, "</pre>")?;
153 }
154 ErrorReportFormat::Json => {
155 let escaped_code = code.replace("\"", "\\\"").replace("\n", "\\n");
159 writeln!(writer, "{{ \"source_code\": \"{}\" }}", escaped_code)?;
160 }
161 }
162 }
163
164 Ok(())
165 }
166
167 pub fn report_to_string<E>(&self, error: &E, config: &ErrorReportConfig) -> String
169 where
170 E: std::error::Error,
171 {
172 let mut buffer = Vec::new();
173 let _ = self.report(error, config, &mut buffer);
174 String::from_utf8_lossy(&buffer).to_string()
175 }
176
177 pub fn report_to_string_with_syntax<E>(
187 &self,
188 error: &E,
189 config: &ErrorReportConfig,
190 source_code: Option<&str>,
191 ) -> String
192 where
193 E: std::error::Error,
194 {
195 let mut buffer = Vec::new();
196 let _ = self.report_with_syntax(error, config, source_code, &mut buffer);
197 String::from_utf8_lossy(&buffer).to_string()
198 }
199
200 fn report_plain<W, E>(
201 &self,
202 error: &E,
203 config: &ErrorReportConfig,
204 writer: &mut W,
205 ) -> io::Result<()>
206 where
207 W: Write,
208 E: std::error::Error,
209 {
210 writeln!(writer, "Error: {}", error)?;
214
215 if config.include_source_chain {
217 let mut source = error.source();
218 let mut depth = 0;
219
220 while let Some(err) = source {
221 if let Some(max_depth) = config.max_chain_depth {
222 if depth >= max_depth {
223 writeln!(writer, "... (more causes hidden)")?;
224 break;
225 }
226 }
227
228 writeln!(writer, "Caused by: {}", err)?;
229 source = err.source();
230 depth += 1;
231 }
232 }
233
234 if config.include_backtrace {
237 }
240
241 Ok(())
242 }
243
244 fn report_json<W, E>(
245 &self,
246 error: &E,
247 config: &ErrorReportConfig,
248 writer: &mut W,
249 ) -> io::Result<()>
250 where
251 W: Write,
252 E: std::error::Error,
253 {
254 let mut json = String::from("{");
256
257 json.push_str(&format!(
259 "\"error\": \"{}\"",
260 error.to_string().replace("\"", "\\\"")
261 ));
262
263 if config.include_source_chain {
265 json.push_str(", \"causes\": [");
266 let mut source = error.source();
267 let mut is_first = true;
268 let mut depth = 0;
269
270 while let Some(err) = source {
271 if let Some(max_depth) = config.max_chain_depth {
272 if depth >= max_depth {
273 break;
274 }
275 }
276
277 if !is_first {
278 json.push_str(", ");
279 }
280 json.push_str(&format!("\"{}\"", err.to_string().replace("\"", "\\\"")));
281
282 source = err.source();
283 is_first = false;
284 depth += 1;
285 }
286 json.push_str("]");
287 }
288
289 json.push_str("}");
290
291 if config.pretty_print_json {
293 json = json
295 .replace("{", "{\n ")
296 .replace("}", "\n}")
297 .replace(", ", ",\n ");
298 }
299
300 writeln!(writer, "{}", json)?;
301 Ok(())
302 }
303
304 fn report_markdown<W, E>(
305 &self,
306 error: &E,
307 config: &ErrorReportConfig,
308 writer: &mut W,
309 ) -> io::Result<()>
310 where
311 W: Write,
312 E: std::error::Error,
313 {
314 writeln!(writer, "## Error\n\n```")?;
316 writeln!(writer, "{}", error)?;
317 writeln!(writer, "```")?;
318
319 if config.include_source_chain {
321 writeln!(writer, "\n### Causes\n")?;
322 let mut source = error.source();
323 let mut depth = 0;
324
325 while let Some(err) = source {
326 if let Some(max_depth) = config.max_chain_depth {
327 if depth >= max_depth {
328 writeln!(writer, "... (more causes hidden)")?;
329 break;
330 }
331 }
332
333 writeln!(writer, "- {}", err)?;
334 source = err.source();
335 depth += 1;
336 }
337 }
338
339 Ok(())
340 }
341
342 fn report_html<W, E>(
343 &self,
344 error: &E,
345 config: &ErrorReportConfig,
346 writer: &mut W,
347 ) -> io::Result<()>
348 where
349 W: Write,
350 E: std::error::Error,
351 {
352 writeln!(writer, "<div class=\"error\">")?;
354 writeln!(writer, " <h3>Error</h3>")?;
355 writeln!(
356 writer,
357 " <pre>{}</pre>",
358 error.to_string().replace("<", "<").replace(">", ">")
359 )?;
360
361 if config.include_source_chain {
363 writeln!(writer, " <h4>Causes</h4>")?;
364 writeln!(writer, " <ul>")?;
365
366 let mut source = error.source();
367 let mut depth = 0;
368
369 while let Some(err) = source {
370 if let Some(max_depth) = config.max_chain_depth {
371 if depth >= max_depth {
372 writeln!(writer, " <li>... (more causes hidden)</li>")?;
373 break;
374 }
375 }
376
377 writeln!(
378 writer,
379 " <li>{}</li>",
380 err.to_string().replace("<", "<").replace(">", ">")
381 )?;
382
383 source = err.source();
384 depth += 1;
385 }
386
387 writeln!(writer, " </ul>")?;
388 }
389
390 writeln!(writer, "</div>")?;
391 Ok(())
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use std::error::Error;
399 use std::fmt;
400
401 #[derive(Debug)]
403 struct TestError {
404 message: String,
405 source: Option<Box<dyn Error + Send + Sync>>,
406 }
407
408 impl fmt::Display for TestError {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 write!(f, "{}", self.message)
411 }
412 }
413
414 impl Error for TestError {
415 fn source(&self) -> Option<&(dyn Error + 'static)> {
416 self.source
417 .as_ref()
418 .map(|s| s.as_ref() as &(dyn Error + 'static))
419 }
420 }
421
422 #[test]
423 fn test_error_reporter_plain_format() {
424 let error = TestError {
426 message: "Test error message".to_string(),
427 source: None,
428 };
429
430 let reporter = ErrorReporter::new();
432 let config = ErrorReportConfig {
433 include_message: true,
434 include_source_chain: true,
435 include_backtrace: false,
436 include_rich_context: false,
437 include_source_location: false,
438 include_severity: false,
439 format: ErrorReportFormat::Plain,
440 max_chain_depth: None,
441 pretty_print_json: false,
442 include_diagnostics: false,
443 };
444
445 let report = reporter.report_to_string(&error, &config);
447
448 assert!(report.contains("Test error message"));
450 }
451
452 #[test]
453 fn test_error_reporter_with_source() {
454 let source_error = TestError {
456 message: "Source error".to_string(),
457 source: None,
458 };
459
460 let error = TestError {
461 message: "Main error".to_string(),
462 source: Some(Box::new(source_error)),
463 };
464
465 let reporter = ErrorReporter::new();
467 let config = ErrorReportConfig {
468 include_message: true,
469 include_source_chain: true,
470 include_backtrace: false,
471 include_rich_context: false,
472 include_source_location: false,
473 include_severity: false,
474 format: ErrorReportFormat::Plain,
475 max_chain_depth: None,
476 pretty_print_json: false,
477 include_diagnostics: false,
478 };
479
480 let report = reporter.report_to_string(&error, &config);
482
483 assert!(report.contains("Main error"));
485 assert!(report.contains("Source error"));
486 }
487
488 #[test]
489 fn test_error_reporter_json_format() {
490 let error = TestError {
492 message: "JSON test error".to_string(),
493 source: None,
494 };
495
496 let reporter = ErrorReporter::new();
498 let config = ErrorReportConfig {
499 format: ErrorReportFormat::Json,
500 ..Default::default()
501 };
502
503 let report = reporter.report_to_string(&error, &config);
505
506 assert!(report.starts_with("{"));
508 assert!(report.ends_with("}\n") || report.ends_with("}"));
509 assert!(report.contains("\"error\""));
510 assert!(report.contains("JSON test error"));
511 }
512
513 #[test]
514 fn test_error_reporter_with_syntax() {
515 let error = TestError {
517 message: "Syntax error in code".to_string(),
518 source: None,
519 };
520
521 let source_code = r#"
523fn main() {
524 let x: i32 = "not an integer"; // Type mismatch error
525 println!("Value: {}", x);
526}
527"#;
528
529 let reporter = ErrorReporter::new();
531 let config = ErrorReportConfig {
532 format: ErrorReportFormat::Markdown,
533 ..Default::default()
534 };
535
536 let report = reporter.report_to_string_with_syntax(&error, &config, Some(source_code));
538
539 assert!(report.contains("Syntax error in code"));
541 assert!(report.contains("Source Code Context"));
542 assert!(report.contains("```rust"));
543 assert!(report.contains("let x: i32 = \"not an integer\";"));
544 }
545}