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///
53/// `rule` is the classification and the only thing worth branching on; the two
54/// strings are for a human. The command path and the implicated argument names
55/// are not fields: both are already inside `hint` and `message` respectively,
56/// and a second, stringly-typed copy of either is a place for them to drift.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct CliError {
59    pub rule: CliErrorRule,
60    pub message: String,
61    pub hint: String,
62}
63
64impl CliError {
65    pub(super) fn new(
66        rule: CliErrorRule,
67        command_path: String,
68        message: impl Into<String>,
69    ) -> Self {
70        let hint = format!("run `{command_path} --help` and choose one registered combination");
71        Self {
72            rule,
73            message: message.into(),
74            hint,
75        }
76    }
77
78    pub(super) fn unregistered(command_path: String) -> Self {
79        Self::new(
80            CliErrorRule::UnregisteredCombination,
81            command_path,
82            "arguments do not match a registered CLI combination",
83        )
84    }
85
86    /// Fixed process exit status for every closed-world CLI error.
87    ///
88    /// Derived rather than stored: every rule in [`CliErrorRule`] exits 2, and
89    /// a field would only be somewhere for that to drift.
90    pub const fn exit_code(&self) -> u8 {
91        2
92    }
93}
94
95impl std::fmt::Display for CliError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.write_str(&self.message)
98    }
99}
100
101impl std::error::Error for CliError {}
102
103pub(super) fn invalid_value(command_path: &str, name: String, message: &str) -> CliError {
104    CliError::new(
105        CliErrorRule::InvalidArgumentValue,
106        command_path.to_string(),
107        format!("invalid value for `{name}`: {message}"),
108    )
109}
110
111pub(super) fn missing_value(command_path: &str, name: &str) -> CliError {
112    CliError::new(
113        CliErrorRule::MissingArgumentValue,
114        command_path.to_string(),
115        format!("argument `{name}` requires a value"),
116    )
117}
118
119pub(super) fn duplicate_error(command_path: &str, name: &str) -> CliError {
120    CliError::new(
121        CliErrorRule::DuplicateArgument,
122        command_path.to_string(),
123        format!("argument `{name}` may not be repeated"),
124    )
125}