1use clap::{ArgAction, Command};
24use std::fmt;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ArgKind {
29 Flag,
31 RequiredArg,
33 OptionalArg,
35 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#[derive(Debug, Clone)]
54pub struct ExpectedArg {
55 pub cli_name: String,
57 pub rust_name: String,
59 pub kind: ArgKind,
61}
62
63impl ExpectedArg {
64 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 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 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 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#[derive(Debug, Clone)]
103pub enum ArgMismatch {
104 MissingInCommand {
106 cli_name: String,
107 rust_name: String,
108 expected_kind: ArgKind,
109 },
110 NotAFlag {
112 cli_name: String,
113 actual_action: String,
114 },
115 UnexpectedFlag {
117 cli_name: String,
118 expected_kind: ArgKind,
119 },
120 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#[derive(Debug, Clone)]
239pub struct HandlerMismatchError {
240 pub handler_name: String,
242 pub command_name: Option<String>,
244 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
273fn is_flag_action(action: &ArgAction) -> bool {
275 matches!(action, ArgAction::SetTrue | ArgAction::SetFalse)
276}
277
278fn 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
294pub 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 let arg = command
325 .get_arguments()
326 .find(|a| a.get_id() == exp.cli_name.as_str());
327
328 match arg {
329 None => {
330 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 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 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 } 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 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 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 }
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 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}