diagprint 0.8.1

A Rust diagnostics lifecycle framework for structured diagnostics, rendering, remediation, CI, editors, and telemetry.
Documentation
use diagprint::{
    Applicability, DiagnosticErrorExt, DiagnosticMetadata, Reporter, Severity, Suggestion,
};
use std::io;
use thiserror::Error;

#[derive(Debug, Error)]
enum DemoError {
    #[error("configuration loading failed")]
    Config {
        #[source]
        source: io::Error,
    },

    #[error("unsupported mode `{mode}`")]
    Mode { mode: String },
}

impl DiagnosticMetadata for DemoError {
    fn diagnostic_severity(&self) -> Severity {
        match self {
            Self::Config { .. } => Severity::Error,
            Self::Mode { .. } => Severity::Warning,
        }
    }

    fn diagnostic_code(&self) -> Option<String> {
        Some(
            match self {
                Self::Config { .. } => "DEMO-CONFIG",
                Self::Mode { .. } => "DEMO-MODE",
            }
            .into(),
        )
    }

    fn diagnostic_help(&self) -> Option<String> {
        Some(match self {
            Self::Config { .. } => "Verify the configuration source.".into(),

            Self::Mode { .. } => "Choose a supported mode.".into(),
        })
    }

    fn diagnostic_notes(&self) -> Vec<String> {
        vec!["typed diagnostic metadata".into()]
    }

    fn diagnostic_suggestions(&self) -> Vec<Suggestion> {
        match self {
            Self::Mode { .. } => {
                vec![
                    Suggestion::new("Select a supported mode").applicability(Applicability::Manual),
                ]
            }

            Self::Config { .. } => Vec::new(),
        }
    }
}

#[test]
fn typed_metadata_is_applied() {
    let reporter = Reporter::builder().build().unwrap();

    let error = DemoError::Mode {
        mode: "quantum".into(),
    };

    let diagnostic = reporter.diagnostic_from_typed_error(&error);

    assert_eq!(diagnostic.severity, Severity::Warning);
    assert_eq!(diagnostic.code.as_deref(), Some("DEMO-MODE"));

    assert_eq!(diagnostic.help.as_deref(), Some("Choose a supported mode."));

    assert_eq!(diagnostic.notes, vec!["typed diagnostic metadata"]);

    assert_eq!(diagnostic.suggestions.len(), 1);

    assert_eq!(diagnostic.suggestions[0].title, "Select a supported mode");
}

#[test]
fn thiserror_source_chain_is_preserved() {
    let reporter = Reporter::builder().build().unwrap();

    let error = DemoError::Config {
        source: io::Error::new(io::ErrorKind::NotFound, "settings file is missing"),
    };

    let diagnostic = reporter.diagnostic_from_typed_error(&error);

    let cause = diagnostic.cause.expect("source cause");

    assert_eq!(cause.message, "settings file is missing");

    assert!(cause.source.is_none());
}

#[test]
fn nested_io_error_receives_inferred_intelligence() {
    let reporter = Reporter::builder().build().unwrap();

    let error = DemoError::Config {
        source: io::Error::new(io::ErrorKind::PermissionDenied, "access denied"),
    };

    let diagnostic = reporter.diagnostic_from_typed_error(&error);

    assert_eq!(diagnostic.suggestions.len(), 1);

    let suggestion = &diagnostic.suggestions[0];

    assert_eq!(suggestion.applicability, Applicability::Manual);

    assert!(suggestion.title.to_ascii_lowercase().contains("permission"));
}

#[test]
fn extension_trait_matches_reporter_conversion() {
    let reporter = Reporter::builder().build().unwrap();

    let error = DemoError::Mode {
        mode: "unknown".into(),
    };

    let via_reporter = reporter.diagnostic_from_typed_error(&error);

    let via_extension = error.to_diagprint(&reporter);

    assert_eq!(via_reporter.message, via_extension.message);

    assert_eq!(via_reporter.code, via_extension.code);

    assert_eq!(via_reporter.severity, via_extension.severity);
}

#[test]
fn inferred_io_suggestion_does_not_become_machine_applicable() {
    let reporter = Reporter::builder().build().unwrap();

    let error = DemoError::Config {
        source: io::Error::new(io::ErrorKind::NotFound, "missing"),
    };

    let diagnostic = error.to_diagprint(&reporter);

    assert!(
        diagnostic
            .suggestions
            .iter()
            .all(|suggestion| { suggestion.applicability != Applicability::MachineApplicable })
    );
}