Skip to main content

agent_first_data/cli_spec/
error.rs

1use serde::Serialize;
2
3/// Stable closed set of invocation-error rules.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
5#[serde(rename_all = "snake_case")]
6pub enum CliErrorRule {
7    InvalidUtf8,
8    UnknownCommand,
9    UnknownArgument,
10    MissingArgumentValue,
11    InvalidArgumentValue,
12    DuplicateArgument,
13    UnexpectedPositional,
14    UnregisteredCombination,
15}
16
17impl CliErrorRule {
18    pub const fn as_str(self) -> &'static str {
19        match self {
20            Self::InvalidUtf8 => "invalid_utf8",
21            Self::UnknownCommand => "unknown_command",
22            Self::UnknownArgument => "unknown_argument",
23            Self::MissingArgumentValue => "missing_argument_value",
24            Self::InvalidArgumentValue => "invalid_argument_value",
25            Self::DuplicateArgument => "duplicate_argument",
26            Self::UnexpectedPositional => "unexpected_positional",
27            Self::UnregisteredCombination => "unregistered_combination",
28        }
29    }
30
31    /// The `error.code` this failure is reported under.
32    ///
33    /// The classification belongs in the code, beside `document_type_mismatch`
34    /// and the rest — not in a second field next to a generic `cli_error`,
35    /// which asked an agent to learn a second place to look and cost a closed
36    /// enum kept in step across four files and four mirrored schemas.
37    pub const fn code(self) -> &'static str {
38        match self {
39            Self::InvalidUtf8 => "cli_invalid_utf8",
40            Self::UnknownCommand => "cli_unknown_command",
41            Self::UnknownArgument => "cli_unknown_argument",
42            Self::MissingArgumentValue => "cli_missing_argument_value",
43            Self::InvalidArgumentValue => "cli_invalid_argument_value",
44            Self::DuplicateArgument => "cli_duplicate_argument",
45            Self::UnexpectedPositional => "cli_unexpected_positional",
46            Self::UnregisteredCombination => "cli_unregistered_combination",
47        }
48    }
49}
50
51/// Structured CLI-resolution error. It never contains raw argument values.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct CliError {
54    pub rule: CliErrorRule,
55    pub command_path: String,
56    pub argument_names: Vec<String>,
57    pub message: String,
58    pub hint: String,
59}
60
61impl CliError {
62    pub(super) fn new(
63        rule: CliErrorRule,
64        command_path: String,
65        argument_names: Vec<String>,
66        message: impl Into<String>,
67    ) -> Self {
68        let hint = format!("run `{command_path} --help` and choose one registered combination");
69        Self {
70            rule,
71            command_path,
72            argument_names,
73            message: message.into(),
74            hint,
75        }
76    }
77
78    pub(super) fn unregistered(command_path: String, argument_names: Vec<String>) -> Self {
79        Self::new(
80            CliErrorRule::UnregisteredCombination,
81            command_path,
82            argument_names,
83            "arguments do not match a registered CLI combination",
84        )
85    }
86
87    /// Fixed process exit status for every closed-world CLI error.
88    ///
89    /// Derived rather than stored: every rule in [`CliErrorRule`] exits 2, and
90    /// a field would only be somewhere for that to drift.
91    pub const fn exit_code(&self) -> u8 {
92        2
93    }
94}
95
96impl std::fmt::Display for CliError {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str(&self.message)
99    }
100}
101
102impl std::error::Error for CliError {}
103
104pub(super) fn invalid_value(command_path: &str, name: String, message: &str) -> CliError {
105    CliError::new(
106        CliErrorRule::InvalidArgumentValue,
107        command_path.to_string(),
108        vec![name.clone()],
109        format!("invalid value for `{name}`: {message}"),
110    )
111}
112
113pub(super) fn missing_value(command_path: &str, name: &str) -> CliError {
114    CliError::new(
115        CliErrorRule::MissingArgumentValue,
116        command_path.to_string(),
117        vec![name.to_string()],
118        format!("argument `{name}` requires a value"),
119    )
120}
121
122pub(super) fn duplicate_error(command_path: &str, name: &str) -> CliError {
123    CliError::new(
124        CliErrorRule::DuplicateArgument,
125        command_path.to_string(),
126        vec![name.to_string()],
127        format!("argument `{name}` may not be repeated"),
128    )
129}