Skip to main content

charon_error/report/
error_report.rs

1use crate::{
2    ErrorAttachment, ErrorFmt, ErrorFmtLoD, ErrorFmtSettings, ErrorFormatObj, ErrorFrame,
3    ErrorSensitivityLabel, IndentationStyle, ResultER, ResultExt, map,
4};
5use crate::{LinkDebugIde, StringError};
6use colored::*;
7use std::error::Error;
8use std::fmt::{Debug, Display};
9use tracing::instrument;
10
11/// The core error type that collects a chain of [`ErrorFrame`]s.
12///
13/// An `ErrorReport` holds an ordered list of error frames, from the most specific
14/// (the original trigger) to the most general (the top-level context). Each frame
15/// captures the error, source location, timestamp, backtrace, tracing span, and
16/// optional attachments.
17///
18/// # Creating an ErrorReport
19///
20/// ```rust
21/// use charon_error::{ErrorReport, StringError};
22///
23/// let report = ErrorReport::from_error(StringError::new("disk full"));
24/// ```
25///
26/// # Chaining errors
27///
28/// ```rust
29/// use charon_error::{ErrorReport, StringError};
30///
31/// let report = ErrorReport::from_error(StringError::new("io error"))
32///     .push_error(StringError::new("failed to save file"));
33/// ```
34///
35/// # Using with `Result`
36///
37/// Use [`ResultER<T>`](crate::ResultER) and the [`ResultExt`](crate::ResultExt)
38/// trait for ergonomic error handling:
39///
40/// ```rust,no_run
41/// use charon_error::prelude::*;
42///
43/// fn read_file() -> ResultER<String> {
44///     std::fs::read_to_string("data.txt")
45///         .change_context(StringError::new("failed to read data file"))
46/// }
47/// ```
48#[derive(Debug)]
49#[must_use = "error reports must be used or returned"]
50pub struct ErrorReport {
51    /// All error frames listed from most specific error (trigger)
52    /// to most general error (effected).
53    pub frames: Vec<ErrorFrame>,
54    /// Unique error id to identify this error report.
55    unique_id: String,
56}
57
58impl ErrorFmt for ErrorReport {
59    #[instrument]
60    fn create_format_obj(&self, settings: ErrorFmtSettings) -> ResultER<ErrorFormatObj> {
61        let object = ErrorFormatObj::Object(map! {
62            "last_error" => ErrorFormatObj::ColorString(self.get_last_error_title().bright_red().bold()),
63            "unique_id" => ErrorFormatObj::String(self.unique_id.clone()),
64            "frames" => ErrorFormatObj::Array(self.frames.iter().map(|f| f.create_format_obj(settings)).collect::<ResultER<Vec<_>>>()?),
65        });
66        Ok(match settings.level_of_detail {
67            ErrorFmtLoD::Compact => object.filter_object_fields(vec!["last_error", "frames"])?,
68            ErrorFmtLoD::Medium | ErrorFmtLoD::SubmitReport => {
69                object.filter_object_fields(vec!["last_error", "frames", "unique_id"])?
70            }
71            ErrorFmtLoD::Full | ErrorFmtLoD::Debug => object,
72        })
73    }
74}
75
76impl ErrorReport {
77    /// Create an `ErrorReport` from any error type.
78    ///
79    /// This is the primary constructor. The error is wrapped in an [`ErrorFrame`]
80    /// that captures the source location, backtrace, and current tracing span.
81    #[track_caller]
82    #[must_use = "the newly created report must be used"]
83    pub fn from_error<E: Error + Send + Sync + 'static>(error: E) -> Self {
84        Self {
85            frames: vec![ErrorFrame::new(error)],
86            unique_id: Self::create_new_unique_id(),
87        }
88    }
89
90    /// Create an `ErrorReport` from a boxed dynamic error.
91    #[track_caller]
92    #[must_use = "the newly created report must be used"]
93    pub fn from_dyn_error(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
94        Self {
95            frames: vec![ErrorFrame::from_dyn_error(error)],
96            unique_id: Self::create_new_unique_id(),
97        }
98    }
99
100    /// Create an `ErrorReport` from an `anyhow::Error` reference.
101    ///
102    /// Walks the error chain and creates a frame for each cause.
103    #[track_caller]
104    #[must_use = "the newly created report must be used"]
105    pub fn from_anyhow_error_ref(error: &anyhow::Error) -> Self {
106        // Can not use closure here because of `#[track_caller]`
107        let errors: Vec<_> = error.chain().collect();
108        let mut frames: Vec<ErrorFrame> = vec![];
109        for cause in errors {
110            frames.push(ErrorFrame::new(StringError::from_error(cause)));
111        }
112        if frames.is_empty() {
113            std::panic::panic_any(StringError::new(
114                "Calling `ErrorReport::from_anyhow_error_ref()` on anyhow::Error with no causes. \
115                This should not be possible, please report.",
116            ));
117        }
118        Self {
119            frames,
120            unique_id: "".to_owned(),
121        }
122    }
123
124    /// Add a new error to the chain, providing higher-level context.
125    ///
126    /// Returns `self` for chaining.
127    #[track_caller]
128    #[must_use = "the modified report with the new error must be used"]
129    pub fn push_error<R: Error + Send + Sync + 'static>(mut self, new_error: R) -> Self {
130        self.frames.push(ErrorFrame::new(new_error));
131        self
132    }
133
134    /// Attach labeled data to the most recent error frame.
135    ///
136    /// Attachments are key-value pairs wrapped in an [`ErrorSensitivityLabel`]
137    /// to control visibility in reports.
138    ///
139    /// # Panics
140    ///
141    /// Panics if the report has no frames.
142    #[track_caller]
143    #[must_use = "the modified report with the attachment must be used"]
144    pub fn attach<S: Into<String>>(
145        mut self,
146        key_name: S,
147        attachment: ErrorSensitivityLabel<ErrorAttachment>,
148    ) -> Self {
149        if self.frames.is_empty() {
150            std::panic::panic_any(self.push_error(StringError::new(
151                "Calling `ErrorReport::attach()` on ErrorReport with no frames.",
152            )));
153        }
154        let last_frame = self.frames.last_mut().unwrap_error();
155        last_frame
156            .attachments
157            .insert(key_name.into(), Box::new(attachment));
158        self
159    }
160
161    /// Get a reference to the last (most general) error in the chain.
162    ///
163    /// # Panics
164    ///
165    /// Panics if the report has no frames.
166    // Size of `&(dyn Error)` is not known, so have to keep it in a Box.
167    #[allow(clippy::borrowed_box)]
168    #[must_use]
169    pub fn get_last_error(&self) -> &Box<dyn Error + Send + Sync + 'static> {
170        if self.frames.is_empty() {
171            std::panic::panic_any(StringError::new(
172                "Calling `ErrorReport::get_last_error()` on ErrorReport with no frames.",
173            )); // TODO Print `self` error too
174        }
175        &self.frames.last().unwrap_error().error
176    }
177
178    /// Get the display string of the last error in the chain.
179    #[must_use]
180    pub fn get_last_error_title(&self) -> String {
181        self.get_last_error().to_string()
182    }
183
184    /// Create a new Error Report ID hash.
185    fn create_new_unique_id() -> String {
186        let amount_chars: usize = 20;
187        use rand::RngExt;
188        let hash: String = rand::rng()
189            .sample_iter(rand::distr::Alphanumeric)
190            .take(amount_chars)
191            .map(char::from)
192            .collect();
193        hash
194    }
195
196    /// Get the unique identifier for this error report.
197    #[must_use]
198    pub fn get_unique_id(&self) -> String {
199        self.unique_id.clone()
200    }
201
202    /// Shorthand to attach a public string to the most recent frame.
203    #[must_use = "the modified report with the attachment must be used"]
204    pub fn attach_public_string<S: Into<String>, A: Display>(
205        self,
206        key_name: S,
207        attachment: A,
208    ) -> Self {
209        self.attach(
210            key_name,
211            ErrorSensitivityLabel::Public(ErrorAttachment::String(attachment.to_string())),
212        )
213    }
214}
215
216impl Display for ErrorReport {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        write!(
219            f,
220            "{}",
221            self.stringify(ErrorFmtSettings {
222                level_of_detail: ErrorFmtLoD::Medium,
223                frame_limit: None,
224                enable_color: true,
225                link_format: LinkDebugIde::Vscode, // TODO make global Setting
226                indentation_style: IndentationStyle::FourSpaces,
227                ..Default::default()
228            })
229            .unwrap_error()
230        )
231    }
232}
233
234impl<E: Error + Send + Sync + 'static> From<E> for ErrorReport {
235    #[track_caller]
236    fn from(error: E) -> Self {
237        Self::from_error(error)
238    }
239}