1use crate::kdl;
2use crate::miette::{NamedSource, SourceSpan};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
15#[non_exhaustive]
16pub enum UsageErr {
17 #[error("Invalid flag `{token}`: {reason}")]
18 InvalidFlag {
19 token: String,
20 reason: String,
21 span: SourceSpan,
22 input: String,
23 },
24
25 #[error("Missing required flag: --{0} <{0}>")]
26 MissingFlag(String),
27
28 #[error("Flag --{0} cannot be used multiple times")]
29 DuplicateFlag(String),
30
31 #[error("Missing one of the required flags in group {group}: {members}")]
37 MissingGroup { group: String, members: String },
38
39 #[error("Invalid usage config")]
40 InvalidInput(String, SourceSpan, NamedSource<String>),
41
42 #[error("Missing required arg: <{0}>")]
43 MissingArg(String),
44
45 #[error("`{0}` needs a subcommand: one of {1}")]
51 MissingSubcommand(String, String),
52
53 #[error("Argument <{0}> can only be set after a `--` separator")]
54 ArgRequiresDoubleDash(String),
55
56 #[error("{0}")]
57 Help(String),
58
59 #[error("{0}")]
60 Version(String),
61
62 #[error("Invalid usage config: {0}")]
63 Miette(#[from] crate::miette::MietteError),
64
65 #[error(transparent)]
66 IO(#[from] std::io::Error),
67
68 #[error(transparent)]
69 Strum(#[from] strum::ParseError),
70
71 #[error(transparent)]
72 FromUtf8Error(#[from] std::string::FromUtf8Error),
73
74 #[cfg(feature = "tera")]
75 #[error(transparent)]
76 TeraError(#[from] tera::Error),
77
78 #[error(transparent)]
79 KdlError(#[from] kdl::KdlError),
80
81 #[error("{0}\nFile: {1}")]
86 FileError(std::io::Error, std::path::PathBuf),
87
88 #[error("{0}")]
91 ShellError(String),
92
93 #[error("Variadic argument <{name}> requires at least {min} value(s), got {got}")]
94 VarArgTooFew {
95 name: String,
96 min: usize,
97 got: usize,
98 },
99
100 #[error("Variadic argument <{name}> accepts at most {max} value(s), got {got}")]
101 VarArgTooMany {
102 name: String,
103 max: usize,
104 got: usize,
105 },
106
107 #[error("Variadic flag --{name} requires at least {min} value(s), got {got}")]
108 VarFlagTooFew {
109 name: String,
110 min: usize,
111 got: usize,
112 },
113
114 #[error("Variadic flag --{name} accepts at most {max} value(s), got {got}")]
115 VarFlagTooMany {
116 name: String,
117 max: usize,
118 got: usize,
119 },
120
121 #[error("Invalid file path: {0}")]
122 InvalidPath(String),
123
124 #[error("Invalid spec view: {0}")]
125 InvalidView(String),
126
127 #[error("Invalid output declaration: {0}")]
132 InvalidOutput(String),
133
134 #[error("Invalid value for {name}: {value}: {reason}")]
135 InvalidValue {
136 name: String,
137 value: String,
138 reason: String,
139 },
140
141 #[error("Unsupported shell: {0}")]
142 UnsupportedShell(String),
143
144 #[error("No injected output was provided for mount command: {0}")]
145 MissingMountOutput(String),
146}
147pub type Result<T> = std::result::Result<T, UsageErr>;
148
149impl UsageErr {
150 fn code_name(&self) -> Option<&'static str> {
151 match self {
152 Self::FileError(..) => Some("usage::file"),
153 Self::ShellError(..) => Some("usage::shell"),
154 _ => None,
155 }
156 }
157
158 pub(crate) fn render(&self) -> String {
159 let rendered = match self {
160 Self::InvalidInput(message, span, source) => crate::miette::render_source(
161 "Invalid usage config",
162 source.name(),
163 source.inner(),
164 *span,
165 message,
166 None,
167 ),
168 Self::InvalidFlag {
169 reason,
170 span,
171 input,
172 ..
173 } => crate::miette::render_source(&self.to_string(), "", input, *span, reason, None),
174 Self::KdlError(error) => error.render(),
175 _ => self.to_string(),
176 };
177 if let Some(code) = self.code_name() {
178 format!(" {code}\n\n{rendered}")
179 } else {
180 rendered
181 }
182 }
183}
184
185#[cfg(feature = "miette")]
186impl ::miette::Diagnostic for UsageErr {
187 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
188 self.code_name()
189 .map(|code| Box::new(code) as Box<dyn std::fmt::Display>)
190 }
191
192 fn source_code(&self) -> Option<&dyn ::miette::SourceCode> {
193 match self {
194 Self::InvalidInput(_, _, source) => Some(source),
195 Self::InvalidFlag { input, .. } => Some(input),
196 _ => None,
197 }
198 }
199
200 fn labels(&self) -> Option<Box<dyn Iterator<Item = ::miette::LabeledSpan> + '_>> {
201 let (span, label) = match self {
202 Self::InvalidInput(message, span, _) => (*span, message.as_str()),
203 Self::InvalidFlag { reason, span, .. } => (*span, reason.as_str()),
204 _ => return None,
205 };
206 let label = ::miette::LabeledSpan::at(span.offset()..span.offset() + span.len(), label);
207 Some(Box::new(std::iter::once(label)))
208 }
209
210 fn related<'a>(
211 &'a self,
212 ) -> Option<Box<dyn Iterator<Item = &'a dyn ::miette::Diagnostic> + 'a>> {
213 match self {
214 Self::KdlError(error) => Some(Box::new(
215 error
216 .diagnostics
217 .iter()
218 .map(|diagnostic| diagnostic as &dyn ::miette::Diagnostic),
219 )),
220 _ => None,
221 }
222 }
223}
224
225#[macro_export]
226macro_rules! bail_parse {
227 ($ctx:expr, $span:expr, $fmt:literal) => {{
228 let span: $crate::miette::SourceSpan = ($span.offset(), $span.len()).into();
229 let msg = format!($fmt);
230 let err = $ctx.build_err(msg, span);
231 return std::result::Result::Err(err);
232 }};
233 ($ctx:expr, $span:expr, $fmt:literal, $($arg:tt)*) => {{
234 let span: $crate::miette::SourceSpan = ($span.offset(), $span.len()).into();
235 let msg = format!($fmt, $($arg)*);
236 let err = $ctx.build_err(msg, span);
237 return std::result::Result::Err(err);
238 }};
239}
240
241#[cfg(test)]
242mod tests {
243 use super::UsageErr;
244
245 #[test]
246 fn native_renderer_preserves_diagnostic_codes() {
247 let file = UsageErr::FileError(
248 std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
249 "missing.kdl".into(),
250 );
251 assert!(file.render().contains("usage::file"));
252 assert!(UsageErr::ShellError("failed".into())
253 .render()
254 .contains("usage::shell"));
255 }
256
257 #[cfg(feature = "miette")]
258 #[test]
259 fn miette_interop_preserves_diagnostic_codes() {
260 use miette::Diagnostic;
261
262 let error = UsageErr::ShellError("failed".into());
263 assert_eq!(error.code().unwrap().to_string(), "usage::shell");
264 }
265}