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