1use std::any::Any;
25use std::fmt;
26use std::sync::Arc;
27
28use crate::context::Context;
29use crate::error::ClickError;
30use crate::parameter::{Nargs, Parameter, ParameterConfig};
31use crate::types::{CompletionItem, StringType, TypeConverter};
32
33pub trait AnyTypeConverter: Send + Sync {
41 fn name(&self) -> &str;
43
44 fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String>;
46
47 fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String>;
51
52 fn get_metavar(&self) -> Option<String>;
54
55 fn split_envvar_value(&self, value: &str) -> Vec<String>;
57
58 fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem>;
60}
61
62impl<T> AnyTypeConverter for T
63where
64 T: TypeConverter + Send + Sync,
65 T::Value: Send + Sync + 'static,
66{
67 fn name(&self) -> &str {
68 TypeConverter::name(self)
69 }
70
71 fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
72 self.convert(value)
73 .map(|v| Box::new(v) as Box<dyn Any + Send + Sync>)
74 }
75
76 fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String> {
77 let mut converted = Vec::with_capacity(values.len());
78 for value in values {
79 converted.push(self.convert(value)?);
80 }
81 Ok(Box::new(converted))
82 }
83
84 fn get_metavar(&self) -> Option<String> {
85 TypeConverter::get_metavar(self)
86 }
87
88 fn split_envvar_value(&self, value: &str) -> Vec<String> {
89 TypeConverter::split_envvar_value(self, value)
90 }
91
92 fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
93 TypeConverter::shell_complete(self, incomplete)
94 }
95}
96
97pub type ShellCompleteCallback = Arc<dyn Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync>;
99
100pub struct Argument {
133 pub config: ParameterConfig,
135
136 pub default_value: Option<String>,
138
139 type_converter: Box<dyn AnyTypeConverter>,
141
142 shell_complete_callback: Option<ShellCompleteCallback>,
144}
145
146impl fmt::Debug for Argument {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 f.debug_struct("Argument")
149 .field("config", &self.config)
150 .field("default_value", &self.default_value)
151 .field("type_name", &self.type_converter.name())
152 .field(
153 "has_shell_complete",
154 &self.shell_complete_callback.is_some(),
155 )
156 .finish()
157 }
158}
159
160impl Argument {
161 #[allow(clippy::new_ret_no_self)]
174 pub fn new(name: &str) -> ArgumentBuilder {
175 ArgumentBuilder::new(name)
176 }
177
178 pub fn builder(name: &str) -> ArgumentBuilder {
180 ArgumentBuilder::new(name)
181 }
182
183 pub fn type_converter(&self) -> &dyn AnyTypeConverter {
185 self.type_converter.as_ref()
186 }
187
188 pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
191 self.type_converter.convert_any(value)
192 }
193
194 pub fn convert(&self, value: &str) -> Result<String, String> {
198 let any_val = self.type_converter.convert_any(value)?;
199 any_val.downcast::<String>().map(|v| *v).map_err(|_| {
200 "Type conversion returned non-String value; use convert_any() instead".to_string()
201 })
202 }
203
204 pub fn get_completions(&self, ctx: &Context, incomplete: &str) -> Vec<CompletionItem> {
209 if let Some(ref callback) = self.shell_complete_callback {
210 callback(ctx, incomplete)
211 } else {
212 self.type_converter.shell_complete(incomplete)
213 }
214 }
215
216 pub fn has_shell_complete_callback(&self) -> bool {
218 self.shell_complete_callback.is_some()
219 }
220
221 pub fn default_value(&self) -> Option<&str> {
223 self.default_value.as_deref()
224 }
225
226 pub fn make_metavar(&self) -> String {
237 let mut var = if let Some(metavar) = &self.config.metavar {
238 metavar.clone()
239 } else if let Some(type_metavar) = self.type_converter.get_metavar() {
240 if type_metavar.contains('|') {
241 if type_metavar.contains('{') || type_metavar.contains('}') {
242 type_metavar
243 } else {
244 format!("{{{}}}", type_metavar)
245 }
246 } else {
247 self.config.name.to_uppercase()
248 }
249 } else {
250 self.config.name.to_uppercase()
251 };
252
253 if self.config.deprecated.is_some() {
255 var.push('!');
256 }
257
258 if !self.config.required {
260 var = format!("[{}]", var);
261 }
262
263 match self.config.nargs {
265 Nargs::Variadic => var.push_str("..."),
266 Nargs::Count(n) if n != 1 => var.push_str("..."),
267 _ => {}
268 }
269
270 var
271 }
272}
273
274impl Parameter for Argument {
275 fn name(&self) -> &str {
276 &self.config.name
277 }
278
279 fn human_readable_name(&self) -> String {
280 if let Some(metavar) = &self.config.metavar {
282 metavar.clone()
283 } else {
284 self.config.name.to_uppercase()
285 }
286 }
287
288 fn nargs(&self) -> Nargs {
289 self.config.nargs
290 }
291
292 fn multiple(&self) -> bool {
293 self.config.multiple
294 }
295
296 fn is_eager(&self) -> bool {
297 self.config.is_eager
298 }
299
300 fn expose_value(&self) -> bool {
301 self.config.expose_value
302 }
303
304 fn required(&self) -> bool {
305 self.config.required
306 }
307
308 fn envvar(&self) -> Option<&[String]> {
309 self.config.envvar.as_deref()
310 }
311
312 fn help(&self) -> Option<&str> {
313 self.config.help.as_deref()
314 }
315
316 fn hidden(&self) -> bool {
317 self.config.hidden
318 }
319
320 fn get_metavar(&self) -> Option<String> {
321 Some(self.make_metavar())
322 }
323
324 fn get_help_record(&self) -> Option<(String, String)> {
325 if self.config.hidden {
327 return None;
328 }
329
330 let metavar = self.make_metavar();
331 let help = self.config.help.clone().unwrap_or_default();
332
333 Some((metavar, help))
334 }
335
336 fn param_type_name(&self) -> &str {
337 "argument"
338 }
339}
340
341pub struct ArgumentBuilder {
364 config: ParameterConfig,
365 default_value: Option<String>,
366 type_converter: Option<Box<dyn AnyTypeConverter>>,
367 required_explicitly_set: bool,
369 shell_complete_callback: Option<ShellCompleteCallback>,
371}
372
373impl ArgumentBuilder {
374 fn new(name: &str) -> Self {
376 Self {
377 config: ParameterConfig::new(name),
378 default_value: None,
379 type_converter: None,
380 required_explicitly_set: false,
381 shell_complete_callback: None,
382 }
383 }
384
385 pub fn help(mut self, help: &str) -> Self {
387 self.config.help = Some(help.to_string());
388 self
389 }
390
391 pub fn required(mut self, required: bool) -> Self {
395 self.config.required = required;
396 self.required_explicitly_set = true;
397 self
398 }
399
400 pub fn default(mut self, value: impl Into<String>) -> Self {
405 self.default_value = Some(value.into());
406 self
407 }
408
409 pub fn envvar(mut self, name: &str) -> Self {
414 self.config.envvar = Some(vec![name.to_string()]);
415 self
416 }
417
418 pub fn envvars(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
422 self.config.envvar = Some(names.into_iter().map(|n| n.into()).collect());
423 self
424 }
425
426 pub fn nargs(mut self, n: Nargs) -> Self {
428 self.config.nargs = n;
429 if matches!(n, Nargs::Variadic) {
431 self.config.multiple = true;
432 }
433 self
434 }
435
436 pub fn callback<F>(mut self, callback: F) -> Self
438 where
439 F: Fn(
440 &Context,
441 &dyn Parameter,
442 Arc<dyn Any + Send + Sync>,
443 ) -> Result<Arc<dyn Any + Send + Sync>, ClickError>
444 + Send
445 + Sync
446 + 'static,
447 {
448 self.config.callback = Some(Arc::new(callback));
449 self
450 }
451
452 pub fn multiple(mut self) -> Self {
456 self.config.nargs = Nargs::Variadic;
457 self.config.multiple = true;
458 self
459 }
460
461 pub fn type_<T>(mut self, type_: T) -> Self
484 where
485 T: TypeConverter + Send + Sync + 'static,
486 T::Value: Send + Sync + 'static,
487 {
488 self.type_converter = Some(Box::new(type_));
489 self
490 }
491
492 pub fn shell_complete<F>(mut self, callback: F) -> Self
514 where
515 F: Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync + 'static,
516 {
517 self.shell_complete_callback = Some(Arc::new(callback));
518 self
519 }
520
521 pub fn metavar(mut self, metavar: &str) -> Self {
525 self.config.metavar = Some(metavar.to_string());
526 self
527 }
528
529 pub fn hidden(mut self, hidden: bool) -> Self {
531 self.config.hidden = hidden;
532 self
533 }
534
535 pub fn eager(mut self, eager: bool) -> Self {
537 self.config.is_eager = eager;
538 self
539 }
540
541 pub fn expose_value(mut self, expose: bool) -> Self {
543 self.config.expose_value = expose;
544 self
545 }
546
547 pub fn deprecated(mut self, deprecated: bool) -> Self {
549 self.config = self.config.deprecated(deprecated);
550 self
551 }
552
553 pub fn deprecated_with_message(mut self, message: impl Into<String>) -> Self {
555 self.config = self.config.deprecated_with_message(message);
556 self
557 }
558
559 pub fn build(mut self) -> Argument {
567 if !self.required_explicitly_set {
569 if self.default_value.is_some() {
570 self.config.required = false;
572 } else {
573 self.config.required = match self.config.nargs {
575 Nargs::Count(n) => n > 0,
576 Nargs::Variadic => true,
577 Nargs::Optional => false,
578 };
579 }
580 }
581
582 let type_converter: Box<dyn AnyTypeConverter> =
584 self.type_converter.unwrap_or_else(|| Box::new(StringType));
585
586 Argument {
587 config: self.config,
588 default_value: self.default_value,
589 type_converter,
590 shell_complete_callback: self.shell_complete_callback,
591 }
592 }
593}
594
595#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn test_argument_creation_defaults() {
605 let arg = Argument::new("filename").build();
606
607 assert_eq!(arg.name(), "filename");
608 assert!(arg.required());
609 assert!(!arg.multiple());
610 assert!(!arg.is_eager());
611 assert!(arg.expose_value());
612 assert_eq!(arg.nargs(), Nargs::Count(1));
613 assert!(arg.default_value().is_none());
614 assert_eq!(arg.param_type_name(), "argument");
615 }
616
617 #[test]
618 fn test_argument_human_readable_name() {
619 let arg = Argument::new("filename").build();
621 assert_eq!(arg.human_readable_name(), "FILENAME");
622
623 let arg = Argument::new("file").metavar("PATH").build();
625 assert_eq!(arg.human_readable_name(), "PATH");
626 }
627
628 #[test]
629 fn test_argument_with_default_is_optional() {
630 let arg = Argument::new("output").default("out.txt").build();
631
632 assert!(!arg.required());
633 assert_eq!(arg.default_value(), Some("out.txt"));
634 }
635
636 #[test]
637 fn test_argument_explicit_required_with_default() {
638 let arg = Argument::new("output")
640 .default("out.txt")
641 .required(true)
642 .build();
643
644 assert!(arg.required());
645 assert_eq!(arg.default_value(), Some("out.txt"));
646 }
647
648 #[test]
649 fn test_argument_explicit_optional_without_default() {
650 let arg = Argument::new("output").required(false).build();
651
652 assert!(!arg.required());
653 assert!(arg.default_value().is_none());
654 }
655
656 #[test]
657 fn test_argument_variadic() {
658 let arg = Argument::new("files").multiple().build();
659
660 assert!(arg.multiple());
661 assert_eq!(arg.nargs(), Nargs::Variadic);
662 }
663
664 #[test]
665 fn test_argument_nargs_variadic() {
666 let arg = Argument::new("files").nargs(Nargs::Variadic).build();
667
668 assert!(arg.multiple());
669 assert_eq!(arg.nargs(), Nargs::Variadic);
670 }
671
672 #[test]
673 fn test_argument_nargs_optional() {
674 let arg = Argument::new("file").nargs(Nargs::Optional).build();
675
676 assert!(!arg.required());
678 assert_eq!(arg.nargs(), Nargs::Optional);
679 }
680
681 #[test]
682 fn test_argument_nargs_count_zero() {
683 let arg = Argument::new("flag").nargs(Nargs::Count(0)).build();
684
685 assert!(!arg.required());
687 }
688
689 #[test]
690 fn test_argument_help_record_required() {
691 let arg = Argument::new("filename").help("The input file").build();
692
693 let record = arg.get_help_record();
694 assert!(record.is_some());
695
696 let (metavar, help) = record.unwrap();
697 assert_eq!(metavar, "FILENAME");
698 assert_eq!(help, "The input file");
699 }
700
701 #[test]
702 fn test_argument_help_record_optional() {
703 let arg = Argument::new("filename")
704 .required(false)
705 .help("The input file")
706 .build();
707
708 let record = arg.get_help_record();
709 assert!(record.is_some());
710
711 let (metavar, help) = record.unwrap();
712 assert_eq!(metavar, "[FILENAME]");
713 assert_eq!(help, "The input file");
714 }
715
716 #[test]
717 fn test_argument_help_record_variadic() {
718 let arg = Argument::new("files")
719 .multiple()
720 .help("Files to process")
721 .build();
722
723 let record = arg.get_help_record();
724 assert!(record.is_some());
725
726 let (metavar, _) = record.unwrap();
727 assert_eq!(metavar, "FILES...");
728 }
729
730 #[test]
731 fn test_argument_help_record_optional_variadic() {
732 let arg = Argument::new("files").multiple().required(false).build();
733
734 let record = arg.get_help_record();
735 let (metavar, _) = record.unwrap();
736 assert_eq!(metavar, "[FILES]...");
737 }
738
739 #[test]
740 fn test_argument_hidden() {
741 let arg = Argument::new("secret").hidden(true).build();
742
743 assert!(arg.hidden());
744 assert!(arg.get_help_record().is_none());
745 }
746
747 #[test]
748 fn test_argument_envvar() {
749 let arg = Argument::new("filename").envvar("MY_FILE").build();
750
751 let envvars = arg.envvar();
752 assert!(envvars.is_some());
753 assert_eq!(envvars.unwrap(), &["MY_FILE"]);
754 }
755
756 #[test]
757 fn test_argument_multiple_envvars() {
758 let arg = Argument::new("filename")
759 .envvars(["MY_FILE", "FALLBACK_FILE"])
760 .build();
761
762 let envvars = arg.envvar();
763 assert!(envvars.is_some());
764 assert_eq!(envvars.unwrap(), &["MY_FILE", "FALLBACK_FILE"]);
765 }
766
767 #[test]
768 fn test_argument_convert() {
769 let arg = Argument::new("text").build();
770
771 let result = arg.convert("hello world");
772 assert!(result.is_ok());
773 assert_eq!(result.unwrap(), "hello world");
774 }
775
776 #[test]
777 fn test_argument_deprecated_marker() {
778 let arg = Argument::new("old").deprecated(true).build();
779
780 let metavar = arg.make_metavar();
781 assert!(metavar.contains('!'));
782 }
783
784 #[test]
785 fn test_argument_custom_metavar() {
786 let arg = Argument::new("file").metavar("PATH").build();
787
788 assert_eq!(arg.make_metavar(), "PATH");
789 }
790
791 #[test]
792 fn test_argument_nargs_count_multiple() {
793 let arg = Argument::new("pair").nargs(Nargs::Count(2)).build();
794
795 let metavar = arg.make_metavar();
796 assert_eq!(metavar, "PAIR...");
797 }
798
799 #[test]
800 fn test_argument_debug() {
801 let arg = Argument::new("test").build();
802 let debug_str = format!("{:?}", arg);
803 assert!(debug_str.contains("Argument"));
804 assert!(debug_str.contains("test"));
805 }
806
807 #[test]
812 fn test_argument_with_int_type() {
813 use crate::types::INT;
814
815 let arg = Argument::new("count").type_(INT).build();
816
817 let result = arg.convert_any("42");
819 assert!(result.is_ok());
820
821 let boxed = result.unwrap();
823 let value = boxed.downcast_ref::<i64>();
824 assert!(value.is_some());
825 assert_eq!(*value.unwrap(), 42i64);
826 }
827
828 #[test]
829 fn test_argument_with_int_type_error() {
830 use crate::types::INT;
831
832 let arg = Argument::new("count").type_(INT).build();
833
834 let result = arg.convert_any("not-a-number");
835 assert!(result.is_err());
836 }
837
838 #[test]
839 fn test_argument_with_float_type() {
840 use crate::types::FLOAT;
841
842 let arg = Argument::new("value").type_(FLOAT).build();
843
844 let result = arg.convert_any("3.14");
845 assert!(result.is_ok());
846
847 let boxed = result.unwrap();
848 let value = boxed.downcast_ref::<f64>();
849 assert!(value.is_some());
850 assert!((value.unwrap() - 3.14).abs() < 0.001);
851 }
852
853 #[test]
854 fn test_argument_with_path_type() {
855 use crate::types::PathType;
856 use std::path::PathBuf;
857
858 let arg = Argument::new("path").type_(PathType::new()).build();
859
860 let result = arg.convert_any("/some/path");
861 assert!(result.is_ok());
862
863 let boxed = result.unwrap();
864 let value = boxed.downcast_ref::<PathBuf>();
865 assert!(value.is_some());
866 assert_eq!(*value.unwrap(), PathBuf::from("/some/path"));
867 }
868
869 #[test]
870 fn test_argument_with_choice_type() {
871 use crate::types::Choice;
872
873 let arg = Argument::new("format")
874 .type_(Choice::new(["json", "xml", "yaml"]))
875 .build();
876
877 let result = arg.convert_any("json");
878 assert!(result.is_ok());
879
880 let boxed = result.unwrap();
881 let value = boxed.downcast_ref::<String>();
882 assert!(value.is_some());
883 assert_eq!(value.unwrap(), "json");
884
885 let result = arg.convert_any("csv");
887 assert!(result.is_err());
888 }
889
890 #[test]
891 fn test_argument_string_convert_fallback() {
892 let arg = Argument::new("text").build();
894 let result = arg.convert("hello");
895 assert!(result.is_ok());
896 assert_eq!(result.unwrap(), "hello");
897 }
898
899 #[test]
900 fn test_argument_convert_non_string_returns_error() {
901 use crate::types::INT;
902
903 let arg = Argument::new("count").type_(INT).build();
905 let result = arg.convert("42");
906 assert!(result.is_err());
907 assert!(result.unwrap_err().contains("non-String"));
908 }
909
910 #[test]
915 fn test_argument_shell_complete_callback() {
916 use crate::context::ContextBuilder;
917
918 let arg = Argument::new("filename")
919 .shell_complete(|_ctx, incomplete| {
920 vec![
921 CompletionItem::new(format!("{}.txt", incomplete)),
922 CompletionItem::new(format!("{}.md", incomplete)),
923 ]
924 })
925 .build();
926
927 assert!(arg.has_shell_complete_callback());
928
929 let ctx = ContextBuilder::new().build();
930 let completions = arg.get_completions(&ctx, "test");
931
932 assert_eq!(completions.len(), 2);
933 assert_eq!(completions[0].value, "test.txt");
934 assert_eq!(completions[1].value, "test.md");
935 }
936
937 #[test]
938 fn test_argument_shell_complete_from_type() {
939 use crate::context::ContextBuilder;
940 use crate::types::Choice;
941
942 let arg = Argument::new("format")
944 .type_(Choice::new(["json", "xml", "yaml"]))
945 .build();
946
947 assert!(!arg.has_shell_complete_callback());
948
949 let ctx = ContextBuilder::new().build();
950 let completions = arg.get_completions(&ctx, "j");
951
952 assert_eq!(completions.len(), 1);
953 assert_eq!(completions[0].value, "json");
954 }
955
956 #[test]
957 fn test_argument_shell_complete_overrides_type() {
958 use crate::context::ContextBuilder;
959 use crate::types::Choice;
960
961 let arg = Argument::new("format")
963 .type_(Choice::new(["json", "xml", "yaml"]))
964 .shell_complete(|_ctx, _incomplete| vec![CompletionItem::new("custom")])
965 .build();
966
967 let ctx = ContextBuilder::new().build();
968 let completions = arg.get_completions(&ctx, "j");
969
970 assert_eq!(completions.len(), 1);
972 assert_eq!(completions[0].value, "custom");
973 }
974
975 #[test]
976 fn test_argument_no_shell_complete() {
977 use crate::context::ContextBuilder;
978
979 let arg = Argument::new("text").build();
981
982 assert!(!arg.has_shell_complete_callback());
983
984 let ctx = ContextBuilder::new().build();
985 let completions = arg.get_completions(&ctx, "test");
986
987 assert!(completions.is_empty());
988 }
989
990 #[test]
991 fn test_argument_shell_complete_with_context() {
992 use crate::context::ContextBuilder;
993
994 let arg = Argument::new("name")
996 .shell_complete(|ctx, incomplete| {
997 let prefix = ctx.info_name().unwrap_or("default");
999 vec![CompletionItem::new(format!("{}_{}", prefix, incomplete))]
1000 })
1001 .build();
1002
1003 let ctx = ContextBuilder::new().info_name("myapp").build();
1004 let completions = arg.get_completions(&ctx, "test");
1005
1006 assert_eq!(completions.len(), 1);
1007 assert_eq!(completions[0].value, "myapp_test");
1008 }
1009}