#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unsafe_code,
unused_import_braces,
unused_qualifications
)]
use core::fmt::{self, Debug, Display};
use std::error::Error;
const SUMMARY_PREFIX: &str = "\u{001b}[97;41;22mError:\u{001b}[91;49;1m ";
const REASON_PREFIX: &str = "\u{001b}[93;49;1m - \u{001b}[97;49;1m";
const HELPTEXT_PREFIX: &str = "\u{001b}[37;49;2m";
const RESET: &str = "\u{001b}[0m";
fn error_sources(mut source: Option<&(dyn Error + 'static)>) -> Option<Vec<String>> {
if source.is_some() {
let mut reasons = Vec::new();
while let Some(error) = source {
reasons.push(error.to_string());
source = error.source();
}
Some(reasons)
} else {
None
}
}
pub trait UFE: Error {
fn summary(&self) -> String {
self.to_string()
}
fn reasons(&self) -> Option<Vec<String>> {
error_sources(self.source())
}
fn helptext(&self) -> Option<String> {
None
}
fn pretty_summary(&self) -> String {
[SUMMARY_PREFIX, &self.summary(), RESET].concat()
}
fn pretty_reasons(&self) -> Option<String> {
if let Some(reasons) = self.reasons() {
let mut reason_strings = Vec::with_capacity(reasons.len());
for reason in reasons {
let bullet_point = [REASON_PREFIX, &reason].concat();
reason_strings.push(bullet_point);
}
Some([&reason_strings.join("\n"), RESET].concat())
} else {
None
}
}
fn pretty_helptext(&self) -> Option<String> {
if let Some(helptext) = self.helptext() {
Some([HELPTEXT_PREFIX, &helptext, RESET].concat())
} else {
None
}
}
fn print(&self) {
eprintln!("{}", self.pretty_summary());
if let Some(reasons) = self.pretty_reasons() {
eprintln!("{}", reasons);
}
if let Some(helptext) = self.pretty_helptext() {
eprintln!("{}", helptext);
}
}
fn print_and_exit(&self) {
self.print();
std::process::exit(1)
}
}
#[derive(Debug)]
pub struct UserFacingError {
summary: String,
reasons: Option<Vec<String>>,
helptext: Option<String>,
source: Option<Box<(dyn Error)>>,
}
impl UFE for UserFacingError {
fn summary(&self) -> String {
self.summary.clone()
}
fn reasons(&self) -> Option<Vec<String>> {
self.reasons.clone()
}
fn helptext(&self) -> Option<String> {
self.helptext.clone()
}
}
impl Display for UserFacingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let summary = self.pretty_summary();
let reasons = self.pretty_reasons();
let helptext = self.pretty_helptext();
match (summary, reasons, helptext) {
(summary, None, None) => writeln!(f, "{}", summary),
(summary, Some(reasons), None) => writeln!(f, "{}\n{}", summary, reasons),
(summary, None, Some(helptext)) => writeln!(f, "{}\n{}", summary, helptext),
(summary, Some(reasons), Some(helptext)) => {
writeln!(f, "{}\n{}\n{}", summary, reasons, helptext)
}
}
}
}
impl Error for UserFacingError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self.source {
Some(_) => self.source.as_deref(),
None => None,
}
}
}
impl From<Box<(dyn Error)>> for UserFacingError {
fn from(error: Box<(dyn Error)>) -> UserFacingError {
let summary = error.to_string();
let reasons = error_sources(error.source());
let source = Some(error);
UserFacingError {
summary,
reasons,
helptext: None,
source,
}
}
}
impl From<&(dyn Error)> for UserFacingError {
fn from(error: &(dyn Error)) -> UserFacingError {
let summary = error.to_string();
let reasons = error_sources(error.source());
UserFacingError {
summary,
reasons,
helptext: None,
source: None,
}
}
}
impl<T: Debug> From<Result<T, Box<dyn Error>>> for UserFacingError {
fn from(error: Result<T, Box<dyn Error>>) -> UserFacingError {
let error = error.unwrap_err();
let summary = error.to_string();
let reasons = error_sources(error.source());
UserFacingError {
summary,
reasons,
helptext: None,
source: None,
}
}
}
impl UserFacingError {
pub fn new(summary: &str) -> UserFacingError {
UserFacingError {
summary: summary.to_string(),
reasons: None,
helptext: None,
source: None,
}
}
pub fn update(&mut self, summary: &str) {
self.summary = summary.into();
}
pub fn push(&mut self, summary: &str) {
match self.reasons.as_mut() {
Some(reasons) => reasons.push(summary.into()),
None => self.reasons = Some(vec![summary.into()]),
}
}
pub fn reason(mut self, reason: &str) -> UserFacingError {
self.reasons = match self.reasons {
Some(mut reasons) => {
reasons.push(reason.into());
Some(reasons)
}
None => Some(vec![reason.into()]),
};
self
}
pub fn clear_reasons(&mut self) {
self.reasons = None;
}
pub fn help(mut self, helptext: &str) -> UserFacingError {
self.helptext = Some(helptext.into());
self
}
pub fn clear_helptext(&mut self) {
self.helptext = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
static S: &'static str = "Test Error";
static R: &'static str = "Reason 1";
static H: &'static str = "Try Again";
#[test]
fn new_test() {
eprintln!("{}", UserFacingError::new("Test Error"));
}
#[test]
fn summary_test() {
let e = UserFacingError::new(S);
let expected = [SUMMARY_PREFIX, S, RESET, "\n"].concat();
assert_eq!(e.to_string(), String::from(expected));
eprintln!("{}", e);
}
#[test]
fn helptext_test() {
let e = UserFacingError::new(S).help(H);
let expected = format!(
"{}{}{}\n{}{}{}\n",
SUMMARY_PREFIX, S, RESET, HELPTEXT_PREFIX, H, RESET
);
assert_eq!(e.to_string(), String::from(expected));
eprintln!("{}", e);
}
#[test]
fn reason_test() {
let e = UserFacingError::new(S).reason(R).reason(R);
let reasons = vec![String::from(R), String::from(R)];
let mut reason_strings = Vec::with_capacity(reasons.len());
for reason in reasons {
let bullet_point = [REASON_PREFIX, &reason].concat();
reason_strings.push(bullet_point);
}
let reasons = [&reason_strings.join("\n"), RESET].concat();
let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, S, RESET, reasons);
assert_eq!(e.to_string(), String::from(expected));
eprintln!("{}", e);
}
#[test]
fn reason_and_helptext_test() {
let e = UserFacingError::new(S).reason(R).reason(R).help(H);
let reasons = vec![String::from(R), String::from(R)];
let mut reason_strings = Vec::with_capacity(reasons.len());
for reason in reasons {
let bullet_point = [REASON_PREFIX, &reason].concat();
reason_strings.push(bullet_point);
}
let reasons = [&reason_strings.join("\n"), RESET].concat();
let expected = format!(
"{}{}{}\n{}\n{}{}{}\n",
SUMMARY_PREFIX, S, RESET, reasons, HELPTEXT_PREFIX, H, RESET
);
assert_eq!(e.to_string(), String::from(expected));
eprintln!("{}", e);
}
#[test]
fn from_error_test() {
let error_text = "Error";
let ioe = std::io::Error::new(std::io::ErrorKind::Other, error_text);
fn de(ioe: std::io::Error) -> Box<dyn Error> {
Box::new(ioe)
}
let ufe: UserFacingError = de(ioe).into();
let expected = [SUMMARY_PREFIX, error_text, RESET, "\n"].concat();
assert_eq!(ufe.to_string(), expected);
}
#[test]
fn from_error_source_test() {
let ufe: UserFacingError = get_super_error().into();
let expected = [
SUMMARY_PREFIX,
"SuperError",
RESET,
"\n",
REASON_PREFIX,
"Sidekick",
RESET,
"\n",
]
.concat();
assert_eq!(ufe.to_string(), expected);
}
#[derive(Debug)]
struct SuperError {
side: SuperErrorSideKick,
}
impl Display for SuperError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SuperError")
}
}
impl Error for SuperError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.side)
}
}
#[derive(Debug)]
struct SuperErrorSideKick;
impl Display for SuperErrorSideKick {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Sidekick")
}
}
impl Error for SuperErrorSideKick {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
fn get_super_error() -> Result<(), Box<dyn Error>> {
Err(Box::new(SuperError {
side: SuperErrorSideKick,
}))
}
}