Skip to main content

rajac_base/
error.rs

1use std::error::Error as StdError;
2use std::fmt::{Debug, Display, Formatter};
3use std::panic::Location;
4use tracing_error::{SpanTrace, SpanTraceStatus};
5
6use crate::shared_string::SharedString;
7use crate::unansi;
8
9#[derive(Debug)]
10pub enum ErrorKind {
11    Message(SharedString),
12    Std(Box<dyn StdError + Send + Sync + 'static>),
13}
14
15impl Display for ErrorKind {
16    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Self::Message(message) => f.write_str(message),
19            Self::Std(error) => Display::fmt(error, f),
20        }
21    }
22}
23
24#[derive(Debug)]
25pub struct RajacError {
26    kind: ErrorKind,
27    source: Option<Box<RajacError>>,
28    location: &'static Location<'static>,
29    span_trace: SpanTrace,
30}
31
32impl RajacError {
33    #[track_caller]
34    pub fn new(kind: ErrorKind) -> Self {
35        Self::at_location(kind, Location::caller())
36    }
37
38    pub fn at_location(kind: ErrorKind, location: &'static Location<'static>) -> Self {
39        Self {
40            kind,
41            source: None,
42            location,
43            span_trace: SpanTrace::capture(),
44        }
45    }
46
47    #[track_caller]
48    pub fn message(s: impl Into<SharedString>) -> Self {
49        Self::message_at_location(s, Location::caller())
50    }
51
52    pub fn message_at_location(
53        s: impl Into<SharedString>,
54        location: &'static Location<'static>,
55    ) -> Self {
56        Self::at_location(ErrorKind::Message(s.into()), location)
57    }
58
59    #[track_caller]
60    pub fn std(error: impl StdError + Send + Sync + 'static) -> Self {
61        Self::std_at_location(error, Location::caller())
62    }
63
64    pub fn std_at_location(
65        error: impl StdError + Send + Sync + 'static,
66        location: &'static Location<'static>,
67    ) -> Self {
68        Self::at_location(ErrorKind::Std(Box::new(error)), location)
69    }
70
71    pub fn kind(&self) -> &ErrorKind {
72        &self.kind
73    }
74
75    pub fn source(&self) -> Option<&RajacError> {
76        self.source.as_deref()
77    }
78
79    pub fn location(&self) -> &'static Location<'static> {
80        self.location
81    }
82
83    pub fn span_trace(&self) -> &SpanTrace {
84        &self.span_trace
85    }
86
87    pub fn with_source(mut self, source: impl Into<RajacError>) -> Self {
88        self.source = Some(Box::new(source.into()));
89        self
90    }
91
92    #[track_caller]
93    pub fn with_std_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
94        self.source = Some(Box::new(RajacError::std_at_location(
95            source,
96            Location::caller(),
97        )));
98        self
99    }
100
101    pub fn with_std_source_at_location(
102        mut self,
103        source: impl StdError + Send + Sync + 'static,
104        location: &'static Location<'static>,
105    ) -> Self {
106        self.source = Some(Box::new(RajacError::std_at_location(source, location)));
107        self
108    }
109
110    pub fn write_to(&self, write: &mut dyn std::fmt::Write) -> std::fmt::Result {
111        writeln!(write, "{} {}", style("1;31", "× error"), self.kind)?;
112        self.write_details(write, "")?;
113        Ok(())
114    }
115
116    pub fn to_test_string(&self) -> String {
117        let mut test_string = String::new();
118        self.write_to(&mut test_string).unwrap();
119        unansi(&test_string)
120    }
121}
122
123impl RajacError {
124    fn write_details(&self, write: &mut dyn std::fmt::Write, prefix: &str) -> std::fmt::Result {
125        let show_span_trace = self.source.is_none();
126
127        writeln!(
128            write,
129            "{}{} {}:{}:{}",
130            prefix,
131            style("2;37", "  at"),
132            self.location.file(),
133            self.location.line(),
134            self.location.column()
135        )?;
136
137        if show_span_trace && self.span_trace.status() == SpanTraceStatus::CAPTURED {
138            writeln!(write, "{}{}", prefix, style("36", "  span trace:"))?;
139            write_span_trace(write, prefix, &self.span_trace)?;
140        }
141
142        if let Some(source) = self.source.as_deref() {
143            writeln!(
144                write,
145                "{}{} {}",
146                prefix,
147                style("33", "caused by:"),
148                source.kind
149            )?;
150            source.write_child_details(write, &format!("{prefix}   "))?;
151        }
152
153        Ok(())
154    }
155
156    fn write_child_details(
157        &self,
158        write: &mut dyn std::fmt::Write,
159        prefix: &str,
160    ) -> std::fmt::Result {
161        let show_span_trace = self.source.is_none();
162
163        writeln!(
164            write,
165            "{}{} {}:{}:{}",
166            prefix,
167            style("2;37", "  at"),
168            self.location.file(),
169            self.location.line(),
170            self.location.column()
171        )?;
172
173        if show_span_trace && self.span_trace.status() == SpanTraceStatus::CAPTURED {
174            writeln!(write, "{}{}", prefix, style("36", "  span trace:"))?;
175            write_span_trace(write, prefix, &self.span_trace)?;
176        }
177
178        if let Some(source) = self.source.as_deref() {
179            writeln!(
180                write,
181                "{}{} {}",
182                prefix,
183                style("33", "caused by:"),
184                source.kind
185            )?;
186            source.write_child_details(write, &format!("{prefix}   "))?;
187        }
188
189        Ok(())
190    }
191}
192
193fn write_span_trace(
194    write: &mut dyn std::fmt::Write,
195    prefix: &str,
196    span_trace: &SpanTrace,
197) -> std::fmt::Result {
198    let mut result = Ok(());
199    let mut span_index = 0;
200
201    span_trace.with_spans(|metadata, fields| {
202        if span_index > 0 && writeln!(write).is_err() {
203            result = Err(std::fmt::Error);
204            return false;
205        }
206
207        if writeln!(
208            write,
209            "{}    {}: {}::{}",
210            prefix,
211            span_index,
212            metadata.target(),
213            metadata.name()
214        )
215        .is_err()
216        {
217            result = Err(std::fmt::Error);
218            return false;
219        }
220
221        if !fields.is_empty()
222            && writeln!(
223                write,
224                "{}       {}",
225                prefix,
226                format_span_trace_fields(fields)
227            )
228            .is_err()
229        {
230            result = Err(std::fmt::Error);
231            return false;
232        }
233
234        if let Some((file, line)) = metadata
235            .file()
236            .and_then(|file| metadata.line().map(|line| (file, line)))
237            && writeln!(write, "{}       at {}:{}", prefix, file, line).is_err()
238        {
239            result = Err(std::fmt::Error);
240            return false;
241        }
242
243        span_index += 1;
244        true
245    });
246
247    result
248}
249
250fn format_span_trace_fields(fields: &str) -> String {
251    let mut formatted = String::new();
252
253    for (index, field) in fields.split_whitespace().enumerate() {
254        if index > 0 {
255            formatted.push(' ');
256        }
257
258        if let Some((key, value)) = field.split_once('=') {
259            formatted.push_str(key);
260            formatted.push(':');
261            formatted.push(' ');
262            formatted.push_str(&style("1;97", value));
263        } else {
264            formatted.push_str(field);
265        }
266    }
267
268    formatted
269}
270
271fn style(code: &str, text: &str) -> String {
272    format!("\u{1b}[{code}m{text}\u{1b}[0m")
273}
274
275impl<T> From<T> for RajacError
276where
277    T: StdError + Send + Sync + 'static,
278{
279    #[track_caller]
280    fn from(value: T) -> Self {
281        Self::std(value)
282    }
283}
284
285#[macro_export]
286macro_rules! err {
287    ($($arg:tt)*) => {
288        $crate::error::RajacError::message(format!($($arg)*))
289    };
290}
291pub use err;
292
293#[macro_export]
294macro_rules! bail {
295    ($($arg:tt)*) => {
296        return Err($crate::err!($($arg)*))
297    };
298}
299pub use bail;
300
301#[cfg(test)]
302mod tests {
303    use super::format_span_trace_fields;
304
305    #[test]
306    fn test_format_span_trace_fields() {
307        let rendered = format_span_trace_fields(
308            "sources_dir=verification/sources output_dir=verification/output/rajac",
309        );
310        let rendered = crate::unansi(&rendered);
311
312        assert_eq!(
313            rendered,
314            "sources_dir: verification/sources output_dir: verification/output/rajac"
315        );
316    }
317}