Skip to main content

charon_error/submit/
simple_error_report.rs

1//! Simple error report output without URL generation.
2//!
3//! [`SimpleErrorReport`] produces a markdown-formatted report suitable
4//! for terminal display or piping to a file. No external service is needed.
5
6use crate::{
7    ERGlobalSettings, ErrorFmt, ErrorFmtLoD, ErrorFmtSettings, ErrorReport, GlobalSettings,
8    IndentationStyle, LinkDebugIde, StringError, SubmitErrorReport,
9};
10use colored::Colorize;
11use std::sync::{OnceLock, RwLock};
12use url::Url;
13
14/// Simple implementation of [`SubmitErrorReport`].
15///
16/// Generates a structured, markdown-formatted error report suitable for both
17/// humans and AI/LLMs. Does not generate URLs — the report is the output.
18///
19/// Respects the `NO_COLOR` environment variable: when set, terminal hyperlinks
20/// are disabled, producing clean plain text (markdown is unaffected).
21///
22/// # Example
23///
24/// ```rust,no_run
25/// use charon_error::prelude::*;
26/// use charon_error::prelude::simple_er::*;
27///
28/// SimpleERGlobalSettings::set_global_settings(SimpleERGlobalSettings {}).unwrap_error();
29/// ```
30///
31/// Example Output:
32/// ```text
33/// PANIC: A panic occurred during execution.
34/// * Message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`'
35/// * Path: 'src/main.rs:72:21'
36/// PANIC: The application encountered an error it could not recover from.
37/// {
38///   last_error: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`',
39///   unique_id: '4qYnZ5u3pWAHdVZJEQB0',
40///   frames: [
41///     {
42///       message: 'No such file or directory (os error 2)',
43///       source_location: src/cli/config.rs:15:10,
44///       span_trace: [
45///         charon_demo::cli::config::read_config at (src/cli/config.rs:7:0),
46///         charon_demo::cli::start_server at (src/cli.rs:82:0),
47///         charon_demo::cli::start at (src/cli.rs:35:0),
48///       ],
49///       attachments: {},
50///       date_time: 2026-02-28T23:56:31.182899732+00:00,
51///     },
52///     {
53///       message: 'The config file failed to load correctly.',
54///       source_location: src/cli/config.rs:15:10,
55///       span_trace: [
56///         charon_demo::cli::config::read_config at (src/cli/config.rs:7:0),
57///         charon_demo::cli::start_server at (src/cli.rs:82:0),
58///         charon_demo::cli::start at (src/cli.rs:35:0),
59///       ],
60///       attachments: {
61///         file_path: '../config.toml',
62///       },
63///       date_time: 2026-02-28T23:56:31.183006561+00:00,
64///     },
65///     {
66///       message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`',
67///       source_location: src/main.rs:72:21,
68///       span_trace: [],
69///       attachments: {},
70///       date_time: 2026-02-28T23:56:31.183038050+00:00,
71///     },
72///   ],
73/// }
74/// No report link available.
75/// ```
76#[derive(Debug, Clone)]
77pub struct SimpleErrorReport<'a> {
78    /// Reference to the error report being formatted.
79    pub error_report: &'a ErrorReport,
80}
81
82impl<'a> SubmitErrorReport<'a> for SimpleErrorReport<'a> {
83    fn new(error: &'a ErrorReport) -> Self {
84        SimpleErrorReport {
85            error_report: error,
86        }
87    }
88
89    fn get_error_report(&self) -> &ErrorReport {
90        self.error_report
91    }
92
93    fn get_title(&self) -> String {
94        self.error_report.get_last_error_title()
95    }
96
97    fn create_message(&self) -> Result<String, ErrorReport> {
98        self.create_bug_report()
99    }
100
101    fn create_bug_report(&self) -> Result<String, ErrorReport> {
102        let link_format = ERGlobalSettings::get_or_default_settings()
103            .map(|c| c.link_format)
104            .unwrap_or(LinkDebugIde::File);
105
106        let last_error = self.get_title();
107
108        let mut report = String::new();
109
110        // Title
111        report.push_str(&format!("{}\n\n", "# Error Report".bright_cyan().bold()));
112
113        // Error
114        report.push_str(&format!("{}\n\n", "## Error".bright_cyan().bold()));
115        report.push_str(&format!("{}\n\n", last_error.bright_red().bold()));
116
117        // Error report (structured output from ErrorFmt)
118        let summary = self.error_report.stringify(ErrorFmtSettings {
119            level_of_detail: ErrorFmtLoD::SubmitReport,
120            frame_limit: None,
121            enable_color: true,
122            link_format,
123            indentation_style: IndentationStyle::TwoSpaces,
124            ..Default::default()
125        })?;
126        report.push_str(&format!("{}\n\n", "## Error report".bright_cyan().bold()));
127        report.push_str("```\n");
128        report.push_str(&summary);
129        report.push_str("\n```\n\n");
130
131        // Backtrace
132        if let Some(last_frame) = self.error_report.frames.last() {
133            let backtrace_string = last_frame.create_backtrace_string();
134            if !backtrace_string.trim().is_empty() {
135                report.push_str(&format!("{}\n\n", "## Backtrace".bright_cyan().bold()));
136                report.push_str("```\n");
137                report.push_str(&backtrace_string);
138                report.push_str("```\n\n");
139            }
140        }
141
142        // System info
143        report.push_str(&format!("{}\n\n", "## System".bright_cyan().bold()));
144        report.push_str(&format!(
145            "* **OS:** {} {}\n",
146            std::env::consts::OS,
147            std::env::consts::ARCH
148        ));
149        report.push_str(&format!("* **Version:** {}\n", env!("CARGO_PKG_VERSION")));
150
151        Ok(report)
152    }
153
154    fn create_submit_url(&self) -> Result<Url, ErrorReport> {
155        Ok(Url::parse("data:text/plain,N/A")?)
156    }
157
158    fn create_submit_url_limited(&self, _max_length: usize) -> Result<Url, ErrorReport> {
159        Ok(Url::parse("data:text/plain,N/A")?)
160    }
161
162    fn check_existing_reports(&self) -> Result<Url, ErrorReport> {
163        Ok(Url::parse("data:text/plain,N/A")?)
164    }
165
166    fn validate_settings(&self) -> Result<(), ErrorReport> {
167        if !SimpleERGlobalSettings::is_set() {
168            Err(StringError::new(
169                "SimpleERGlobalSettings has not been set, this could create runtime issues.",
170            )
171            .into())
172        } else {
173            Ok(())
174        }
175    }
176}
177
178/// Global configuration for the simple error report handler.
179///
180/// Set once at application startup via
181/// [`GlobalSettings::set_global_settings`].
182#[derive(Debug, Clone, Default)]
183pub struct SimpleERGlobalSettings {}
184
185static SIMPLE_REPORT_GLOBAL_CONFIG: OnceLock<RwLock<SimpleERGlobalSettings>> = OnceLock::new();
186
187impl GlobalSettings for SimpleERGlobalSettings {
188    type Setting = SimpleERGlobalSettings;
189
190    fn once_lock() -> &'static OnceLock<RwLock<Self::Setting>> {
191        &SIMPLE_REPORT_GLOBAL_CONFIG
192    }
193    fn get_setting_object_name() -> &'static str {
194        "SIMPLE_REPORT_GLOBAL_CONFIG"
195    }
196}