Skip to main content

runi_cli/launcher/
error.rs

1use std::fmt;
2
3/// Errors produced by the launcher when parsing command-line arguments.
4#[derive(Debug)]
5pub enum Error {
6    UnknownOption(String),
7    UnknownSubcommand {
8        name: String,
9        available: Vec<String>,
10    },
11    MissingValue(String),
12    UnexpectedValue(String),
13    /// A required option was not provided at all (distinct from
14    /// `MissingValue`, which fires when the option name was typed without a
15    /// following value).
16    MissingOption(String),
17    MissingArgument(String),
18    MissingSubcommand {
19        available: Vec<String>,
20    },
21    ExtraArgument(String),
22    InvalidValue {
23        name: String,
24        value: String,
25        message: String,
26    },
27    /// Sentinel used internally when the user passes `-h`/`--help`.
28    HelpRequested,
29    /// Wraps an error produced while parsing a subcommand so the launcher can
30    /// print the right (sub)command help. `path` is the chain from the root
31    /// schema to the failing subcommand, newest-last.
32    InSubcommand {
33        path: Vec<String>,
34        source: Box<Error>,
35    },
36    /// Wraps an error returned by a subcommand's `run` method. The launcher
37    /// uses this to distinguish user runtime failures from parse-origin
38    /// errors. It matters because `SubCommandOf::run` may legitimately
39    /// reuse parse-origin variants (e.g. `MissingArgument`) for its own
40    /// post-parse validation, so variant-based classification alone is
41    /// not enough.
42    Runtime(Box<Error>),
43    Custom(String),
44}
45
46impl Error {
47    pub fn invalid_value(
48        name: impl Into<String>,
49        value: impl Into<String>,
50        message: impl Into<String>,
51    ) -> Self {
52        Self::InvalidValue {
53            name: name.into(),
54            value: value.into(),
55            message: message.into(),
56        }
57    }
58
59    pub fn custom(message: impl Into<String>) -> Self {
60        Self::Custom(message.into())
61    }
62
63    /// Whether this error originated from argument parsing (as opposed to a
64    /// user command's runtime logic). The launcher uses this to decide
65    /// whether to print help text alongside the error message.
66    pub fn is_parse_error(&self) -> bool {
67        matches!(
68            self,
69            Error::UnknownOption(_)
70                | Error::UnknownSubcommand { .. }
71                | Error::MissingValue(_)
72                | Error::UnexpectedValue(_)
73                | Error::MissingOption(_)
74                | Error::MissingArgument(_)
75                | Error::MissingSubcommand { .. }
76                | Error::ExtraArgument(_)
77                | Error::InvalidValue { .. }
78                | Error::HelpRequested
79                | Error::InSubcommand { .. }
80        )
81    }
82}
83
84impl fmt::Display for Error {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Error::UnknownOption(opt) => write!(f, "unknown option: {opt}"),
88            Error::UnknownSubcommand { name, available } => {
89                write!(f, "unknown subcommand: {name}")?;
90                if !available.is_empty() {
91                    write!(f, " (available: {})", available.join(", "))?;
92                }
93                Ok(())
94            }
95            Error::MissingValue(opt) => write!(f, "missing value for option: {opt}"),
96            Error::UnexpectedValue(opt) => write!(f, "option {opt} does not take a value"),
97            Error::MissingOption(name) => write!(f, "missing required option: {name}"),
98            Error::MissingArgument(name) => write!(f, "missing required argument: <{name}>"),
99            Error::MissingSubcommand { available } => {
100                write!(f, "a subcommand is required")?;
101                if !available.is_empty() {
102                    write!(f, " (available: {})", available.join(", "))?;
103                }
104                Ok(())
105            }
106            Error::ExtraArgument(arg) => write!(f, "unexpected argument: {arg}"),
107            Error::InvalidValue {
108                name,
109                value,
110                message,
111            } => {
112                write!(f, "invalid value '{value}' for {name}: {message}")
113            }
114            Error::HelpRequested => write!(f, "help requested"),
115            Error::InSubcommand { path, source } => {
116                write!(f, "in subcommand '{}': {}", path.join(" "), source)
117            }
118            Error::Runtime(inner) => write!(f, "{inner}"),
119            Error::Custom(msg) => write!(f, "{msg}"),
120        }
121    }
122}
123
124impl std::error::Error for Error {}
125
126pub type Result<T> = std::result::Result<T, Error>;