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