1use std::fmt;
13
14use miette::{Diagnostic, Report};
15
16pub struct ExitCoded {
23 code: u8,
24 source: Box<dyn Diagnostic + Send + Sync + 'static>,
25}
26
27impl ExitCoded {
28 pub fn new(code: u8, source: impl Diagnostic + Send + Sync + 'static) -> Self {
30 Self { code, source: Box::new(source) }
31 }
32
33 #[must_use]
35 pub const fn exit_code(&self) -> u8 {
36 self.code
37 }
38}
39
40impl fmt::Debug for ExitCoded {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 fmt::Debug::fmt(&self.source, f)
43 }
44}
45
46impl fmt::Display for ExitCoded {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 fmt::Display::fmt(&self.source, f)
49 }
50}
51
52impl std::error::Error for ExitCoded {
53 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
54 self.source.source()
55 }
56}
57
58impl Diagnostic for ExitCoded {
59 fn code(&self) -> Option<Box<dyn fmt::Display + '_>> {
60 self.source.code()
61 }
62
63 fn severity(&self) -> Option<miette::Severity> {
64 self.source.severity()
65 }
66
67 fn help(&self) -> Option<Box<dyn fmt::Display + '_>> {
68 self.source.help()
69 }
70
71 fn url(&self) -> Option<Box<dyn fmt::Display + '_>> {
72 self.source.url()
73 }
74
75 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
76 self.source.source_code()
77 }
78
79 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
80 self.source.labels()
81 }
82
83 fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
84 self.source.related()
85 }
86
87 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
88 self.source.diagnostic_source()
89 }
90}
91
92pub trait WithExitCode: Sized {
94 fn with_exit_code(self, code: u8) -> ExitCoded;
96}
97
98impl<E> WithExitCode for E
99where
100 E: Diagnostic + Send + Sync + 'static,
101{
102 fn with_exit_code(self, code: u8) -> ExitCoded {
103 ExitCoded::new(code, self)
104 }
105}
106
107#[must_use]
112pub fn exit_code_of(report: &Report) -> Option<u8> {
113 report.downcast_ref::<ExitCoded>().map(ExitCoded::exit_code)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::{exit_code_of, ExitCoded, WithExitCode};
119 use miette::{Diagnostic, Report};
120 use thiserror::Error;
121
122 #[derive(Debug, Error, Diagnostic)]
123 #[error("boom: {0}")]
124 #[diagnostic(code(test::boom), help("try again"))]
125 struct Boom(&'static str);
126
127 #[test]
128 fn attaches_and_reads_back_the_code() {
129 let report: Report = Boom("x").with_exit_code(2).into();
130 assert_eq!(exit_code_of(&report), Some(2));
131 }
132
133 #[test]
134 fn plain_error_has_no_attached_code() {
135 let report: Report = Boom("x").into();
136 assert_eq!(exit_code_of(&report), None);
137 }
138
139 #[test]
140 fn delegates_diagnostic_facets_transparently() {
141 let coded = Boom("x").with_exit_code(7);
142 assert_eq!(coded.to_string(), "boom: x");
143 assert_eq!(coded.code().map(|c| c.to_string()).as_deref(), Some("test::boom"));
144 assert_eq!(coded.help().map(|h| h.to_string()).as_deref(), Some("try again"));
145 assert_eq!(coded.exit_code(), 7);
146 }
147
148 #[test]
149 fn survives_question_mark_propagation() {
150 fn inner() -> Result<(), ExitCoded> {
151 Err(Boom("deep").with_exit_code(3))
152 }
153 fn outer() -> miette::Result<()> {
154 inner()?;
155 Ok(())
156 }
157 let report = outer().expect_err("inner fails");
158 assert_eq!(exit_code_of(&report), Some(3));
159 }
160}