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}