Skip to main content

pimalaya_cli/
error.rs

1//! Command failure reporting to stdout.
2
3use std::{
4    backtrace::{Backtrace, BacktraceStatus},
5    fmt, process,
6};
7
8use anyhow::{Error, Result};
9use log::{Level, log_enabled};
10use serde::{Serialize, Serializer, ser::SerializeStruct};
11
12use crate::printer::Printer;
13
14/// Wraps an [`anyhow::Error`] to render it, with its source chain and
15/// backtrace, as text or JSON.
16pub struct ErrorReport(Error);
17
18impl ErrorReport {
19    /// Unwraps a result, or prints the error to the printer and exits
20    /// with status 1.
21    pub fn eval<T>(printer: &mut impl Printer, result: Result<T, Error>) -> T {
22        match result {
23            Ok(res) => res,
24            Err(err) => {
25                printer.out(ErrorReport::from(err)).unwrap();
26                process::exit(1);
27            }
28        }
29    }
30
31    fn sources(&self) -> impl Iterator<Item = String> + '_ {
32        self.0.chain().skip(1).map(ToString::to_string)
33    }
34
35    fn suggestions(&self) -> Option<&str> {
36        if !log_enabled!(Level::Debug) || !log_enabled!(Level::Trace) {
37            Some("Run with --log-level to enable more verbose logs")
38        } else {
39            None
40        }
41    }
42
43    fn backtrace(&self) -> Option<&Backtrace> {
44        let backtrace = self.0.backtrace();
45
46        if let BacktraceStatus::Captured = backtrace.status() {
47            Some(backtrace)
48        } else {
49            None
50        }
51    }
52}
53
54impl From<Error> for ErrorReport {
55    fn from(err: Error) -> Self {
56        Self(err)
57    }
58}
59
60impl fmt::Display for ErrorReport {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "Error: {}", self.0)?;
63
64        let mut header_printed = false;
65        for err in self.sources() {
66            if !header_printed {
67                writeln!(f)?;
68                writeln!(f)?;
69                write!(f, "Caused by:")?;
70                header_printed = true;
71            }
72
73            writeln!(f)?;
74            write!(f, " - {}", err.trim())?;
75        }
76
77        if let Some(backtrace) = self.backtrace() {
78            writeln!(f)?;
79            writeln!(f)?;
80            writeln!(f, "Backtrace:")?;
81            write!(f, "{backtrace}")?;
82        }
83
84        if let Some(suggestion) = self.suggestions() {
85            writeln!(f)?;
86            writeln!(f)?;
87            writeln!(f, "Suggestions:")?;
88            write!(f, " - {suggestion}")?;
89        }
90
91        writeln!(f)
92    }
93}
94
95impl Serialize for ErrorReport {
96    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
97        let sources: Vec<_> = self.0.chain().skip(1).map(ToString::to_string).collect();
98        let backtrace = self.backtrace().map(ToString::to_string);
99
100        let mut s = serializer.serialize_struct("ErrorReport", 3)?;
101        s.serialize_field("error", &self.0.to_string())?;
102        s.serialize_field("sources", &sources)?;
103        s.serialize_field("backtrace", &backtrace)?;
104        s.end()
105    }
106}