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).map(|v| Box::new(v) as Box<dyn Any + Send + Sync>)
73 }
74
75 fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String> {
76 let mut converted = Vec::with_capacity(values.len());
77 for value in values {
78 converted.push(self.convert(value)?);
79 }
80 Ok(Box::new(converted))
81 }
82
83 fn get_metavar(&self) -> Option<String> {
84 TypeConverter::get_metavar(self)
85 }
86
87 fn split_envvar_value(&self, value: &str) -> Vec<String> {
88 TypeConverter::split_envvar_value(self, value)
89 }
90
91 fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
92 TypeConverter::shell_complete(self, incomplete)
93 }
94}
95
96pub type ShellCompleteCallback = Arc<dyn Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync>;
98
99pub struct Argument {
132 pub config: ParameterConfig,
134
135 pub default_value: Option<String>,
137
138 type_converter: Box<dyn AnyTypeConverter>,
140
141 shell_complete_callback: Option<ShellCompleteCallback>,
143}
144
145impl fmt::Debug for Argument {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.debug_struct("Argument")
148 .field("config", &self.config)
149 .field("default_value", &self.default_value)
150 .field("type_name", &self.type_converter.name())
151 .field("has_shell_complete", &self.shell_complete_callback.is_some())
152 .finish()
153 }
154}
155
156impl Argument {
157 #[allow(clippy::new_ret_no_self)]
170 pub fn new(name: &str) -> ArgumentBuilder {
171 ArgumentBuilder::new(name)
172 }
173
174 pub fn builder(name: &str) -> ArgumentBuilder {
176 ArgumentBuilder::new(name)
177 }
178
179 pub fn type_converter(&self) -> &dyn AnyTypeConverter {
181 self.type_converter.as_ref()
182 }
183
184 pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
187 self.type_converter.convert_any(value)
188 }
189
190 pub fn convert(&self, value: &str) -> Result<String, String> {
194 let any_val = self.type_converter.convert_any(value)?;
195 any_val
196 .downcast::<String>()
197 .map(|v| *v)
198 .map_err(|_| "Type conversion returned non-String value; use convert_any() instead".to_string())
199 }
200
201 pub fn get_completions(&self, ctx: &Context, incomplete: &str) -> Vec<CompletionItem> {
206 if let Some(ref callback) = self.shell_complete_callback {
207 callback(ctx, incomplete)
208 } else {
209 self.type_converter.shell_complete(incomplete)
210 }
211 }
212
213 pub fn has_shell_complete_callback(&self) -> bool {
215 self.shell_complete_callback.is_some()
216 }
217
218 pub fn default_value(&self) -> Option<&str> {
220 self.default_value.as_deref()
221 }
222
223 pub fn make_metavar(&self) -> String {
234 let mut var = if let Some(metavar) = &self.config.metavar {
235 metavar.clone()
236 } else if let Some(type_metavar) = self.type_converter.get_metavar() {
237 if type_metavar.contains('|') {
238 if type_metavar.contains('{') || type_metavar.contains('}') {
239 type_metavar
240 } else {
241 format!("{{{}}}", type_metavar)
242 }
243 } else {
244 self.config.name.to_uppercase()
245 }
246 } else {
247 self.config.name.to_uppercase()
248 };
249
250 if self.config.deprecated.is_some() {
252 var.push('!');
253 }
254
255 if !self.config.required {
257 var = format!("[{}]", var);
258 }
259
260 match self.config.nargs {
262 Nargs::Variadic => var.push_str("..."),
263 Nargs::Count(n) if n != 1 => var.push_str("..."),
264 _ => {}
265 }
266
267 var
268 }
269}
270
271impl Parameter for Argument {
272 fn name(&self) -> &str {
273 &self.config.name
274 }
275
276 fn human_readable_name(&self) -> String {
277 if let Some(metavar) = &self.config.metavar {
279 metavar.clone()
280 } else {
281 self.config.name.to_uppercase()
282 }
283 }
284
285 fn nargs(&self) -> Nargs {
286 self.config.nargs
287 }
288
289 fn multiple(&self) -> bool {
290 self.config.multiple
291 }
292
293 fn is_eager(&self) -> bool {
294 self.config.is_eager
295 }
296
297 fn expose_value(&self) -> bool {
298 self.config.expose_value
299 }
300
301 fn required(&self) -> bool {
302 self.config.required
303 }
304
305 fn envvar(&self) -> Option<&[String]> {
306 self.config.envvar.as_deref()
307 }
308
309 fn help(&self) -> Option<&str> {
310 self.config.help.as_deref()
311 }
312
313 fn hidden(&self) -> bool {
314 self.config.hidden
315 }
316
317 fn get_metavar(&self) -> Option<String> {
318 Some(self.make_metavar())
319 }
320
321 fn get_help_record(&self) -> Option<(String, String)> {
322 if self.config.hidden {
324 return None;
325 }
326
327 let metavar = self.make_metavar();
328 let help = self.config.help.clone().unwrap_or_default();
329
330 Some((metavar, help))
331 }
332
333 fn param_type_name(&self) -> &str {
334 "argument"
335 }
336}
337
338pub struct ArgumentBuilder {
361 config: ParameterConfig,
362 default_value: Option<String>,
363 type_converter: Option<Box<dyn AnyTypeConverter>>,
364 required_explicitly_set: bool,
366 shell_complete_callback: Option<ShellCompleteCallback>,
368}
369
370impl ArgumentBuilder {
371 fn new(name: &str) -> Self {
373 Self {
374 config: ParameterConfig::new(name),
375 default_value: None,
376 type_converter: None,
377 required_explicitly_set: false,
378 shell_complete_callback: None,
379 }
380 }
381
382 pub fn help(mut self, help: &str) -> Self {
384 self.config.help = Some(help.to_string());
385 self
386 }
387
388 pub fn required(mut self, required: bool) -> Self {
392 self.config.required = required;
393 self.required_explicitly_set = true;
394 self
395 }
396
397 pub fn default(mut self, value: impl Into<String>) -> Self {
402 self.default_value = Some(value.into());
403 self
404 }
405
406 pub fn envvar(mut self, name: &str) -> Self {
411 self.config.envvar = Some(vec![name.to_string()]);
412 self
413 }
414
415 pub fn envvars(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
419 self.config.envvar = Some(names.into_iter().map(|n| n.into()).collect());
420 self
421 }
422
423 pub fn nargs(mut self, n: Nargs) -> Self {
425 self.config.nargs = n;
426 if matches!(n, Nargs::Variadic) {
428 self.config.multiple = true;
429 }
430 self
431 }
432
433 pub fn callback<F>(mut self, callback: F) -> Self
435 where
436 F: Fn(&Context, &dyn Parameter, Arc<dyn Any + Send + Sync>)
437 -> Result<Arc<dyn Any + Send + Sync>, ClickError>
438 + Send
439 + Sync
440 + 'static,
441 {
442 self.config.callback = Some(Arc::new(callback));
443 self
444 }
445
446 pub fn multiple(mut self) -> Self {
450 self.config.nargs = Nargs::Variadic;
451 self.config.multiple = true;
452 self
453 }
454
455 pub fn type_<T>(mut self, type_: T) -> Self
478 where
479 T: TypeConverter + Send + Sync + 'static,
480 T::Value: Send + Sync + 'static,
481 {
482 self.type_converter = Some(Box::new(type_));
483 self
484 }
485
486 pub fn shell_complete<F>(mut self, callback: F) -> Self
508 where
509 F: Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync + 'static,
510 {
511 self.shell_complete_callback = Some(Arc::new(callback));
512 self
513 }
514
515 pub fn metavar(mut self, metavar: &str) -> Self {
519 self.config.metavar = Some(metavar.to_string());
520 self
521 }
522
523 pub fn hidden(mut self, hidden: bool) -> Self {
525 self.config.hidden = hidden;
526 self
527 }
528
529 pub fn eager(mut self, eager: bool) -> Self {
531 self.config.is_eager = eager;
532 self
533 }
534
535 pub fn expose_value(mut self, expose: bool) -> Self {
537 self.config.expose_value = expose;
538 self
539 }
540
541 pub fn deprecated(mut self, deprecated: bool) -> Self {
543 self.config = self.config.deprecated(deprecated);
544 self
545 }
546
547 pub fn deprecated_with_message(mut self, message: impl Into<String>) -> Self {
549 self.config = self.config.deprecated_with_message(message);
550 self
551 }
552
553 pub fn build(mut self) -> Argument {
561 if !self.required_explicitly_set {
563 if self.default_value.is_some() {
564 self.config.required = false;
566 } else {
567 self.config.required = match self.config.nargs {
569 Nargs::Count(n) => n > 0,
570 Nargs::Variadic => true,
571 Nargs::Optional => false,
572 };
573 }
574 }
575
576 let type_converter: Box<dyn AnyTypeConverter> =
578 self.type_converter.unwrap_or_else(|| Box::new(StringType));
579
580 Argument {
581 config: self.config,
582 default_value: self.default_value,
583 type_converter,
584 shell_complete_callback: self.shell_complete_callback,
585 }
586 }
587}
588
589#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn test_argument_creation_defaults() {
599 let arg = Argument::new("filename").build();
600
601 assert_eq!(arg.name(), "filename");
602 assert!(arg.required());
603 assert!(!arg.multiple());
604 assert!(!arg.is_eager());
605 assert!(arg.expose_value());
606 assert_eq!(arg.nargs(), Nargs::Count(1));
607 assert!(arg.default_value().is_none());
608 assert_eq!(arg.param_type_name(), "argument");
609 }
610
611 #[test]
612 fn test_argument_human_readable_name() {
613 let arg = Argument::new("filename").build();
615 assert_eq!(arg.human_readable_name(), "FILENAME");
616
617 let arg = Argument::new("file").metavar("PATH").build();
619 assert_eq!(arg.human_readable_name(), "PATH");
620 }
621
622 #[test]
623 fn test_argument_with_default_is_optional() {
624 let arg = Argument::new("output").default("out.txt").build();
625
626 assert!(!arg.required());
627 assert_eq!(arg.default_value(), Some("out.txt"));
628 }
629
630 #[test]
631 fn test_argument_explicit_required_with_default() {
632 let arg = Argument::new("output")
634 .default("out.txt")
635 .required(true)
636 .build();
637
638 assert!(arg.required());
639 assert_eq!(arg.default_value(), Some("out.txt"));
640 }
641
642 #[test]
643 fn test_argument_explicit_optional_without_default() {
644 let arg = Argument::new("output").required(false).build();
645
646 assert!(!arg.required());
647 assert!(arg.default_value().is_none());
648 }
649
650 #[test]
651 fn test_argument_variadic() {
652 let arg = Argument::new("files").multiple().build();
653
654 assert!(arg.multiple());
655 assert_eq!(arg.nargs(), Nargs::Variadic);
656 }
657
658 #[test]
659 fn test_argument_nargs_variadic() {
660 let arg = Argument::new("files").nargs(Nargs::Variadic).build();
661
662 assert!(arg.multiple());
663 assert_eq!(arg.nargs(), Nargs::Variadic);
664 }
665
666 #[test]
667 fn test_argument_nargs_optional() {
668 let arg = Argument::new("file").nargs(Nargs::Optional).build();
669
670 assert!(!arg.required());
672 assert_eq!(arg.nargs(), Nargs::Optional);
673 }
674
675 #[test]
676 fn test_argument_nargs_count_zero() {
677 let arg = Argument::new("flag").nargs(Nargs::Count(0)).build();
678
679 assert!(!arg.required());
681 }
682
683 #[test]
684 fn test_argument_help_record_required() {
685 let arg = Argument::new("filename").help("The input file").build();
686
687 let record = arg.get_help_record();
688 assert!(record.is_some());
689
690 let (metavar, help) = record.unwrap();
691 assert_eq!(metavar, "FILENAME");
692 assert_eq!(help, "The input file");
693 }
694
695 #[test]
696 fn test_argument_help_record_optional() {
697 let arg = Argument::new("filename")
698 .required(false)
699 .help("The input file")
700 .build();
701
702 let record = arg.get_help_record();
703 assert!(record.is_some());
704
705 let (metavar, help) = record.unwrap();
706 assert_eq!(metavar, "[FILENAME]");
707 assert_eq!(help, "The input file");
708 }
709
710 #[test]
711 fn test_argument_help_record_variadic() {
712 let arg = Argument::new("files")
713 .multiple()
714 .help("Files to process")
715 .build();
716
717 let record = arg.get_help_record();
718 assert!(record.is_some());
719
720 let (metavar, _) = record.unwrap();
721 assert_eq!(metavar, "FILES...");
722 }
723
724 #[test]
725 fn test_argument_help_record_optional_variadic() {
726 let arg = Argument::new("files").multiple().required(false).build();
727
728 let record = arg.get_help_record();
729 let (metavar, _) = record.unwrap();
730 assert_eq!(metavar, "[FILES]...");
731 }
732
733 #[test]
734 fn test_argument_hidden() {
735 let arg = Argument::new("secret").hidden(true).build();
736
737 assert!(arg.hidden());
738 assert!(arg.get_help_record().is_none());
739 }
740
741 #[test]
742 fn test_argument_envvar() {
743 let arg = Argument::new("filename").envvar("MY_FILE").build();
744
745 let envvars = arg.envvar();
746 assert!(envvars.is_some());
747 assert_eq!(envvars.unwrap(), &["MY_FILE"]);
748 }
749
750 #[test]
751 fn test_argument_multiple_envvars() {
752 let arg = Argument::new("filename")
753 .envvars(["MY_FILE", "FALLBACK_FILE"])
754 .build();
755
756 let envvars = arg.envvar();
757 assert!(envvars.is_some());
758 assert_eq!(envvars.unwrap(), &["MY_FILE", "FALLBACK_FILE"]);
759 }
760
761 #[test]
762 fn test_argument_convert() {
763 let arg = Argument::new("text").build();
764
765 let result = arg.convert("hello world");
766 assert!(result.is_ok());
767 assert_eq!(result.unwrap(), "hello world");
768 }
769
770 #[test]
771 fn test_argument_deprecated_marker() {
772 let arg = Argument::new("old").deprecated(true).build();
773
774 let metavar = arg.make_metavar();
775 assert!(metavar.contains('!'));
776 }
777
778 #[test]
779 fn test_argument_custom_metavar() {
780 let arg = Argument::new("file").metavar("PATH").build();
781
782 assert_eq!(arg.make_metavar(), "PATH");
783 }
784
785 #[test]
786 fn test_argument_nargs_count_multiple() {
787 let arg = Argument::new("pair").nargs(Nargs::Count(2)).build();
788
789 let metavar = arg.make_metavar();
790 assert_eq!(metavar, "PAIR...");
791 }
792
793 #[test]
794 fn test_argument_debug() {
795 let arg = Argument::new("test").build();
796 let debug_str = format!("{:?}", arg);
797 assert!(debug_str.contains("Argument"));
798 assert!(debug_str.contains("test"));
799 }
800
801 #[test]
806 fn test_argument_with_int_type() {
807 use crate::types::INT;
808
809 let arg = Argument::new("count").type_(INT).build();
810
811 let result = arg.convert_any("42");
813 assert!(result.is_ok());
814
815 let boxed = result.unwrap();
817 let value = boxed.downcast_ref::<i64>();
818 assert!(value.is_some());
819 assert_eq!(*value.unwrap(), 42i64);
820 }
821
822 #[test]
823 fn test_argument_with_int_type_error() {
824 use crate::types::INT;
825
826 let arg = Argument::new("count").type_(INT).build();
827
828 let result = arg.convert_any("not-a-number");
829 assert!(result.is_err());
830 }
831
832 #[test]
833 fn test_argument_with_float_type() {
834 use crate::types::FLOAT;
835
836 let arg = Argument::new("value").type_(FLOAT).build();
837
838 let result = arg.convert_any("3.14");
839 assert!(result.is_ok());
840
841 let boxed = result.unwrap();
842 let value = boxed.downcast_ref::<f64>();
843 assert!(value.is_some());
844 assert!((value.unwrap() - 3.14).abs() < 0.001);
845 }
846
847 #[test]
848 fn test_argument_with_path_type() {
849 use crate::types::PathType;
850 use std::path::PathBuf;
851
852 let arg = Argument::new("path").type_(PathType::new()).build();
853
854 let result = arg.convert_any("/some/path");
855 assert!(result.is_ok());
856
857 let boxed = result.unwrap();
858 let value = boxed.downcast_ref::<PathBuf>();
859 assert!(value.is_some());
860 assert_eq!(*value.unwrap(), PathBuf::from("/some/path"));
861 }
862
863 #[test]
864 fn test_argument_with_choice_type() {
865 use crate::types::Choice;
866
867 let arg = Argument::new("format")
868 .type_(Choice::new(["json", "xml", "yaml"]))
869 .build();
870
871 let result = arg.convert_any("json");
872 assert!(result.is_ok());
873
874 let boxed = result.unwrap();
875 let value = boxed.downcast_ref::<String>();
876 assert!(value.is_some());
877 assert_eq!(value.unwrap(), "json");
878
879 let result = arg.convert_any("csv");
881 assert!(result.is_err());
882 }
883
884 #[test]
885 fn test_argument_string_convert_fallback() {
886 let arg = Argument::new("text").build();
888 let result = arg.convert("hello");
889 assert!(result.is_ok());
890 assert_eq!(result.unwrap(), "hello");
891 }
892
893 #[test]
894 fn test_argument_convert_non_string_returns_error() {
895 use crate::types::INT;
896
897 let arg = Argument::new("count").type_(INT).build();
899 let result = arg.convert("42");
900 assert!(result.is_err());
901 assert!(result.unwrap_err().contains("non-String"));
902 }
903
904 #[test]
909 fn test_argument_shell_complete_callback() {
910 use crate::context::ContextBuilder;
911
912 let arg = Argument::new("filename")
913 .shell_complete(|_ctx, incomplete| {
914 vec![
915 CompletionItem::new(format!("{}.txt", incomplete)),
916 CompletionItem::new(format!("{}.md", incomplete)),
917 ]
918 })
919 .build();
920
921 assert!(arg.has_shell_complete_callback());
922
923 let ctx = ContextBuilder::new().build();
924 let completions = arg.get_completions(&ctx, "test");
925
926 assert_eq!(completions.len(), 2);
927 assert_eq!(completions[0].value, "test.txt");
928 assert_eq!(completions[1].value, "test.md");
929 }
930
931 #[test]
932 fn test_argument_shell_complete_from_type() {
933 use crate::context::ContextBuilder;
934 use crate::types::Choice;
935
936 let arg = Argument::new("format")
938 .type_(Choice::new(["json", "xml", "yaml"]))
939 .build();
940
941 assert!(!arg.has_shell_complete_callback());
942
943 let ctx = ContextBuilder::new().build();
944 let completions = arg.get_completions(&ctx, "j");
945
946 assert_eq!(completions.len(), 1);
947 assert_eq!(completions[0].value, "json");
948 }
949
950 #[test]
951 fn test_argument_shell_complete_overrides_type() {
952 use crate::context::ContextBuilder;
953 use crate::types::Choice;
954
955 let arg = Argument::new("format")
957 .type_(Choice::new(["json", "xml", "yaml"]))
958 .shell_complete(|_ctx, _incomplete| {
959 vec![CompletionItem::new("custom")]
960 })
961 .build();
962
963 let ctx = ContextBuilder::new().build();
964 let completions = arg.get_completions(&ctx, "j");
965
966 assert_eq!(completions.len(), 1);
968 assert_eq!(completions[0].value, "custom");
969 }
970
971 #[test]
972 fn test_argument_no_shell_complete() {
973 use crate::context::ContextBuilder;
974
975 let arg = Argument::new("text").build();
977
978 assert!(!arg.has_shell_complete_callback());
979
980 let ctx = ContextBuilder::new().build();
981 let completions = arg.get_completions(&ctx, "test");
982
983 assert!(completions.is_empty());
984 }
985
986 #[test]
987 fn test_argument_shell_complete_with_context() {
988 use crate::context::ContextBuilder;
989
990 let arg = Argument::new("name")
992 .shell_complete(|ctx, incomplete| {
993 let prefix = ctx.info_name().unwrap_or("default");
995 vec![CompletionItem::new(format!("{}_{}", prefix, incomplete))]
996 })
997 .build();
998
999 let ctx = ContextBuilder::new().info_name("myapp").build();
1000 let completions = arg.get_completions(&ctx, "test");
1001
1002 assert_eq!(completions.len(), 1);
1003 assert_eq!(completions[0].value, "myapp_test");
1004 }
1005}