1use crate::WireValue;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct AnyErrorFrame {
11 pub function: Option<String>,
12 pub file: Option<String>,
13 pub line: Option<usize>,
14 pub column: Option<usize>,
15 pub ip: Option<usize>,
16}
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct AnyError {
21 pub category: String,
22 pub message: String,
23 pub code: Option<String>,
24 pub payload: WireValue,
25 pub frames: Vec<AnyErrorFrame>,
26 pub cause: Option<Box<AnyError>>,
27}
28
29impl AnyError {
30 pub fn from_wire(value: &WireValue) -> Option<Self> {
32 let obj = value.as_object()?;
33 let category = obj.get("category").and_then(WireValue::as_str)?.to_string();
34 if category != "AnyError" {
35 return None;
36 }
37
38 let payload = obj.get("payload").cloned().unwrap_or(WireValue::Null);
39 let message = obj
40 .get("message")
41 .and_then(WireValue::as_str)
42 .map(ToString::to_string)
43 .unwrap_or_else(|| brief_value(&payload));
44 let code = obj
45 .get("code")
46 .and_then(WireValue::as_str)
47 .map(str::to_string);
48
49 let frames = obj
50 .get("trace_info")
51 .map(parse_trace_info)
52 .unwrap_or_default();
53
54 let cause = obj
55 .get("cause")
56 .filter(|v| !v.is_null())
57 .and_then(Self::from_wire)
58 .map(Box::new);
59
60 Some(Self {
61 category,
62 message,
63 code,
64 payload,
65 frames,
66 cause,
67 })
68 }
69
70 pub fn primary_location(&self) -> Option<AnyErrorFrame> {
72 self.frames.first().cloned().or_else(|| {
73 self.cause
74 .as_deref()
75 .and_then(|cause| cause.primary_location())
76 })
77 }
78}
79
80pub trait AnyErrorRenderer {
82 fn render(&self, error: &AnyError) -> String;
83}
84
85#[derive(Debug, Default, Clone, Copy)]
87pub struct PlainAnyErrorRenderer;
88
89impl AnyErrorRenderer for PlainAnyErrorRenderer {
90 fn render(&self, error: &AnyError) -> String {
91 let mut out = String::from("Uncaught exception:");
92 render_plain_node(error, true, &mut out);
93 out
94 }
95}
96
97#[derive(Debug, Default, Clone, Copy)]
99pub struct AnsiAnyErrorRenderer;
100
101impl AnyErrorRenderer for AnsiAnyErrorRenderer {
102 fn render(&self, error: &AnyError) -> String {
103 let mut out = String::new();
104 out.push_str("\x1b[1;31mUncaught exception:\x1b[0m");
105 render_ansi_node(error, true, &mut out);
106 out
107 }
108}
109
110#[derive(Debug, Default, Clone, Copy)]
112pub struct HtmlAnyErrorRenderer;
113
114impl AnyErrorRenderer for HtmlAnyErrorRenderer {
115 fn render(&self, error: &AnyError) -> String {
116 let mut out = String::new();
117 out.push_str("<div class=\"shape-error\">");
118 out.push_str("<div class=\"shape-error-header\">Uncaught exception:</div>");
119 render_html_node(error, true, &mut out);
120 out.push_str("</div>");
121 out
122 }
123}
124
125pub fn render_any_error_with<R: AnyErrorRenderer>(
127 value: &WireValue,
128 renderer: &R,
129) -> Option<String> {
130 AnyError::from_wire(value).map(|err| renderer.render(&err))
131}
132
133pub fn render_any_error_plain(value: &WireValue) -> Option<String> {
135 render_any_error_with(value, &PlainAnyErrorRenderer)
136}
137
138pub fn render_any_error_ansi(value: &WireValue) -> Option<String> {
140 render_any_error_with(value, &AnsiAnyErrorRenderer)
141}
142
143pub fn render_any_error_html(value: &WireValue) -> Option<String> {
145 render_any_error_with(value, &HtmlAnyErrorRenderer)
146}
147
148pub fn render_any_error_terminal(value: &WireValue) -> Option<String> {
154 if terminal_supports_ansi() {
155 render_any_error_ansi(value)
156 } else {
157 render_any_error_plain(value)
158 }
159}
160
161fn terminal_supports_ansi() -> bool {
162 if std::env::var_os("NO_COLOR").is_some() {
163 return false;
164 }
165
166 if std::env::var("CLICOLOR").ok().as_deref() == Some("0") {
167 return false;
168 }
169
170 if std::env::var_os("FORCE_COLOR").is_some()
171 || std::env::var("CLICOLOR_FORCE")
172 .map(|v| v != "0")
173 .unwrap_or(false)
174 {
175 return true;
176 }
177
178 match std::env::var("TERM") {
179 Ok(term) if !term.is_empty() && term != "dumb" => true,
180 _ => false,
181 }
182}
183
184fn render_plain_node(error: &AnyError, root: bool, out: &mut String) {
185 if root {
186 if let Some(code) = &error.code {
187 out.push_str(&format!("\nError [{}]: {}", code, error.message));
188 } else {
189 out.push_str(&format!("\nError: {}", error.message));
190 }
191 } else if let Some(code) = &error.code {
192 out.push_str(&format!("\nCaused by [{}]: {}", code, error.message));
193 } else {
194 out.push_str(&format!("\nCaused by: {}", error.message));
195 }
196
197 for frame in &error.frames {
198 out.push_str("\n at ");
199 out.push_str(frame.function.as_deref().unwrap_or("<anonymous>"));
200 if frame.file.is_some() || frame.line.is_some() || frame.column.is_some() {
201 out.push_str(" (");
202 match (&frame.file, frame.line, frame.column) {
203 (Some(file), Some(line), Some(column)) => {
204 out.push_str(&format!("{file}:{line}:{column}"))
205 }
206 (Some(file), Some(line), None) => out.push_str(&format!("{file}:{line}")),
207 (Some(file), None, _) => out.push_str(file),
208 (None, Some(line), Some(column)) => out.push_str(&format!("line {line}:{column}")),
209 (None, Some(line), None) => out.push_str(&format!("line {line}")),
210 (None, None, Some(column)) => out.push_str(&format!("column {column}")),
211 (None, None, None) => {}
212 }
213 out.push(')');
214 }
215 if let Some(ip) = frame.ip {
216 out.push_str(&format!(" [ip {}]", ip));
217 }
218 }
219
220 if let Some(cause) = &error.cause {
221 render_plain_node(cause, false, out);
222 }
223}
224
225fn render_ansi_node(error: &AnyError, root: bool, out: &mut String) {
226 if root {
227 if let Some(code) = &error.code {
228 out.push_str(&format!(
229 "\n\x1b[1;31mError [{}]\x1b[0m: {}",
230 code, error.message
231 ));
232 } else {
233 out.push_str(&format!("\n\x1b[1;31mError\x1b[0m: {}", error.message));
234 }
235 } else if let Some(code) = &error.code {
236 out.push_str(&format!(
237 "\n\x1b[33mCaused by [{}]\x1b[0m: {}",
238 code, error.message
239 ));
240 } else {
241 out.push_str(&format!("\n\x1b[33mCaused by\x1b[0m: {}", error.message));
242 }
243
244 for frame in &error.frames {
245 out.push_str("\n \x1b[36mat\x1b[0m ");
246 out.push_str(frame.function.as_deref().unwrap_or("<anonymous>"));
247 if frame.file.is_some() || frame.line.is_some() || frame.column.is_some() {
248 out.push_str(" (\x1b[2m");
249 match (&frame.file, frame.line, frame.column) {
250 (Some(file), Some(line), Some(column)) => {
251 out.push_str(&format!("{file}:{line}:{column}"))
252 }
253 (Some(file), Some(line), None) => out.push_str(&format!("{file}:{line}")),
254 (Some(file), None, _) => out.push_str(file),
255 (None, Some(line), Some(column)) => out.push_str(&format!("line {line}:{column}")),
256 (None, Some(line), None) => out.push_str(&format!("line {line}")),
257 (None, None, Some(column)) => out.push_str(&format!("column {column}")),
258 (None, None, None) => {}
259 }
260 out.push_str("\x1b[0m)");
261 }
262 if let Some(ip) = frame.ip {
263 out.push_str(&format!(" [ip {}]", ip));
264 }
265 }
266
267 if let Some(cause) = &error.cause {
268 render_ansi_node(cause, false, out);
269 }
270}
271
272fn render_html_node(error: &AnyError, root: bool, out: &mut String) {
273 if root {
274 out.push_str("<div class=\"shape-error-main\">");
275 if let Some(code) = &error.code {
276 out.push_str(&format!(
277 "<span class=\"shape-error-label\">Error [{}]</span>: <span class=\"shape-error-message\">{}</span>",
278 escape_html(code),
279 escape_html(&error.message),
280 ));
281 } else {
282 out.push_str(&format!(
283 "<span class=\"shape-error-label\">Error</span>: <span class=\"shape-error-message\">{}</span>",
284 escape_html(&error.message),
285 ));
286 }
287 out.push_str("</div>");
288 } else {
289 out.push_str("<div class=\"shape-error-cause\">");
290 if let Some(code) = &error.code {
291 out.push_str(&format!(
292 "<span class=\"shape-error-cause-label\">Caused by [{}]</span>: <span class=\"shape-error-message\">{}</span>",
293 escape_html(code),
294 escape_html(&error.message),
295 ));
296 } else {
297 out.push_str(&format!(
298 "<span class=\"shape-error-cause-label\">Caused by</span>: <span class=\"shape-error-message\">{}</span>",
299 escape_html(&error.message),
300 ));
301 }
302 out.push_str("</div>");
303 }
304
305 for frame in &error.frames {
306 out.push_str("<div class=\"shape-error-frame\">");
307 out.push_str("<span class=\"shape-error-at\">at</span> ");
308 out.push_str(&escape_html(
309 frame.function.as_deref().unwrap_or("<anonymous>"),
310 ));
311 if frame.file.is_some() || frame.line.is_some() || frame.column.is_some() {
312 out.push_str(" <span class=\"shape-error-loc\">(");
313 match (&frame.file, frame.line, frame.column) {
314 (Some(file), Some(line), Some(column)) => {
315 out.push_str(&escape_html(&format!("{file}:{line}:{column}")))
316 }
317 (Some(file), Some(line), None) => {
318 out.push_str(&escape_html(&format!("{file}:{line}")))
319 }
320 (Some(file), None, _) => out.push_str(&escape_html(file)),
321 (None, Some(line), Some(column)) => {
322 out.push_str(&escape_html(&format!("line {line}:{column}")))
323 }
324 (None, Some(line), None) => out.push_str(&escape_html(&format!("line {line}"))),
325 (None, None, Some(column)) => {
326 out.push_str(&escape_html(&format!("column {column}")))
327 }
328 (None, None, None) => {}
329 }
330 out.push_str(")</span>");
331 }
332 if let Some(ip) = frame.ip {
333 out.push_str(&format!(
334 " <span class=\"shape-error-ip\">[ip {}]</span>",
335 ip
336 ));
337 }
338 out.push_str("</div>");
339 }
340
341 if let Some(cause) = &error.cause {
342 render_html_node(cause, false, out);
343 }
344}
345
346fn parse_trace_info(value: &WireValue) -> Vec<AnyErrorFrame> {
347 let Some(obj) = value.as_object() else {
348 return Vec::new();
349 };
350 let kind = obj
351 .get("kind")
352 .and_then(WireValue::as_str)
353 .unwrap_or("full");
354 if kind == "single" {
355 obj.get("frame")
356 .and_then(parse_trace_frame)
357 .into_iter()
358 .collect()
359 } else {
360 match obj.get("frames") {
361 Some(WireValue::Array(frames)) => frames.iter().filter_map(parse_trace_frame).collect(),
362 _ => Vec::new(),
363 }
364 }
365}
366
367fn parse_trace_frame(value: &WireValue) -> Option<AnyErrorFrame> {
368 let obj = value.as_object()?;
369 Some(AnyErrorFrame {
370 function: obj
371 .get("function")
372 .and_then(WireValue::as_str)
373 .map(str::to_string),
374 file: obj
375 .get("file")
376 .and_then(WireValue::as_str)
377 .map(str::to_string),
378 line: obj.get("line").and_then(as_usize),
379 column: obj.get("column").and_then(as_usize),
380 ip: obj.get("ip").and_then(as_usize),
381 })
382}
383
384fn as_usize(value: &WireValue) -> Option<usize> {
385 match value {
386 WireValue::Integer(i) if *i >= 0 => Some(*i as usize),
387 WireValue::Number(n) if *n >= 0.0 => Some(*n as usize),
388 WireValue::I8(v) if *v >= 0 => Some(*v as usize),
389 WireValue::U8(v) => Some(*v as usize),
390 WireValue::I16(v) if *v >= 0 => Some(*v as usize),
391 WireValue::U16(v) => Some(*v as usize),
392 WireValue::I32(v) if *v >= 0 => Some(*v as usize),
393 WireValue::U32(v) => Some(*v as usize),
394 WireValue::I64(v) if *v >= 0 => Some(*v as usize),
395 WireValue::U64(v) => usize::try_from(*v).ok(),
396 WireValue::Isize(v) if *v >= 0 => Some(*v as usize),
397 WireValue::Usize(v) => usize::try_from(*v).ok(),
398 WireValue::Ptr(v) => usize::try_from(*v).ok(),
399 WireValue::F32(v) if *v >= 0.0 => Some(*v as usize),
400 _ => None,
401 }
402}
403
404fn brief_value(value: &WireValue) -> String {
405 match value {
406 WireValue::Null => "null".to_string(),
407 WireValue::Bool(v) => v.to_string(),
408 WireValue::Integer(v) => v.to_string(),
409 WireValue::Number(v) => v.to_string(),
410 WireValue::I8(v) => v.to_string(),
411 WireValue::U8(v) => v.to_string(),
412 WireValue::I16(v) => v.to_string(),
413 WireValue::U16(v) => v.to_string(),
414 WireValue::I32(v) => v.to_string(),
415 WireValue::U32(v) => v.to_string(),
416 WireValue::I64(v) => v.to_string(),
417 WireValue::U64(v) => v.to_string(),
418 WireValue::Isize(v) => v.to_string(),
419 WireValue::Usize(v) => v.to_string(),
420 WireValue::Ptr(v) => format!("0x{v:x}"),
421 WireValue::F32(v) => v.to_string(),
422 WireValue::String(v) => v.clone(),
423 WireValue::Result { ok, value } => {
424 if *ok {
425 format!("Ok({})", brief_value(value))
426 } else {
427 format!("Err({})", brief_value(value))
428 }
429 }
430 WireValue::Object(_) => "{object}".to_string(),
431 WireValue::Array(v) => format!("[array:{}]", v.len()),
432 WireValue::FunctionRef { name } => format!("fn {}", name),
433 WireValue::Timestamp(ts) => format!("ts({})", ts),
434 WireValue::Duration { value, unit } => format!("{value:?}{unit:?}"),
435 WireValue::Range { .. } => "<range>".to_string(),
436 WireValue::Table(t) => format!("<table {}x{}>", t.row_count, t.column_count),
437 WireValue::PrintResult(pr) => pr.rendered.clone(),
438 WireValue::Content(node) => format!("<content:{}>", node),
439 }
440}
441
442fn escape_html(text: &str) -> String {
443 text.chars()
444 .flat_map(|c| match c {
445 '&' => "&".chars().collect::<Vec<_>>(),
446 '<' => "<".chars().collect::<Vec<_>>(),
447 '>' => ">".chars().collect::<Vec<_>>(),
448 '"' => """.chars().collect::<Vec<_>>(),
449 '\'' => "'".chars().collect::<Vec<_>>(),
450 _ => vec![c],
451 })
452 .collect()
453}
454
455trait WireValueObjectExt {
456 fn as_object(&self) -> Option<&std::collections::BTreeMap<String, WireValue>>;
457}
458
459impl WireValueObjectExt for WireValue {
460 fn as_object(&self) -> Option<&std::collections::BTreeMap<String, WireValue>> {
461 if let WireValue::Object(obj) = self {
462 Some(obj)
463 } else {
464 None
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use std::collections::BTreeMap;
473
474 fn trace_frame(function: &str, file: &str, line: i64, column: i64, ip: i64) -> WireValue {
475 let mut obj = BTreeMap::new();
476 obj.insert(
477 "function".to_string(),
478 WireValue::String(function.to_string()),
479 );
480 obj.insert("file".to_string(), WireValue::String(file.to_string()));
481 obj.insert("line".to_string(), WireValue::Integer(line));
482 obj.insert("column".to_string(), WireValue::Integer(column));
483 obj.insert("ip".to_string(), WireValue::Integer(ip));
484 WireValue::Object(obj)
485 }
486
487 fn trace_info_single(frame: WireValue) -> WireValue {
488 let mut obj = BTreeMap::new();
489 obj.insert("kind".to_string(), WireValue::String("single".to_string()));
490 obj.insert("frame".to_string(), frame);
491 WireValue::Object(obj)
492 }
493
494 fn any_error(
495 message: &str,
496 code: Option<&str>,
497 cause: Option<WireValue>,
498 trace_info: WireValue,
499 ) -> WireValue {
500 let mut obj = BTreeMap::new();
501 obj.insert(
502 "category".to_string(),
503 WireValue::String("AnyError".to_string()),
504 );
505 obj.insert(
506 "payload".to_string(),
507 WireValue::String(message.to_string()),
508 );
509 obj.insert("cause".to_string(), cause.unwrap_or(WireValue::Null));
510 obj.insert("trace_info".to_string(), trace_info);
511 obj.insert(
512 "message".to_string(),
513 WireValue::String(message.to_string()),
514 );
515 obj.insert(
516 "code".to_string(),
517 code.map(|c| WireValue::String(c.to_string()))
518 .unwrap_or(WireValue::Null),
519 );
520 WireValue::Object(obj)
521 }
522
523 #[test]
524 fn parse_and_render_plain() {
525 let root = any_error(
526 "low level",
527 None,
528 None,
529 trace_info_single(trace_frame("read_file", "cfg.shape", 3, 10, 11)),
530 );
531 let outer = any_error(
532 "high level",
533 Some("OPTION_NONE"),
534 Some(root),
535 trace_info_single(trace_frame("load_config", "cfg.shape", 7, 12, 29)),
536 );
537
538 let parsed = AnyError::from_wire(&outer).expect("should parse anyerror");
539 assert_eq!(parsed.code.as_deref(), Some("OPTION_NONE"));
540 assert_eq!(parsed.frames[0].line, Some(7));
541 assert_eq!(parsed.frames[0].column, Some(12));
542
543 let rendered = PlainAnyErrorRenderer.render(&parsed);
544 assert!(rendered.contains("Uncaught exception:"));
545 assert!(rendered.contains("Error [OPTION_NONE]: high level"));
546 assert!(rendered.contains("cfg.shape:7:12"));
547 assert!(rendered.contains("Caused by: low level"));
548 }
549
550 #[test]
551 fn render_ansi_and_html() {
552 let err = any_error(
553 "boom <bad>",
554 Some("E_TEST"),
555 None,
556 trace_info_single(trace_frame("run", "main.shape", 1, 2, 3)),
557 );
558 let ansi = render_any_error_ansi(&err).expect("ansi render");
559 assert!(ansi.contains("\x1b[1;31m"));
560 assert!(ansi.contains("E_TEST"));
561
562 let html = render_any_error_html(&err).expect("html render");
563 assert!(html.contains("shape-error"));
564 assert!(html.contains("E_TEST"));
565 assert!(html.contains("<bad>"));
566 }
567}