Skip to main content

charon_error/error_formating/
error_fmt.rs

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