Skip to main content

usage/
error.rs

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/// Everything that can go wrong reading a spec or a command line against one.
10///
11/// `#[non_exhaustive]`, so a caller matching on it needs a `_` arm. That is the point:
12/// this enum grows every time the spec learns to say something new — `MissingGroup`
13/// arrived with groups, `ArgRequiresDoubleDash` with `double_dash` — and without this
14/// each one is a major release for everyone downstream.
15///
16/// With the `miette` feature enabled, this implements `miette::Diagnostic` so applications can
17/// pass it directly to their existing miette reporter. The feature is disabled by default.
18#[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    /// A required group had none of its members given.
36    ///
37    /// Its own variant rather than a [`UsageErr::MissingFlag`] holding a sentence,
38    /// because there is no one flag to name: the group is the thing that was not
39    /// satisfied, and a caller that renders errors itself needs the members as members.
40    #[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    /// A command that declares `subcommand_required` was given none.
57    ///
58    /// The spec could say this and the parser did not read it, so `mise generate` — which
59    /// declares it — parsed as though it were a complete invocation. usage-argv and clap both
60    /// refuse it.
61    #[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    /// A file the spec model was asked to read could not be read.
93    ///
94    /// Carries the path as well as the io error: "No such file or directory" on its own
95    /// names nothing, and this is reported for spec files given on a command line.
96    #[error("{0}\nFile: {1}")]
97    FileError(std::io::Error, std::path::PathBuf),
98
99    /// A `run=` script could not be run, exited non-zero, or produced output usage
100    /// could not read. The message names the shell and the script.
101    #[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    /// A command's `output`/`select` declarations do not agree with each other, or with
139    /// the flags around them. Spanless like [`UsageErr::InvalidView`], because selection
140    /// is resolved once the whole document is read — a `select` may name a flag declared
141    /// on an ancestor, so the node spans are long gone by the time it can be checked.
142    #[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}