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
161 #[error(
164 "Unknown environment variable{} under {prefix}: {}",
165 if names.len() == 1 { "" } else { "s" },
166 names.join(", ")
167 )]
168 UnknownEnv { prefix: String, names: Vec<String> },
169}
170pub type Result<T> = std::result::Result<T, UsageErr>;
171
172impl UsageErr {
173 fn code_name(&self) -> Option<&'static str> {
174 match self {
175 Self::FileError(..) => Some("usage::file"),
176 Self::ShellError(..) => Some("usage::shell"),
177 _ => None,
178 }
179 }
180
181 pub(crate) fn render(&self) -> String {
182 let rendered = match self {
183 Self::InvalidInput(message, span, source) => crate::miette::render_source(
184 "Invalid usage config",
185 source.name(),
186 source.inner(),
187 *span,
188 message,
189 None,
190 ),
191 Self::InvalidFlag {
192 reason,
193 span,
194 input,
195 ..
196 } => crate::miette::render_source(&self.to_string(), "", input, *span, reason, None),
197 Self::KdlError(error) => error.render(),
198 _ => self.to_string(),
199 };
200 if let Some(code) = self.code_name() {
201 format!(" {code}\n\n{rendered}")
202 } else {
203 rendered
204 }
205 }
206}
207
208#[cfg(feature = "miette")]
209impl ::miette::Diagnostic for UsageErr {
210 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
211 self.code_name()
212 .map(|code| Box::new(code) as Box<dyn std::fmt::Display>)
213 }
214
215 fn source_code(&self) -> Option<&dyn ::miette::SourceCode> {
216 match self {
217 Self::InvalidInput(_, _, source) => Some(source),
218 Self::InvalidFlag { input, .. } => Some(input),
219 _ => None,
220 }
221 }
222
223 fn labels(&self) -> Option<Box<dyn Iterator<Item = ::miette::LabeledSpan> + '_>> {
224 let (span, label) = match self {
225 Self::InvalidInput(message, span, _) => (*span, message.as_str()),
226 Self::InvalidFlag { reason, span, .. } => (*span, reason.as_str()),
227 _ => return None,
228 };
229 let label = ::miette::LabeledSpan::at(span.offset()..span.offset() + span.len(), label);
230 Some(Box::new(std::iter::once(label)))
231 }
232
233 fn related<'a>(
234 &'a self,
235 ) -> Option<Box<dyn Iterator<Item = &'a dyn ::miette::Diagnostic> + 'a>> {
236 match self {
237 Self::KdlError(error) => Some(Box::new(
238 error
239 .diagnostics
240 .iter()
241 .map(|diagnostic| diagnostic as &dyn ::miette::Diagnostic),
242 )),
243 _ => None,
244 }
245 }
246}
247
248#[macro_export]
249macro_rules! bail_parse {
250 ($ctx:expr, $span:expr, $fmt:literal) => {{
251 let span: $crate::miette::SourceSpan = ($span.offset(), $span.len()).into();
252 let msg = format!($fmt);
253 let err = $ctx.build_err(msg, span);
254 return std::result::Result::Err(err);
255 }};
256 ($ctx:expr, $span:expr, $fmt:literal, $($arg:tt)*) => {{
257 let span: $crate::miette::SourceSpan = ($span.offset(), $span.len()).into();
258 let msg = format!($fmt, $($arg)*);
259 let err = $ctx.build_err(msg, span);
260 return std::result::Result::Err(err);
261 }};
262}
263
264#[cfg(test)]
265mod tests {
266 use super::UsageErr;
267
268 #[test]
269 fn native_renderer_preserves_diagnostic_codes() {
270 let file = UsageErr::FileError(
271 std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
272 "missing.kdl".into(),
273 );
274 assert!(file.render().contains("usage::file"));
275 assert!(
276 UsageErr::ShellError("failed".into())
277 .render()
278 .contains("usage::shell")
279 );
280 }
281
282 #[cfg(feature = "miette")]
283 #[test]
284 fn miette_interop_preserves_diagnostic_codes() {
285 use miette::Diagnostic;
286
287 let error = UsageErr::ShellError("failed".into());
288 assert_eq!(error.code().unwrap().to_string(), "usage::shell");
289 }
290}