Skip to main content

usage/
error.rs

1use crate::kdl;
2use crate::miette::{NamedSource, SourceSpan};
3use thiserror::Error;
4
5/// Everything that can go wrong reading a spec or a command line against one.
6///
7/// `#[non_exhaustive]`, so a caller matching on it needs a `_` arm. That is the point:
8/// this enum grows every time the spec learns to say something new — `MissingGroup`
9/// arrived with groups, `ArgRequiresDoubleDash` with `double_dash` — and without this
10/// each one is a major release for everyone downstream.
11///
12/// With the `miette` feature enabled, this implements `miette::Diagnostic` so applications can
13/// pass it directly to their existing miette reporter. The feature is disabled by default.
14#[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    /// A required group had none of its members given.
32    ///
33    /// Its own variant rather than a [`UsageErr::MissingFlag`] holding a sentence,
34    /// because there is no one flag to name: the group is the thing that was not
35    /// satisfied, and a caller that renders errors itself needs the members as members.
36    #[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    /// A command that declares `subcommand_required` was given none.
46    ///
47    /// The spec could say this and the parser did not read it, so `mise generate` — which
48    /// declares it — parsed as though it were a complete invocation. usage-argv and clap both
49    /// refuse it.
50    #[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    /// A file the spec model was asked to read could not be read.
82    ///
83    /// Carries the path as well as the io error: "No such file or directory" on its own
84    /// names nothing, and this is reported for spec files given on a command line.
85    #[error("{0}\nFile: {1}")]
86    FileError(std::io::Error, std::path::PathBuf),
87
88    /// A `run=` script could not be run, exited non-zero, or produced output usage
89    /// could not read. The message names the shell and the script.
90    #[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    /// A command's `output`/`select` declarations do not agree with each other, or with
128    /// the flags around them. Spanless like [`UsageErr::InvalidView`], because selection
129    /// is resolved once the whole document is read — a `select` may name a flag declared
130    /// on an ancestor, so the node spans are long gone by the time it can be checked.
131    #[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}