use std::fmt;
use miette::{Diagnostic, Report};
pub struct ExitCoded {
code: u8,
source: Box<dyn Diagnostic + Send + Sync + 'static>,
}
impl ExitCoded {
pub fn new(code: u8, source: impl Diagnostic + Send + Sync + 'static) -> Self {
Self { code, source: Box::new(source) }
}
#[must_use]
pub const fn exit_code(&self) -> u8 {
self.code
}
}
impl fmt::Debug for ExitCoded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.source, f)
}
}
impl fmt::Display for ExitCoded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.source, f)
}
}
impl std::error::Error for ExitCoded {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.source()
}
}
impl Diagnostic for ExitCoded {
fn code(&self) -> Option<Box<dyn fmt::Display + '_>> {
self.source.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.source.severity()
}
fn help(&self) -> Option<Box<dyn fmt::Display + '_>> {
self.source.help()
}
fn url(&self) -> Option<Box<dyn fmt::Display + '_>> {
self.source.url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.source.source_code()
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
self.source.labels()
}
fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
self.source.related()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.source.diagnostic_source()
}
}
pub trait WithExitCode: Sized {
fn with_exit_code(self, code: u8) -> ExitCoded;
}
impl<E> WithExitCode for E
where
E: Diagnostic + Send + Sync + 'static,
{
fn with_exit_code(self, code: u8) -> ExitCoded {
ExitCoded::new(code, self)
}
}
#[must_use]
pub fn exit_code_of(report: &Report) -> Option<u8> {
report.downcast_ref::<ExitCoded>().map(ExitCoded::exit_code)
}
#[cfg(test)]
mod tests {
use super::{exit_code_of, ExitCoded, WithExitCode};
use miette::{Diagnostic, Report};
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[error("boom: {0}")]
#[diagnostic(code(test::boom), help("try again"))]
struct Boom(&'static str);
#[test]
fn attaches_and_reads_back_the_code() {
let report: Report = Boom("x").with_exit_code(2).into();
assert_eq!(exit_code_of(&report), Some(2));
}
#[test]
fn plain_error_has_no_attached_code() {
let report: Report = Boom("x").into();
assert_eq!(exit_code_of(&report), None);
}
#[test]
fn delegates_diagnostic_facets_transparently() {
let coded = Boom("x").with_exit_code(7);
assert_eq!(coded.to_string(), "boom: x");
assert_eq!(coded.code().map(|c| c.to_string()).as_deref(), Some("test::boom"));
assert_eq!(coded.help().map(|h| h.to_string()).as_deref(), Some("try again"));
assert_eq!(coded.exit_code(), 7);
}
#[test]
fn survives_question_mark_propagation() {
fn inner() -> Result<(), ExitCoded> {
Err(Boom("deep").with_exit_code(3))
}
fn outer() -> miette::Result<()> {
inner()?;
Ok(())
}
let report = outer().expect_err("inner fails");
assert_eq!(exit_code_of(&report), Some(3));
}
}