Skip to main content

standout_dispatch/
verify.rs

1//! Handler-Command verification for detecting mismatches between
2//! `#[handler]` function signatures and clap `Command` definitions.
3//!
4//! This module provides types and functions to verify that a handler's
5//! expected arguments match what the clap Command actually defines,
6//! producing clear error messages when they don't.
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! use standout_dispatch::verify::{ExpectedArg, verify_handler_args};
12//!
13//! // Handler expects these args (generated by #[handler] macro)
14//! let expected = vec![
15//!     ExpectedArg::flag("verbose", "verbose"),
16//!     ExpectedArg::optional_arg("filter", "filter"),
17//! ];
18//!
19//! // Verify against the clap Command
20//! verify_handler_args(&command, "my_handler", &expected)?;
21//! ```
22
23use clap::{ArgAction, Command};
24use std::fmt;
25
26/// Describes what kind of argument a handler expects.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ArgKind {
29    /// A boolean flag (`#[flag]`), extracted via `get_flag()`
30    Flag,
31    /// A required argument (`#[arg] name: String`)
32    RequiredArg,
33    /// An optional argument (`#[arg] name: Option<String>`)
34    OptionalArg,
35    /// A repeatable argument (`#[arg] names: Vec<String>`)
36    VecArg,
37}
38
39impl fmt::Display for ArgKind {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            ArgKind::Flag => write!(f, "boolean flag"),
43            ArgKind::RequiredArg => write!(f, "required argument"),
44            ArgKind::OptionalArg => write!(f, "optional argument"),
45            ArgKind::VecArg => write!(f, "repeatable argument"),
46        }
47    }
48}
49
50/// What a handler expects for a single parameter.
51///
52/// Generated by the `#[handler]` macro via `handler_name__expected_args()`.
53#[derive(Debug, Clone)]
54pub struct ExpectedArg {
55    /// The CLI argument name (e.g., "in-progress")
56    pub cli_name: String,
57    /// The Rust parameter name (e.g., "in_progress")
58    pub rust_name: String,
59    /// What kind of argument this is
60    pub kind: ArgKind,
61}
62
63impl ExpectedArg {
64    /// Create an expected flag.
65    pub fn flag(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
66        Self {
67            cli_name: cli_name.into(),
68            rust_name: rust_name.into(),
69            kind: ArgKind::Flag,
70        }
71    }
72
73    /// Create an expected required argument.
74    pub fn required_arg(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
75        Self {
76            cli_name: cli_name.into(),
77            rust_name: rust_name.into(),
78            kind: ArgKind::RequiredArg,
79        }
80    }
81
82    /// Create an expected optional argument.
83    pub fn optional_arg(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
84        Self {
85            cli_name: cli_name.into(),
86            rust_name: rust_name.into(),
87            kind: ArgKind::OptionalArg,
88        }
89    }
90
91    /// Create an expected vec argument.
92    pub fn vec_arg(cli_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
93        Self {
94            cli_name: cli_name.into(),
95            rust_name: rust_name.into(),
96            kind: ArgKind::VecArg,
97        }
98    }
99}
100
101/// A single mismatch between handler expectation and command definition.
102#[derive(Debug, Clone)]
103pub enum ArgMismatch {
104    /// Handler expects an argument that doesn't exist in the command.
105    MissingInCommand {
106        cli_name: String,
107        rust_name: String,
108        expected_kind: ArgKind,
109    },
110    /// Handler expects a flag but command has a non-flag argument.
111    NotAFlag {
112        cli_name: String,
113        actual_action: String,
114    },
115    /// Handler expects a non-flag but command has a flag.
116    UnexpectedFlag {
117        cli_name: String,
118        expected_kind: ArgKind,
119    },
120    /// Handler expects required but command has optional (or vice versa).
121    RequiredMismatch {
122        cli_name: String,
123        handler_required: bool,
124        command_required: bool,
125    },
126}
127
128impl fmt::Display for ArgMismatch {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            ArgMismatch::MissingInCommand {
132                cli_name,
133                rust_name,
134                expected_kind,
135            } => {
136                writeln!(f, "  Argument `{cli_name}` (parameter `{rust_name}`):")?;
137                writeln!(f, "    - Handler expects: {expected_kind}")?;
138                writeln!(f, "    - Command: argument not defined")?;
139                writeln!(f)?;
140                writeln!(f, "    Fix: Add the argument to your clap Command:")?;
141                match expected_kind {
142                    ArgKind::Flag => {
143                        writeln!(
144                            f,
145                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").action(ArgAction::SetTrue))"
146                        )
147                    }
148                    ArgKind::RequiredArg => {
149                        writeln!(
150                            f,
151                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").required(true))"
152                        )
153                    }
154                    ArgKind::OptionalArg => {
155                        writeln!(
156                            f,
157                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\"))"
158                        )
159                    }
160                    ArgKind::VecArg => {
161                        writeln!(
162                            f,
163                            "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").action(ArgAction::Append))"
164                        )
165                    }
166                }
167            }
168            ArgMismatch::NotAFlag {
169                cli_name,
170                actual_action,
171            } => {
172                writeln!(f, "  Flag `{cli_name}`:")?;
173                writeln!(f, "    - Handler expects: boolean flag (via get_flag)")?;
174                writeln!(f, "    - Command defines: {actual_action}")?;
175                writeln!(f)?;
176                writeln!(f, "    Fix: Change the argument's action to SetTrue:")?;
177                writeln!(
178                    f,
179                    "      .arg(Arg::new(\"{cli_name}\").long(\"{cli_name}\").action(ArgAction::SetTrue))"
180                )
181            }
182            ArgMismatch::UnexpectedFlag {
183                cli_name,
184                expected_kind,
185            } => {
186                writeln!(f, "  Argument `{cli_name}`:")?;
187                writeln!(f, "    - Handler expects: {expected_kind}")?;
188                writeln!(f, "    - Command defines: boolean flag (SetTrue/SetFalse)")?;
189                writeln!(f)?;
190                writeln!(f, "    Fix: Either:")?;
191                writeln!(
192                    f,
193                    "      - Change the handler parameter to `#[flag] {cli_name}: bool`"
194                )?;
195                writeln!(
196                    f,
197                    "      - Or change the command's action: .action(ArgAction::Set)"
198                )
199            }
200            ArgMismatch::RequiredMismatch {
201                cli_name,
202                handler_required,
203                command_required: _,
204            } => {
205                writeln!(f, "  Argument `{cli_name}`:")?;
206                if *handler_required {
207                    writeln!(f, "    - Handler expects: required argument")?;
208                    writeln!(f, "    - Command defines: optional argument")?;
209                    writeln!(f)?;
210                    writeln!(f, "    Fix: Either:")?;
211                    writeln!(
212                        f,
213                        "      - Change handler to `#[arg] {}: Option<T>`",
214                        cli_name.replace('-', "_")
215                    )?;
216                    writeln!(f, "      - Or add `.required(true)` to the command arg")
217                } else {
218                    writeln!(f, "    - Handler expects: optional argument (Option<T>)")?;
219                    writeln!(f, "    - Command defines: required argument")?;
220                    writeln!(f)?;
221                    writeln!(f, "    Fix: Either:")?;
222                    writeln!(
223                        f,
224                        "      - Change handler to `#[arg] {}: T` (not Option)",
225                        cli_name.replace('-', "_")
226                    )?;
227                    writeln!(
228                        f,
229                        "      - Or remove `.required(true)` from the command arg"
230                    )
231                }
232            }
233        }
234    }
235}
236
237/// Error when handler expectations don't match command definition.
238#[derive(Debug, Clone)]
239pub struct HandlerMismatchError {
240    /// The handler function name
241    pub handler_name: String,
242    /// The command name (if known)
243    pub command_name: Option<String>,
244    /// All detected mismatches
245    pub mismatches: Vec<ArgMismatch>,
246}
247
248impl std::error::Error for HandlerMismatchError {}
249
250impl fmt::Display for HandlerMismatchError {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        let cmd_desc = self
253            .command_name
254            .as_ref()
255            .map(|n| format!(" for command `{n}`"))
256            .unwrap_or_default();
257
258        writeln!(
259            f,
260            "Handler `{}` is incompatible with clap Command{cmd_desc}",
261            self.handler_name
262        )?;
263        writeln!(f)?;
264
265        for mismatch in &self.mismatches {
266            write!(f, "{mismatch}")?;
267        }
268
269        Ok(())
270    }
271}
272
273/// Check if an ArgAction represents a boolean flag.
274fn is_flag_action(action: &ArgAction) -> bool {
275    matches!(action, ArgAction::SetTrue | ArgAction::SetFalse)
276}
277
278/// Get a human-readable description of an ArgAction.
279fn describe_action(action: &ArgAction) -> String {
280    match action {
281        ArgAction::Set => "ArgAction::Set (single value)".to_string(),
282        ArgAction::Append => "ArgAction::Append (multiple values)".to_string(),
283        ArgAction::SetTrue => "ArgAction::SetTrue (boolean flag)".to_string(),
284        ArgAction::SetFalse => "ArgAction::SetFalse (boolean flag)".to_string(),
285        ArgAction::Count => "ArgAction::Count (counter)".to_string(),
286        ArgAction::Help => "ArgAction::Help".to_string(),
287        ArgAction::HelpShort => "ArgAction::HelpShort".to_string(),
288        ArgAction::HelpLong => "ArgAction::HelpLong".to_string(),
289        ArgAction::Version => "ArgAction::Version".to_string(),
290        _ => "unknown action".to_string(),
291    }
292}
293
294/// Verify that a handler's expected arguments match a clap Command's definition.
295///
296/// Returns `Ok(())` if all expectations are met, or `Err(HandlerMismatchError)`
297/// with detailed diagnostics if there are mismatches.
298///
299/// # Arguments
300///
301/// * `command` - The clap Command to verify against
302/// * `handler_name` - The handler function name (for error messages)
303/// * `expected` - The arguments the handler expects (from `__expected_args()`)
304///
305/// # Example
306///
307/// ```rust,ignore
308/// let command = Command::new("list")
309///     .arg(Arg::new("verbose").long("verbose").action(ArgAction::SetTrue));
310///
311/// let expected = vec![ExpectedArg::flag("verbose", "verbose")];
312///
313/// verify_handler_args(&command, "list_handler", &expected)?;
314/// ```
315pub fn verify_handler_args(
316    command: &Command,
317    handler_name: &str,
318    expected: &[ExpectedArg],
319) -> Result<(), HandlerMismatchError> {
320    let mut mismatches = Vec::new();
321
322    for exp in expected {
323        // Find the argument in the command
324        let arg = command
325            .get_arguments()
326            .find(|a| a.get_id() == exp.cli_name.as_str());
327
328        match arg {
329            None => {
330                // Argument doesn't exist in command
331                mismatches.push(ArgMismatch::MissingInCommand {
332                    cli_name: exp.cli_name.clone(),
333                    rust_name: exp.rust_name.clone(),
334                    expected_kind: exp.kind.clone(),
335                });
336            }
337            Some(arg) => {
338                let action = arg.get_action();
339
340                match exp.kind {
341                    ArgKind::Flag => {
342                        // Handler expects a flag - command must have SetTrue/SetFalse
343                        if !is_flag_action(action) {
344                            mismatches.push(ArgMismatch::NotAFlag {
345                                cli_name: exp.cli_name.clone(),
346                                actual_action: describe_action(action),
347                            });
348                        }
349                    }
350                    ArgKind::RequiredArg => {
351                        // Handler expects required arg
352                        if is_flag_action(action) {
353                            mismatches.push(ArgMismatch::UnexpectedFlag {
354                                cli_name: exp.cli_name.clone(),
355                                expected_kind: exp.kind.clone(),
356                            });
357                        } else if matches!(action, ArgAction::Count) {
358                            // Count is fine for a required integer arg (it returns 0 if missing)
359                        } else if !arg.is_required_set() && arg.get_default_values().is_empty() {
360                            mismatches.push(ArgMismatch::RequiredMismatch {
361                                cli_name: exp.cli_name.clone(),
362                                handler_required: true,
363                                command_required: false,
364                            });
365                        }
366                    }
367                    ArgKind::OptionalArg => {
368                        // Handler expects optional arg
369                        if is_flag_action(action) {
370                            mismatches.push(ArgMismatch::UnexpectedFlag {
371                                cli_name: exp.cli_name.clone(),
372                                expected_kind: exp.kind.clone(),
373                            });
374                        } else if arg.is_required_set() {
375                            mismatches.push(ArgMismatch::RequiredMismatch {
376                                cli_name: exp.cli_name.clone(),
377                                handler_required: false,
378                                command_required: true,
379                            });
380                        }
381                    }
382                    ArgKind::VecArg => {
383                        // Handler expects vec arg - command should have Append
384                        if is_flag_action(action) {
385                            mismatches.push(ArgMismatch::UnexpectedFlag {
386                                cli_name: exp.cli_name.clone(),
387                                expected_kind: exp.kind.clone(),
388                            });
389                        }
390                        // Note: We don't strictly require Append - Set with multiple
391                        // values can also work. Could add a warning here if needed.
392                    }
393                }
394            }
395        }
396    }
397
398    if mismatches.is_empty() {
399        Ok(())
400    } else {
401        Err(HandlerMismatchError {
402            handler_name: handler_name.to_string(),
403            command_name: Some(command.get_name().to_string()),
404            mismatches,
405        })
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use clap::Arg;
413
414    #[test]
415    fn test_verify_matching_flag() {
416        let command = Command::new("test").arg(
417            Arg::new("verbose")
418                .long("verbose")
419                .action(ArgAction::SetTrue),
420        );
421
422        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
423
424        assert!(verify_handler_args(&command, "test_handler", &expected).is_ok());
425    }
426
427    #[test]
428    fn test_verify_missing_arg() {
429        let command = Command::new("test");
430
431        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
432
433        let err = verify_handler_args(&command, "test_handler", &expected).unwrap_err();
434        assert_eq!(err.mismatches.len(), 1);
435        assert!(matches!(
436            &err.mismatches[0],
437            ArgMismatch::MissingInCommand { cli_name, .. } if cli_name == "verbose"
438        ));
439    }
440
441    #[test]
442    fn test_verify_wrong_action_for_flag() {
443        let command =
444            Command::new("test").arg(Arg::new("verbose").long("verbose").action(ArgAction::Set));
445
446        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
447
448        let err = verify_handler_args(&command, "test_handler", &expected).unwrap_err();
449        assert_eq!(err.mismatches.len(), 1);
450        assert!(matches!(&err.mismatches[0], ArgMismatch::NotAFlag { .. }));
451    }
452
453    #[test]
454    fn test_verify_required_mismatch() {
455        let command =
456            Command::new("test").arg(Arg::new("name").long("name").action(ArgAction::Set));
457        // Command has optional, handler expects required
458
459        let expected = vec![ExpectedArg::required_arg("name", "name")];
460
461        let err = verify_handler_args(&command, "test_handler", &expected).unwrap_err();
462        assert_eq!(err.mismatches.len(), 1);
463        assert!(matches!(
464            &err.mismatches[0],
465            ArgMismatch::RequiredMismatch {
466                handler_required: true,
467                command_required: false,
468                ..
469            }
470        ));
471    }
472
473    #[test]
474    fn test_verify_optional_matches() {
475        let command =
476            Command::new("test").arg(Arg::new("filter").long("filter").action(ArgAction::Set));
477
478        let expected = vec![ExpectedArg::optional_arg("filter", "filter")];
479
480        assert!(verify_handler_args(&command, "test_handler", &expected).is_ok());
481    }
482
483    #[test]
484    fn test_error_message_formatting() {
485        let command =
486            Command::new("list").arg(Arg::new("verbose").long("verbose").action(ArgAction::Set));
487
488        let expected = vec![ExpectedArg::flag("verbose", "verbose")];
489
490        let err = verify_handler_args(&command, "list_handler", &expected).unwrap_err();
491        let msg = err.to_string();
492
493        assert!(msg.contains("Handler `list_handler`"));
494        assert!(msg.contains("command `list`"));
495        assert!(msg.contains("Flag `verbose`"));
496        assert!(msg.contains("ArgAction::SetTrue"));
497    }
498}