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