Skip to main content

standout_dispatch/
verify.rs

1use clap::{ArgAction, Command};
2use std::fmt;
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum ArgKind {
5    Flag,
6    RequiredArg,
7    OptionalArg,
8    VecArg,
9}
10impl fmt::Display for ArgKind {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        match self {
13            ArgKind::Flag => write!(f, "boolean flag"),
14            ArgKind::RequiredArg => write!(f, "required argument"),
15            ArgKind::OptionalArg => write!(f, "optional argument"),
16            ArgKind::VecArg => write!(f, "repeatable argument"),
17        }
18    }
19}
20#[derive(Debug, Clone)]
21pub struct ExpectedArg {
22    pub cli_name: String,
23    pub rust_name: String,
24    pub kind: ArgKind,
25}
26impl ExpectedArg {
27    pub fn flag(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
28        Self {
29            cli_name: cli_name.into(),
30            rust_name: rust_name.into(),
31            kind: ArgKind::Flag,
32        }
33    }
34    pub fn required_arg(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
35        Self {
36            cli_name: cli_name.into(),
37            rust_name: rust_name.into(),
38            kind: ArgKind::RequiredArg,
39        }
40    }
41    pub fn optional_arg(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
42        Self {
43            cli_name: cli_name.into(),
44            rust_name: rust_name.into(),
45            kind: ArgKind::OptionalArg,
46        }
47    }
48    pub fn vec_arg(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
49        Self {
50            cli_name: cli_name.into(),
51            rust_name: rust_name.into(),
52            kind: ArgKind::VecArg,
53        }
54    }
55}
56#[derive(Debug, Clone)]
57pub enum ArgMismatch {
58    MissingInCommand {
59        cli_name: String,
60        rust_name: String,
61        expected_kind: ArgKind,
62    },
63    NotAFlag {
64        cli_name: String,
65        actual_action: String,
66    },
67    UnexpectedFlag {
68        cli_name: String,
69        expected_kind: ArgKind,
70    },
71    RequiredMismatch {
72        cli_name: String,
73        handler_required: bool,
74        command_required: bool,
75    },
76}
77impl fmt::Display for ArgMismatch {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            ArgMismatch::MissingInCommand {
81                cli_name,
82                rust_name,
83                expected_kind,
84            } => {
85                writeln!(f, "  Argument `{cli_name}` (parameter `{rust_name}`):")?;
86                writeln!(f, "    - Handler expects: {expected_kind}")?;
87                writeln!(f, "    - Command: argument not defined")?;
88                writeln!(f)?;
89                writeln!(f, "    Fix: Add the argument to your clap Command:")?;
90                match expected_kind {
91                    ArgKind::Flag => {
92                        writeln!(
93                            f,
94                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").action(ArgAction::SetTrue))"
95                        )
96                    }
97                    ArgKind::RequiredArg => {
98                        writeln!(
99                            f,
100                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").required(true))"
101                        )
102                    }
103                    ArgKind::OptionalArg => {
104                        writeln!(
105                            f,
106                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\"))"
107                        )
108                    }
109                    ArgKind::VecArg => {
110                        writeln!(
111                            f,
112                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").action(ArgAction::Append))"
113                        )
114                    }
115                }
116            }
117            ArgMismatch::NotAFlag {
118                cli_name,
119                actual_action,
120            } => {
121                writeln!(f, "  Flag `{cli_name}`:")?;
122                writeln!(f, "    - Handler expects: boolean flag (via get_flag)")?;
123                writeln!(f, "    - Command defines: {actual_action}")?;
124                writeln!(f)?;
125                writeln!(f, "    Fix: Change the argument's action to SetTrue:")?;
126                writeln!(
127                    f,
128                    "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").action(ArgAction::SetTrue))"
129                )
130            }
131            ArgMismatch::UnexpectedFlag {
132                cli_name,
133                expected_kind,
134            } => {
135                writeln!(f, "  Argument `{cli_name}`:")?;
136                writeln!(f, "    - Handler expects: {expected_kind}")?;
137                writeln!(f, "    - Command defines: boolean flag (SetTrue/SetFalse)")?;
138                writeln!(f)?;
139                writeln!(f, "    Fix: Either:")?;
140                writeln!(
141                    f,
142                    "      - Change the handler parameter to `#[flag] {cli_name}: bool`"
143                )?;
144                writeln!(
145                    f,
146                    "      - Or change the command's action: .action(ArgAction::Set)"
147                )
148            }
149            ArgMismatch::RequiredMismatch {
150                cli_name,
151                handler_required,
152                command_required: _,
153            } => {
154                writeln!(f, "  Argument `{cli_name}`:")?;
155                if *handler_required {
156                    writeln!(f, "    - Handler expects: required argument")?;
157                    writeln!(f, "    - Command defines: optional argument")?;
158                    writeln!(f)?;
159                    writeln!(f, "    Fix: Either:")?;
160                    writeln!(
161                        f,
162                        "      - Change handler to `#[arg] {}: Option<T>`",
163                        cli_name.replace('-', "_")
164                    )?;
165                    writeln!(f, "      - Or add `.required(true)` to the command arg")
166                } else {
167                    writeln!(f, "    - Handler expects: optional argument (Option<T>)")?;
168                    writeln!(f, "    - Command defines: required argument")?;
169                    writeln!(f)?;
170                    writeln!(f, "    Fix: Either:")?;
171                    writeln!(
172                        f,
173                        "      - Change handler to `#[arg] {}: T` (not Option)",
174                        cli_name.replace('-', "_")
175                    )?;
176                    writeln!(
177                        f,
178                        "      - Or remove `.required(true)` from the command arg"
179                    )
180                }
181            }
182        }
183    }
184}
185#[derive(Debug, Clone)]
186pub struct HandlerMismatchError {
187    pub handler_name: String,
188    pub command_name: Option<String>,
189    pub mismatches: Vec<ArgMismatch>,
190}
191impl std::error::Error for HandlerMismatchError {}
192impl fmt::Display for HandlerMismatchError {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        let cmd_desc = self
195            .command_name
196            .as_ref()
197            .map(|n| format!(" for command `{n}`"))
198            .unwrap_or_default();
199        writeln!(
200            f,
201            "Handler `{}` is incompatible with clap Command{cmd_desc}",
202            self.handler_name
203        )?;
204        writeln!(f)?;
205        for mismatch in &self.mismatches {
206            write!(f, "{mismatch}")?;
207        }
208        Ok(())
209    }
210}
211fn is_flag_action(action: &ArgAction) -> bool {
212    matches!(action, ArgAction::SetTrue | ArgAction::SetFalse)
213}
214fn describe_action(action: &ArgAction) -> String {
215    match action {
216        ArgAction::Set => "ArgAction::Set (single value)".to_string(),
217        ArgAction::Append => "ArgAction::Append (multiple values)".to_string(),
218        ArgAction::SetTrue => "ArgAction::SetTrue (boolean flag)".to_string(),
219        ArgAction::SetFalse => "ArgAction::SetFalse (boolean flag)".to_string(),
220        ArgAction::Count => "ArgAction::Count (counter)".to_string(),
221        ArgAction::Help => "ArgAction::Help".to_string(),
222        ArgAction::HelpShort => "ArgAction::HelpShort".to_string(),
223        ArgAction::HelpLong => "ArgAction::HelpLong".to_string(),
224        ArgAction::Version => "ArgAction::Version".to_string(),
225        _ => "unknown action".to_string(),
226    }
227}
228pub fn verify_handler_args(
229    command: &Command,
230    handler_name: &str,
231    expected: &[ExpectedArg],
232) -> Result<(), HandlerMismatchError> {
233    let mut mismatches = Vec::new();
234    for exp in expected {
235        let arg = command
236            .get_arguments()
237            .find(|a| a.get_id() == exp.cli_name.as_str());
238        match arg {
239            None => {
240                mismatches.push(ArgMismatch::MissingInCommand {
241                    cli_name: exp.cli_name.clone(),
242                    rust_name: exp.rust_name.clone(),
243                    expected_kind: exp.kind.clone(),
244                });
245            }
246            Some(arg) => {
247                let action = arg.get_action();
248                match exp.kind {
249                    ArgKind::Flag => {
250                        if !is_flag_action(action) {
251                            mismatches.push(ArgMismatch::NotAFlag {
252                                cli_name: exp.cli_name.clone(),
253                                actual_action: describe_action(action),
254                            });
255                        }
256                    }
257                    ArgKind::RequiredArg => {
258                        if is_flag_action(action) {
259                            mismatches.push(ArgMismatch::UnexpectedFlag {
260                                cli_name: exp.cli_name.clone(),
261                                expected_kind: exp.kind.clone(),
262                            });
263                        } else if matches!(action, ArgAction::Count) {
264                        } else if !arg.is_required_set() && arg.get_default_values().is_empty() {
265                            mismatches.push(ArgMismatch::RequiredMismatch {
266                                cli_name: exp.cli_name.clone(),
267                                handler_required: true,
268                                command_required: false,
269                            });
270                        }
271                    }
272                    ArgKind::OptionalArg => {
273                        if is_flag_action(action) {
274                            mismatches.push(ArgMismatch::UnexpectedFlag {
275                                cli_name: exp.cli_name.clone(),
276                                expected_kind: exp.kind.clone(),
277                            });
278                        } else if arg.is_required_set() {
279                            mismatches.push(ArgMismatch::RequiredMismatch {
280                                cli_name: exp.cli_name.clone(),
281                                handler_required: false,
282                                command_required: true,
283                            });
284                        }
285                    }
286                    ArgKind::VecArg => {
287                        if is_flag_action(action) {
288                            mismatches.push(ArgMismatch::UnexpectedFlag {
289                                cli_name: exp.cli_name.clone(),
290                                expected_kind: exp.kind.clone(),
291                            });
292                        }
293                    }
294                }
295            }
296        }
297    }
298    if mismatches.is_empty() {
299        Ok(())
300    } else {
301        Err(HandlerMismatchError {
302            handler_name: handler_name.to_string(),
303            command_name: Some(command.get_name().to_string()),
304            mismatches,
305        })
306    }
307}
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use clap::Arg;
312    #[test]
313    fn test_verify_matching_flag() {
314        let command = Command::new("test").arg(
315            Arg::new("verbose")
316                .long("verbose")
317                .action(ArgAction::SetTrue),
318        );
319        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
320        assert!(verify_handler_args(&command, "test_handler", &expected).is_ok());
321    }
322    #[test]
323    fn test_verify_missing_arg() {
324        let command = Command::new("test");
325        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
326        let err = verify_handler_args(&command, "test_handler", &expected).unwrap_err();
327        assert_eq!(err.mismatches.len(), 1);
328        assert!(matches!(
329            &err.mismatches[0],
330            ArgMismatch::MissingInCommand { cli_name, .. } if cli_name == "verbose"
331        ));
332    }
333    #[test]
334    fn test_verify_wrong_action_for_flag() {
335        let command =
336            Command::new("test").arg(Arg::new("verbose").long("verbose").action(ArgAction::Set));
337        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
338        let err = verify_handler_args(&command, "test_handler", &expected).unwrap_err();
339        assert_eq!(err.mismatches.len(), 1);
340        assert!(matches!(&err.mismatches[0], ArgMismatch::NotAFlag { .. }));
341    }
342    #[test]
343    fn test_verify_required_mismatch() {
344        let command =
345            Command::new("test").arg(Arg::new("name").long("name").action(ArgAction::Set));
346        let expected = vec![ExpectedArg::required_arg("name", "name")];
347        let err = verify_handler_args(&command, "test_handler", &expected).unwrap_err();
348        assert_eq!(err.mismatches.len(), 1);
349        assert!(matches!(
350            &err.mismatches[0],
351            ArgMismatch::RequiredMismatch {
352                handler_required: true,
353                command_required: false,
354                ..
355            }
356        ));
357    }
358    #[test]
359    fn test_verify_optional_matches() {
360        let command =
361            Command::new("test").arg(Arg::new("filter").long("filter").action(ArgAction::Set));
362        let expected = vec![ExpectedArg::optional_arg("filter", "filter")];
363        assert!(verify_handler_args(&command, "test_handler", &expected).is_ok());
364    }
365    #[test]
366    fn test_error_message_formatting() {
367        let command =
368            Command::new("list").arg(Arg::new("verbose").long("verbose").action(ArgAction::Set));
369        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
370        let err = verify_handler_args(&command, "list_handler", &expected).unwrap_err();
371        let msg = err.to_string();
372        assert!(msg.contains("Handler `list_handler`"));
373        assert!(msg.contains("command `list`"));
374        assert!(msg.contains("Flag `verbose`"));
375        assert!(msg.contains("ArgAction::SetTrue"));
376    }
377}