Skip to main content

charon_error/error_formating/
error_fmt.rs

1//! Error formatting trait, intermediate representation, and settings.
2//!
3//! The formatting pipeline converts any [`ErrorFmt`] implementor into an
4//! [`ErrorFormatObj`] tree (a JSON-like IR), then stringifies it with
5//! configurable detail level, indentation, and color via [`ErrorFmtSettings`].
6
7use crate::{ERGlobalSettings, LinkDebugIde, ResultER, SourceLocation};
8use colored::ColoredString;
9use indexmap::IndexMap;
10use tracing::instrument;
11
12/// Trait for converting error types into a structured [`ErrorFormatObj`] tree.
13///
14/// Implement this trait to control how your type is formatted at different
15/// detail levels. The pipeline is:
16/// `create_format_obj()` → [`ErrorFormatObj`] → `stringify()` → `String`
17pub trait ErrorFmt {
18    /// Convert this value into a structured format object.
19    fn create_format_obj(&self, settings: ErrorFmtSettings) -> ResultER<ErrorFormatObj>;
20
21    /// Convert to string by first creating a format object, then stringifying it.
22    fn stringify(&self, settings: ErrorFmtSettings) -> ResultER<String> {
23        let format_obj = self.create_format_obj(settings)?;
24
25        format_obj.stringify(settings)
26    }
27
28    /// Stringify an existing format object directly.
29    fn stringify_obj(format_obj: ErrorFormatObj, settings: ErrorFmtSettings) -> ResultER<String> {
30        format_obj.stringify(settings)
31    }
32}
33
34/// An intermediate representation for structured error output.
35///
36/// This is a JSON-like tree that the formatting system builds before
37/// converting to a final string. Supports nested objects, arrays,
38/// colored strings, source locations, and more.
39#[derive(Debug, Clone)]
40pub enum ErrorFormatObj {
41    /// Null/empty value.
42    Null,
43    /// Boolean value.
44    Bool(bool),
45    /// Numeric value.
46    Number(i64),
47    /// Plain string value.
48    String(String),
49    /// URL value.
50    Url(url::Url),
51    /// Date and time value (UTC).
52    DateTime(chrono::DateTime<chrono::Utc>),
53    /// Source code location (file:line:column).
54    SourceLocation(SourceLocation),
55    /// A sequence of format objects concatenated without separators.
56    FormatString(Vec<ErrorFormatObj>),
57    /// A string with ANSI color codes.
58    ColorString(ColoredString),
59    /// An ordered list of format objects.
60    Array(Vec<ErrorFormatObj>),
61    /// A key-value map of format objects (insertion-ordered).
62    Object(IndexMap<String, ErrorFormatObj>),
63}
64
65/// Error returned when filtering object fields with a key that does not exist.
66#[derive(thiserror::Error, Debug, Clone)]
67pub enum FilterObjectError {
68    /// The requested key was not found in the object.
69    #[error("The key `{0}` provided does not exist in the Object. It might be mistyped.")]
70    KeyDoesNotExist(String),
71}
72
73impl ErrorFormatObj {
74    /// Keep only the specified keys in an Object variant. Returns an error if
75    /// any key in `list` does not exist.
76    #[instrument]
77    pub fn filter_object_fields(&self, list: Vec<&str>) -> Result<Self, FilterObjectError> {
78        let mut new_self = self.clone();
79        if let ErrorFormatObj::Object(obj) = &mut new_self {
80            // Check if all items in list exist in obj
81            for item in &list {
82                if !obj.contains_key(*item) {
83                    return Err(FilterObjectError::KeyDoesNotExist(item.to_string()));
84                }
85            }
86            obj.retain(|key, _value| list.contains(&key.as_str()));
87        }
88        Ok(new_self)
89    }
90
91    /// Convert this format object to a formatted string.
92    pub fn stringify(&self, settings: ErrorFmtSettings) -> ResultER<String> {
93        Ok(match self {
94            Self::Null => "null".to_owned(),
95            Self::Bool(b) => {
96                if *b {
97                    "true".to_owned()
98                } else {
99                    "false".to_owned()
100                }
101            }
102            Self::Number(n) => format!("{n}"),
103            Self::String(s) => {
104                if settings.inside_format_string {
105                    s.to_string()
106                } else {
107                    format!("'{s}'")
108                }
109            }
110            Self::Url(url) => format!("'{url}'"),
111            Self::DateTime(date_time) => date_time.to_rfc3339(),
112            Self::SourceLocation(sl) => sl.display_location(Some(settings.link_format)),
113            Self::FormatString(fs) => fs
114                .iter()
115                .map(|s| s.stringify(settings.inside_fs()))
116                .collect::<ResultER<Vec<_>>>()?
117                .join(""),
118            Self::ColorString(cs) => {
119                let text = if settings.enable_color {
120                    cs.to_string()
121                } else {
122                    cs.input.to_string()
123                };
124                if settings.inside_format_string {
125                    text.to_string()
126                } else {
127                    format!("'{text}'")
128                }
129            }
130            Self::Array(list) => match settings.level_of_detail {
131                ErrorFmtLoD::Compact => format!(
132                    "[{}]",
133                    list.iter()
134                        .map(|s| s.stringify(settings.reset_inside_fs()))
135                        .collect::<ResultER<Vec<_>>>()?
136                        .join(",")
137                ),
138                ErrorFmtLoD::SubmitReport
139                | ErrorFmtLoD::Medium
140                | ErrorFmtLoD::Full
141                | ErrorFmtLoD::Debug => {
142                    if list.is_empty() {
143                        "[]".to_owned()
144                    } else {
145                        let indented_s = settings.add_indent().reset_inside_fs();
146                        format!(
147                            "[\n{items}{tab}]",
148                            tab = settings.indent(),
149                            items = list
150                                .iter()
151                                .map(|s| Ok(format!(
152                                    "{}{},\n",
153                                    indented_s.indent(),
154                                    s.stringify(indented_s)?
155                                )))
156                                .collect::<ResultER<Vec<_>>>()?
157                                .join("")
158                        )
159                    }
160                }
161            },
162            Self::Object(obj) => match settings.level_of_detail {
163                ErrorFmtLoD::Compact => format!(
164                    "{{{}}}",
165                    obj.iter()
166                        .map(|(k, v)| Ok(format!(
167                            "{} -> {}",
168                            k,
169                            v.stringify(settings.reset_inside_fs())?
170                        )))
171                        .collect::<ResultER<Vec<_>>>()?
172                        .join(","),
173                ),
174                ErrorFmtLoD::SubmitReport
175                | ErrorFmtLoD::Medium
176                | ErrorFmtLoD::Full
177                | ErrorFmtLoD::Debug => {
178                    if obj.is_empty() {
179                        "{}".to_owned()
180                    } else {
181                        let indented_s = settings.add_indent().reset_inside_fs();
182                        format!(
183                            "{{\n{items}{tab}}}",
184                            tab = settings.indent(),
185                            items = obj
186                                .iter()
187                                .map(|(k, v)| Ok(format!(
188                                    "{}{}: {},\n",
189                                    indented_s.indent(),
190                                    k,
191                                    v.stringify(indented_s)?
192                                )))
193                                .collect::<ResultER<Vec<_>>>()?
194                                .join(""),
195                        )
196                    }
197                }
198            },
199        })
200    }
201}
202
203/// Configuration for error formatting output.
204///
205/// Controls the level of detail, coloring, indentation style, and link format.
206///
207/// # Example
208///
209/// ```rust
210/// use charon_error::{ErrorFmtSettings, ErrorFmtLoD, IndentationStyle, LinkDebugIde};
211///
212/// let settings = ErrorFmtSettings {
213///     level_of_detail: ErrorFmtLoD::Compact,
214///     enable_color: false,
215///     link_format: LinkDebugIde::NoLink,
216///     indentation_style: IndentationStyle::TwoSpaces,
217///     ..Default::default()
218/// };
219/// ```
220#[derive(Debug, Clone, Copy)]
221pub struct ErrorFmtSettings {
222    /// How much detail to include in the output.
223    pub level_of_detail: ErrorFmtLoD,
224    /// Maximum number of error frames to include (None = unlimited).
225    pub frame_limit: Option<usize>,
226    /// Whether to include ANSI color codes in output.
227    pub enable_color: bool,
228    /// How source locations are formatted (plain text, VSCode link, etc).
229    pub link_format: LinkDebugIde,
230    /// Indentation style (tabs, 2 spaces, or 4 spaces).
231    pub indentation_style: IndentationStyle,
232    /// Current indentation depth (internal use).
233    pub indentation: usize,
234    /// Whether we are currently inside a format string (internal use).
235    pub inside_format_string: bool,
236    /// Number of initial indentations to skip (internal use).
237    pub skip_first_indentations: usize,
238}
239
240impl ErrorFmtSettings {
241    fn add_indent(self) -> Self {
242        let mut copy_self = self;
243        if copy_self.skip_first_indentations > 0 {
244            copy_self.skip_first_indentations -= 1;
245        } else {
246            copy_self.indentation += 1;
247        }
248        copy_self
249    }
250
251    fn indent(self) -> String {
252        match self.indentation_style {
253            IndentationStyle::Tab => (0..self.indentation).map(|_| "\t").collect::<String>(),
254            IndentationStyle::TwoSpaces => (0..self.indentation).map(|_| "  ").collect::<String>(),
255            IndentationStyle::FourSpaces => {
256                (0..self.indentation).map(|_| "    ").collect::<String>()
257            }
258        }
259    }
260
261    fn inside_fs(self) -> Self {
262        let mut copy_self = self;
263        copy_self.inside_format_string = true;
264        copy_self
265    }
266
267    fn reset_inside_fs(self) -> Self {
268        let mut copy_self = self;
269        copy_self.inside_format_string = false;
270        copy_self
271    }
272}
273
274impl Default for ErrorFmtSettings {
275    fn default() -> Self {
276        Self {
277            level_of_detail: ErrorFmtLoD::Medium,
278            frame_limit: None,
279            enable_color: true,
280            link_format: LinkDebugIde::File,
281            indentation: 0,
282            indentation_style: IndentationStyle::TwoSpaces,
283            inside_format_string: false,
284            skip_first_indentations: 0,
285        }
286    }
287}
288
289impl From<ERGlobalSettings> for ErrorFmtSettings {
290    fn from(settings: ERGlobalSettings) -> Self {
291        ErrorFmtSettings {
292            link_format: settings.link_format,
293            ..Default::default()
294        }
295    }
296}
297
298impl From<&ERGlobalSettings> for ErrorFmtSettings {
299    fn from(settings: &ERGlobalSettings) -> Self {
300        ErrorFmtSettings {
301            link_format: settings.link_format,
302            ..Default::default()
303        }
304    }
305}
306
307/// Indentation style for formatted output.
308#[derive(Debug, Clone, Copy)]
309pub enum IndentationStyle {
310    /// Tab character (`\t`).
311    Tab,
312    /// Two spaces per level.
313    TwoSpaces,
314    /// Four spaces per level.
315    FourSpaces,
316}
317
318/// Level of detail for error formatting output.
319#[derive(Debug, Clone, Copy, Default)]
320pub enum ErrorFmtLoD {
321    /// Minimal output — key fields only.
322    Compact,
323    /// Tailored for issue submission reports (no color, clean text).
324    SubmitReport,
325    /// Balanced detail — the default for most uses.
326    #[default]
327    Medium,
328    /// Full output — all available information.
329    Full,
330    /// Structural debug output for debugging the error system itself.
331    Debug,
332}
333
334/// Create an [`IndexMap`](indexmap::IndexMap) with string keys from key-value pairs.
335///
336/// Used internally by the formatting system to build structured objects.
337#[macro_export]
338macro_rules! map {
339    ($($key:expr => $val:expr),* $(,)*) => ({
340        #[allow(unused_mut)]
341        let mut map = indexmap::IndexMap::new();
342        $( map.insert($key.to_string(), $val); )*
343        map
344    });
345}
346
347/// Create an [`ErrorFormatObj::FormatString`] from multiple values.
348///
349/// Each value is converted to a string and wrapped in `ErrorFormatObj::String`,
350/// then combined into a `FormatString` that concatenates without separators.
351#[macro_export]
352macro_rules! format_string {
353    ($($val:expr),* $(,)*) => ({
354        #[allow(unused_mut)]
355        let mut list = Vec::new();
356        $( list.push(ErrorFormatObj::String($val.to_string())); )*
357        ErrorFormatObj::FormatString(list)
358    });
359}