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}