ts_rust_helper/
error.rs

1//! Helpers for working with errors.
2
3use core::{
4    error::Error,
5    fmt::{self, Write},
6    panic::Location,
7};
8use std::{env::current_exe, ffi::OsStr, path::PathBuf};
9
10use crate::style::{BOLD, RED, RESET};
11
12/// Trait to log a result.
13pub trait ErrorLogger {
14    /// Log the result
15    #[track_caller]
16    fn log_error(self) -> Self;
17}
18
19impl<T, E: fmt::Display> ErrorLogger for Result<T, E> {
20    #[track_caller]
21    fn log_error(self) -> Self {
22        if let Err(error) = self.as_ref() {
23            let location = Location::caller();
24            #[cfg(feature = "log")]
25            log::error!("[{location}] {error}");
26            #[cfg(not(feature = "log"))]
27            eprintln!("{BOLD}{RED}error{RESET}{BOLD}:{RESET} [{location}] {error}");
28        }
29        self
30    }
31}
32impl<T> ErrorLogger for Option<T> {
33    #[track_caller]
34    fn log_error(self) -> Self {
35        if self.is_none() {
36            let location = Location::caller();
37            #[cfg(feature = "log")]
38            log::error!("[{location}] value was None");
39            #[cfg(not(feature = "log"))]
40            eprintln!("{BOLD}{RED}error{RESET}{BOLD}:{RESET} [{location}] value was None");
41        }
42        self
43    }
44}
45
46/// Type alias for a program that reports it's exit.
47pub type ReportProgramExit = Result<(), ProgramReport>;
48
49/// A report for a program exit.
50pub struct ProgramReport(Box<dyn Error + 'static>);
51impl<E: Error + 'static> From<E> for ProgramReport {
52    fn from(value: E) -> Self {
53        Self(Box::new(value))
54    }
55}
56impl fmt::Debug for ProgramReport {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "{self}")
59    }
60}
61impl fmt::Display for ProgramReport {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        let report = Report::new(self.0.as_ref(), ErrorStackStyle::Stacked { indent: 2 });
64
65        let exe_path = current_exe().unwrap_or_else(|_| PathBuf::from("program"));
66        let exe = exe_path
67            .file_name()
68            .unwrap_or_else(|| OsStr::new("program"))
69            .to_string_lossy();
70
71        writeln!(f, "`{exe}` exited unsuccessfully")?;
72        write!(f, "{report}")
73    }
74}
75
76/// Extension trait for reporting a result
77pub trait IntoErrorReport<'a, T>: Sized {
78    /// Convert the result into a report.
79    fn into_report(self) -> Result<T, Report<'a>>;
80}
81
82impl<'a, T, E: Error + 'a> IntoErrorReport<'a, T> for Result<T, E> {
83    fn into_report(self) -> Result<T, Report<'a>> {
84        self.map_err(|source| Report::new(source, ErrorStackStyle::default()))
85    }
86}
87
88impl<'a, T> IntoErrorReport<'a, T> for Option<T> {
89    fn into_report(self) -> Result<T, Report<'a>> {
90        #[derive(Debug)]
91        struct NoneError;
92        impl fmt::Display for NoneError {
93            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94                write!(f, "value was none")
95            }
96        }
97        impl Error for NoneError {}
98
99        self.ok_or_else(|| Report::new(NoneError, ErrorStackStyle::default()))
100    }
101}
102
103/// A report of an error.
104pub struct Report<'a> {
105    /// The source of the error.
106    pub source: Box<dyn Error + 'a>,
107    /// The style of the error.
108    pub style: ErrorStackStyle<'a>,
109}
110impl<'a> Report<'a> {
111    /// Create a new report.
112    pub fn new<E: Error + 'a>(source: E, style: ErrorStackStyle<'a>) -> Self {
113        Self {
114            source: Box::new(source),
115            style,
116        }
117    }
118
119    #[track_caller]
120    /// Log this report as an error
121    pub fn log_error(&self) {
122        let location = Location::caller();
123        #[cfg(feature = "log")]
124        log::error!("[{location}] {self}");
125        #[cfg(not(feature = "log"))]
126        eprintln!("{BOLD}{RED}error{RESET}{BOLD}:{RESET} [{location}] {self}");
127    }
128}
129impl Error for Report<'static> {
130    fn source(&self) -> Option<&(dyn Error + 'static)> {
131        Some(self.source.as_ref())
132    }
133}
134impl fmt::Debug for Report<'_> {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "{self}")
137    }
138}
139impl fmt::Display for Report<'_> {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        let output = self.style.display(self.source.as_ref())?;
142        writeln!(f, "{output}")
143    }
144}
145
146/// Alias for a closure to format an error.
147pub type FmtErrorClosure<'a> = Box<dyn Fn(&mut String, usize, &dyn Error) -> fmt::Result + 'a>;
148
149/// An error stack style.
150pub enum ErrorStackStyle<'a> {
151    /// An inline style.
152    Inline,
153    /// A stacked style
154    Stacked {
155        /// The indent for each item in the stack.
156        indent: usize,
157    },
158    /// A custom style
159    Custom(FmtErrorClosure<'a>),
160}
161impl Default for ErrorStackStyle<'_> {
162    fn default() -> Self {
163        Self::Stacked { indent: 2 }
164    }
165}
166
167impl ErrorStackStyle<'_> {
168    /// Display an error in the given style.
169    pub fn display(&self, source: &dyn Error) -> Result<String, fmt::Error> {
170        let mut output = String::new();
171
172        let fmt_fn = self.fmt_fn();
173
174        let mut current_error = Some(source);
175        let mut index = 1;
176        while let Some(error) = current_error {
177            fmt_fn(&mut output, index, error)?;
178            current_error = error.source();
179            index += 1;
180        }
181
182        Ok(output)
183    }
184
185    fn fmt_fn(&self) -> FmtErrorClosure<'_> {
186        match &self {
187            Self::Inline => Box::new(|f, i, e| write!(f, " ----- {i}. {e}")),
188
189            Self::Stacked { indent } => Box::new(|f, i, e| {
190                writeln!(
191                    f,
192                    "{}{BOLD}{RED}{i}{RESET}{BOLD}.{RESET} {e}",
193                    " ".repeat(*indent)
194                )
195            }),
196
197            Self::Custom(f) => Box::new(f),
198        }
199    }
200}