#![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
}
}
fn pretty_summary(summary: &str) -> String {
[SUMMARY_PREFIX, summary, RESET].concat()
}
fn pretty_reasons(reasons: Reasons) -> Option<String> {
if let Some(reasons) = 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(helptext: Helptext) -> Option<String> {
if let Some(helptext) = helptext {
Some([HELPTEXT_PREFIX, &helptext, RESET].concat())
} 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 print(&self) {
eprintln!("{}", pretty_summary(&self.summary()));
if let Some(reasons) = pretty_reasons(self.reasons()) {
eprintln!("{}", reasons);
}
if let Some(helptext) = pretty_helptext(self.helptext()) {
eprintln!("{}", helptext);
}
}
fn print_and_exit(&self) {
self.print();
std::process::exit(1)
}
fn into_ufe(&self) -> UserFacingError {
UserFacingError {
summary: self.summary(),
reasons: self.reasons(),
helptext: self.helptext(),
source: None,
}
}
}
type Summary = String;
type Reasons = Option<Vec<String>>;
type Helptext = Option<String>;
type Source = Option<Box<(dyn Error)>>;
#[derive(Debug)]
pub struct UserFacingError {
summary: Summary,
reasons: Reasons,
helptext: Helptext,
source: Source,
}
impl Display for UserFacingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let summary = pretty_summary(&self.summary());
let reasons = pretty_reasons(self.reasons());
let helptext = pretty_helptext(self.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 UFE for UserFacingError {
fn summary(&self) -> Summary {
self.summary.clone()
}
fn reasons(&self) -> Reasons {
self.reasons.clone()
}
fn helptext(&self) -> Helptext {
self.helptext.clone()
}
}
fn get_ufe_struct_members(error: &(dyn Error)) -> (Summary, Reasons) {
let summary = error.to_string();
let reasons = error_sources(error.source());
(summary, reasons)
}
impl From<std::io::Error> for UserFacingError {
fn from(error: std::io::Error) -> UserFacingError {
let (summary, reasons) = get_ufe_struct_members(&error);
UserFacingError {
summary,
reasons,
helptext: None,
source: Some(Box::new(error)),
}
}
}
impl From<Box<(dyn Error)>> for UserFacingError {
fn from(error: Box<(dyn Error)>) -> UserFacingError {
let (summary, reasons) = get_ufe_struct_members(error.as_ref());
UserFacingError {
summary,
reasons,
helptext: None,
source: Some(error),
}
}
}
impl From<&(dyn Error)> for UserFacingError {
fn from(error: &(dyn Error)) -> UserFacingError {
let (summary, reasons) = get_ufe_struct_members(error);
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, reasons) = get_ufe_struct_members(error.as_ref());
UserFacingError {
summary,
reasons,
helptext: None,
source: Some(error),
}
}
}
impl UserFacingError {
pub fn new<S: Into<String>>(summary: S) -> UserFacingError {
UserFacingError {
summary: summary.into(),
reasons: None,
helptext: None,
source: None,
}
}
pub fn update<S: Into<String>>(&mut self, summary: S) {
self.summary = summary.into();
}
pub fn push<S: Into<String>>(&mut self, new_summary: S) {
let old_summary = self.summary();
match self.reasons.as_mut() {
Some(reasons) => reasons.insert(0, old_summary),
None => self.reasons = Some(vec![old_summary]),
}
self.summary = new_summary.into();
}
pub fn reason<S: Into<String>>(mut self, reason: S) -> 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<S: Into<String>>(mut self, helptext: S) -> 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(), 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(), expected);
eprintln!("{}", e);
}
#[test]
fn push_test() {
let mut e = UserFacingError::new(S).reason("R1");
e.push("R2");
let reasons = vec![String::from(S), String::from("R1")];
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, "R2", RESET, reasons);
assert_eq!(e.to_string(), expected);
eprintln!("{}", e);
}
#[test]
fn push_test_empty() {
let mut e = UserFacingError::new(S);
e.push("S2");
let reasons = vec![String::from(S)];
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, "S2", RESET, reasons);
assert_eq!(e.to_string(), 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(), 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,
}))
}
#[derive(Debug)]
struct MyError {
mssg: String,
src: Option<Box<dyn Error>>,
}
impl Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.mssg.to_string())
}
}
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.src.as_deref()
}
}
impl UFE for MyError {}
#[test]
fn custom_error_implements_ufe() {
let me = MyError {
mssg: "Program Failed".into(),
src: Some(Box::new(MyError {
mssg: "Reason 1".into(),
src: Some(Box::new(MyError {
mssg: "Reason 2".into(),
src: None,
})),
})),
};
me.print();
me.into_ufe().help("Helptext Added").print();
}
}