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}