#![warn(missing_docs)]
use super::types::ErrorReportFormat;
use std::io::{self, Write};
#[derive(Debug, Clone)]
pub struct ErrorReportConfig {
pub include_message: bool,
pub include_source_chain: bool,
pub include_backtrace: bool,
pub include_rich_context: bool,
pub include_source_location: bool,
pub include_severity: bool,
pub format: ErrorReportFormat,
pub max_chain_depth: Option<usize>,
pub pretty_print_json: bool,
pub include_diagnostics: bool,
}
impl Default for ErrorReportConfig {
fn default() -> Self {
Self {
include_message: true,
include_source_chain: true,
include_backtrace: true,
include_rich_context: true,
include_source_location: true,
include_severity: true,
format: ErrorReportFormat::Plain,
max_chain_depth: None,
pretty_print_json: true,
include_diagnostics: true,
}
}
}
#[derive(Debug, Default)]
pub struct ErrorReporter;
impl ErrorReporter {
pub fn new() -> Self {
Self
}
pub fn report<W, E>(
&self,
error: &E,
config: &ErrorReportConfig,
writer: &mut W,
) -> io::Result<()>
where
W: Write,
E: std::error::Error,
{
match config.format {
ErrorReportFormat::Plain => self.report_plain(error, config, writer),
ErrorReportFormat::Json => self.report_json(error, config, writer),
ErrorReportFormat::Markdown => self.report_markdown(error, config, writer),
ErrorReportFormat::Html => self.report_html(error, config, writer),
}
}
pub fn report_with_syntax<W, E>(
&self,
error: &E,
config: &ErrorReportConfig,
source_code: Option<&str>,
writer: &mut W,
) -> io::Result<()>
where
W: Write,
E: std::error::Error,
{
self.report(error, config, writer)?;
if let Some(code) = source_code {
match config.format {
ErrorReportFormat::Plain => {
writeln!(writer, "\nSource Code Context:")?;
writeln!(writer, "-------------------")?;
for (i, line) in code.lines().enumerate() {
writeln!(writer, "{:4} | {}", i + 1, line)?;
}
}
ErrorReportFormat::Markdown => {
writeln!(writer, "\n### Source Code Context\n")?;
writeln!(writer, "```rust")?;
writeln!(writer, "{}", code)?;
writeln!(writer, "```")?;
}
ErrorReportFormat::Html => {
writeln!(writer, "<h3>Source Code Context</h3>")?;
writeln!(writer, "<pre class=\"code rust\">")?;
let escaped_code = code
.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
writeln!(writer, "{}", escaped_code)?;
writeln!(writer, "</pre>")?;
}
ErrorReportFormat::Json => {
let escaped_code = code.replace("\"", "\\\"").replace("\n", "\\n");
writeln!(writer, "{{ \"source_code\": \"{}\" }}", escaped_code)?;
}
}
}
Ok(())
}
pub fn report_to_string<E>(&self, error: &E, config: &ErrorReportConfig) -> String
where
E: std::error::Error,
{
let mut buffer = Vec::new();
let _ = self.report(error, config, &mut buffer);
String::from_utf8_lossy(&buffer).to_string()
}
pub fn report_to_string_with_syntax<E>(
&self,
error: &E,
config: &ErrorReportConfig,
source_code: Option<&str>,
) -> String
where
E: std::error::Error,
{
let mut buffer = Vec::new();
let _ = self.report_with_syntax(error, config, source_code, &mut buffer);
String::from_utf8_lossy(&buffer).to_string()
}
fn report_plain<W, E>(
&self,
error: &E,
config: &ErrorReportConfig,
writer: &mut W,
) -> io::Result<()>
where
W: Write,
E: std::error::Error,
{
writeln!(writer, "Error: {}", error)?;
if config.include_source_chain {
let mut source = error.source();
let mut depth = 0;
while let Some(err) = source {
if let Some(max_depth) = config.max_chain_depth {
if depth >= max_depth {
writeln!(writer, "... (more causes hidden)")?;
break;
}
}
writeln!(writer, "Caused by: {}", err)?;
source = err.source();
depth += 1;
}
}
if config.include_backtrace {
}
Ok(())
}
fn report_json<W, E>(
&self,
error: &E,
config: &ErrorReportConfig,
writer: &mut W,
) -> io::Result<()>
where
W: Write,
E: std::error::Error,
{
let mut json = String::from("{");
json.push_str(&format!(
"\"error\": \"{}\"",
error.to_string().replace("\"", "\\\"")
));
if config.include_source_chain {
json.push_str(", \"causes\": [");
let mut source = error.source();
let mut is_first = true;
let mut depth = 0;
while let Some(err) = source {
if let Some(max_depth) = config.max_chain_depth {
if depth >= max_depth {
break;
}
}
if !is_first {
json.push_str(", ");
}
json.push_str(&format!("\"{}\"", err.to_string().replace("\"", "\\\"")));
source = err.source();
is_first = false;
depth += 1;
}
json.push_str("]");
}
json.push_str("}");
if config.pretty_print_json {
json = json
.replace("{", "{\n ")
.replace("}", "\n}")
.replace(", ", ",\n ");
}
writeln!(writer, "{}", json)?;
Ok(())
}
fn report_markdown<W, E>(
&self,
error: &E,
config: &ErrorReportConfig,
writer: &mut W,
) -> io::Result<()>
where
W: Write,
E: std::error::Error,
{
writeln!(writer, "## Error\n\n```")?;
writeln!(writer, "{}", error)?;
writeln!(writer, "```")?;
if config.include_source_chain {
writeln!(writer, "\n### Causes\n")?;
let mut source = error.source();
let mut depth = 0;
while let Some(err) = source {
if let Some(max_depth) = config.max_chain_depth {
if depth >= max_depth {
writeln!(writer, "... (more causes hidden)")?;
break;
}
}
writeln!(writer, "- {}", err)?;
source = err.source();
depth += 1;
}
}
Ok(())
}
fn report_html<W, E>(
&self,
error: &E,
config: &ErrorReportConfig,
writer: &mut W,
) -> io::Result<()>
where
W: Write,
E: std::error::Error,
{
writeln!(writer, "<div class=\"error\">")?;
writeln!(writer, " <h3>Error</h3>")?;
writeln!(
writer,
" <pre>{}</pre>",
error.to_string().replace("<", "<").replace(">", ">")
)?;
if config.include_source_chain {
writeln!(writer, " <h4>Causes</h4>")?;
writeln!(writer, " <ul>")?;
let mut source = error.source();
let mut depth = 0;
while let Some(err) = source {
if let Some(max_depth) = config.max_chain_depth {
if depth >= max_depth {
writeln!(writer, " <li>... (more causes hidden)</li>")?;
break;
}
}
writeln!(
writer,
" <li>{}</li>",
err.to_string().replace("<", "<").replace(">", ">")
)?;
source = err.source();
depth += 1;
}
writeln!(writer, " </ul>")?;
}
writeln!(writer, "</div>")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct TestError {
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for TestError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|s| s.as_ref() as &(dyn Error + 'static))
}
}
#[test]
fn test_error_reporter_plain_format() {
let error = TestError {
message: "Test error message".to_string(),
source: None,
};
let reporter = ErrorReporter::new();
let config = ErrorReportConfig {
include_message: true,
include_source_chain: true,
include_backtrace: false,
include_rich_context: false,
include_source_location: false,
include_severity: false,
format: ErrorReportFormat::Plain,
max_chain_depth: None,
pretty_print_json: false,
include_diagnostics: false,
};
let report = reporter.report_to_string(&error, &config);
assert!(report.contains("Test error message"));
}
#[test]
fn test_error_reporter_with_source() {
let source_error = TestError {
message: "Source error".to_string(),
source: None,
};
let error = TestError {
message: "Main error".to_string(),
source: Some(Box::new(source_error)),
};
let reporter = ErrorReporter::new();
let config = ErrorReportConfig {
include_message: true,
include_source_chain: true,
include_backtrace: false,
include_rich_context: false,
include_source_location: false,
include_severity: false,
format: ErrorReportFormat::Plain,
max_chain_depth: None,
pretty_print_json: false,
include_diagnostics: false,
};
let report = reporter.report_to_string(&error, &config);
assert!(report.contains("Main error"));
assert!(report.contains("Source error"));
}
#[test]
fn test_error_reporter_json_format() {
let error = TestError {
message: "JSON test error".to_string(),
source: None,
};
let reporter = ErrorReporter::new();
let config = ErrorReportConfig {
format: ErrorReportFormat::Json,
..Default::default()
};
let report = reporter.report_to_string(&error, &config);
assert!(report.starts_with("{"));
assert!(report.ends_with("}\n") || report.ends_with("}"));
assert!(report.contains("\"error\""));
assert!(report.contains("JSON test error"));
}
#[test]
fn test_error_reporter_with_syntax() {
let error = TestError {
message: "Syntax error in code".to_string(),
source: None,
};
let source_code = r#"
fn main() {
let x: i32 = "not an integer"; // Type mismatch error
println!("Value: {}", x);
}
"#;
let reporter = ErrorReporter::new();
let config = ErrorReportConfig {
format: ErrorReportFormat::Markdown,
..Default::default()
};
let report = reporter.report_to_string_with_syntax(&error, &config, Some(source_code));
assert!(report.contains("Syntax error in code"));
assert!(report.contains("Source Code Context"));
assert!(report.contains("```rust"));
assert!(report.contains("let x: i32 = \"not an integer\";"));
}
}