use std::fmt;
pub trait ErrorTip: std::error::Error {
fn tip(&self) -> Option<String>;
}
#[derive(Debug)]
pub struct Diagnosed {
tip: String,
}
impl Diagnosed {
pub fn new(tip: impl Into<String>) -> Self {
Self { tip: tip.into() }
}
pub fn tip(&self) -> &str {
&self.tip
}
}
impl fmt::Display for Diagnosed {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}
impl std::error::Error for Diagnosed {}
#[derive(Debug)]
pub struct DiagnosedError {
tip: String,
source: anyhow::Error,
}
impl fmt::Display for DiagnosedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.source)
}
}
impl std::error::Error for DiagnosedError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.source()
}
}
impl DiagnosedError {
pub fn tip(&self) -> &str {
&self.tip
}
}
pub trait DiagnoseWithTip<T> {
fn diagnose(self) -> anyhow::Result<T>;
}
impl<T, E> DiagnoseWithTip<T> for Result<T, E>
where
E: ErrorTip + Send + Sync + 'static,
{
fn diagnose(self) -> anyhow::Result<T> {
self.map_err(|e| {
let tip = e.tip();
let err: anyhow::Error = e.into();
match tip {
Some(t) => {
let diagnosed = DiagnosedError {
tip: t,
source: err,
};
anyhow::Error::new(diagnosed)
}
None => err,
}
})
}
}
pub trait DiagnoseExt<T> {
fn with_diagnosis(self, tip: impl Into<String>) -> anyhow::Result<T>;
}
impl<T, E> DiagnoseExt<T> for Result<T, E>
where
E: Into<anyhow::Error>,
{
fn with_diagnosis(self, tip: impl Into<String>) -> anyhow::Result<T> {
self.map_err(|e| {
let source = e.into();
anyhow::Error::new(DiagnosedError {
tip: tip.into(),
source,
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TestError {
msg: String,
has_tip: bool,
}
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.msg)
}
}
impl std::error::Error for TestError {}
impl ErrorTip for TestError {
fn tip(&self) -> Option<String> {
if self.has_tip {
Some("Try doing X instead.".into())
} else {
None
}
}
}
#[test]
fn diagnose_captures_tip() {
let result: Result<(), TestError> = Err(TestError {
msg: "something broke".into(),
has_tip: true,
});
let err = result.diagnose().unwrap_err();
let diagnosed = err.downcast_ref::<DiagnosedError>();
assert!(
diagnosed.is_some(),
"DiagnosedError not found at top of chain"
);
assert_eq!(diagnosed.unwrap().tip(), "Try doing X instead.");
}
#[test]
fn diagnose_without_tip_skips_marker() {
let result: Result<(), TestError> = Err(TestError {
msg: "something broke".into(),
has_tip: false,
});
let err = result.diagnose().unwrap_err();
assert!(
err.downcast_ref::<DiagnosedError>().is_none(),
"DiagnosedError should not be present when tip() returns None"
);
}
#[test]
fn with_diagnosis_attaches_tip() {
let result: Result<(), std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
let err = result.with_diagnosis("Check the path exists.").unwrap_err();
let diagnosed = err.downcast_ref::<DiagnosedError>();
assert!(
diagnosed.is_some(),
"DiagnosedError not found at top of chain"
);
assert_eq!(diagnosed.unwrap().tip(), "Check the path exists.");
}
#[test]
fn diagnosed_display_is_empty() {
let d = Diagnosed::new("some tip");
assert_eq!(d.to_string(), "");
}
}