Skip to main content

click/
argument.rs

1//! Positional argument parameter for click-rs.
2//!
3//! This module provides the `Argument` struct for positional command-line parameters.
4//! Arguments are required by default and appear without dashes in the command line.
5//!
6//! # Reference
7//!
8//! Based on Python Click's `core.py:Argument` class (line 3319+).
9//!
10//! # Example
11//!
12//! ```
13//! use click::argument::Argument;
14//! use click::parameter::Parameter;
15//!
16//! let arg = Argument::new("filename")
17//!     .help("The file to process")
18//!     .build();
19//!
20//! assert!(arg.required());
21//! assert_eq!(arg.human_readable_name(), "FILENAME");
22//! ```
23
24use 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
33// =============================================================================
34// Type-erased Converter Wrapper
35// =============================================================================
36
37/// A type-erased wrapper for TypeConverter that stores converted values as `Box<dyn Any>`.
38///
39/// This allows arguments to work with any TypeConverter, not just those returning String.
40pub trait AnyTypeConverter: Send + Sync {
41    /// Returns the descriptive name of this type.
42    fn name(&self) -> &str;
43
44    /// Convert a string value to the target type, returning as Box<dyn Any>.
45    fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String>;
46
47    /// Convert multiple string values to the target type, returning as Box<dyn Any>.
48    ///
49    /// By default this returns a Vec of the underlying type.
50    fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String>;
51
52    /// Returns the metavar for this type.
53    fn get_metavar(&self) -> Option<String>;
54
55    /// Split an environment variable value into multiple values.
56    fn split_envvar_value(&self, value: &str) -> Vec<String>;
57
58    /// Returns shell completion items for the given incomplete value.
59    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
96/// Custom shell completion callback type for arguments.
97pub type ShellCompleteCallback = Arc<dyn Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync>;
98
99// =============================================================================
100// Argument Struct
101// =============================================================================
102
103/// A positional command-line argument.
104///
105/// Arguments are positional parameters that appear without dashes. Unlike options,
106/// they are required by default and have a single name (no aliases).
107///
108/// # Key Differences from Options
109///
110/// - Arguments are positional (no `--` prefix)
111/// - Required by default (unless a default is provided)
112/// - Single name only (no short/long aliases)
113/// - Name is displayed in uppercase in help text
114///
115/// # Example
116///
117/// ```
118/// use click::argument::Argument;
119/// use click::parameter::Parameter;
120///
121/// // Required argument
122/// let filename = Argument::new("filename").build();
123/// assert!(filename.required());
124///
125/// // Optional argument with default
126/// let output = Argument::new("output")
127///     .default("out.txt")
128///     .build();
129/// assert!(!output.required());
130/// ```
131pub struct Argument {
132    /// Parameter configuration.
133    pub config: ParameterConfig,
134
135    /// The default value (if any).
136    pub default_value: Option<String>,
137
138    /// Type converter for this argument (type-erased to support any return type).
139    type_converter: Box<dyn AnyTypeConverter>,
140
141    /// Custom shell completion callback.
142    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    /// Create a new argument builder with the given name.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// use click::argument::Argument;
163    ///
164    /// let arg = Argument::builder("filename")
165    ///     .help("The file to process")
166    ///     .required(true)
167    ///     .build();
168    /// ```
169    #[allow(clippy::new_ret_no_self)]
170    pub fn new(name: &str) -> ArgumentBuilder {
171        ArgumentBuilder::new(name)
172    }
173
174    /// Alias for `new()` - create a new argument builder.
175    pub fn builder(name: &str) -> ArgumentBuilder {
176        ArgumentBuilder::new(name)
177    }
178
179    /// Get the type converter for this argument.
180    pub fn type_converter(&self) -> &dyn AnyTypeConverter {
181        self.type_converter.as_ref()
182    }
183
184    /// Convert a string value using this argument's type converter.
185    /// Returns the converted value as a boxed Any type.
186    pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
187        self.type_converter.convert_any(value)
188    }
189
190    /// Convert a string value using this argument's type converter.
191    /// This is a convenience method that attempts to downcast to String.
192    /// For non-String types, use `convert_any` instead.
193    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    /// Get shell completions for this argument.
202    ///
203    /// If a custom shell_complete callback is set, it will be used.
204    /// Otherwise, completions from the type converter are returned.
205    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    /// Check if this argument has a custom shell_complete callback.
214    pub fn has_shell_complete_callback(&self) -> bool {
215        self.shell_complete_callback.is_some()
216    }
217
218    /// Get the default value for this argument.
219    pub fn default_value(&self) -> Option<&str> {
220        self.default_value.as_deref()
221    }
222
223    /// Generate the metavar for this argument (used in help text).
224    ///
225    /// The metavar is formatted according to Click's conventions:
226    /// - Custom metavar if set, otherwise uppercase name
227    /// - Wrapped in `[]` if optional
228    /// - Suffixed with `...` if variadic or multiple
229    /// - Suffixed with `!` if deprecated
230    ///
231    /// Note: Unlike options, arguments use their name (uppercase) by default,
232    /// not the type's metavar. This matches Python Click's behavior.
233    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        // Add deprecation marker
251        if self.config.deprecated.is_some() {
252            var.push('!');
253        }
254
255        // Wrap in brackets if optional
256        if !self.config.required {
257            var = format!("[{}]", var);
258        }
259
260        // Add ellipsis for variadic or count > 1
261        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        // Use metavar if set, otherwise uppercase name
278        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        // Hidden arguments don't appear in help
323        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
338// =============================================================================
339// ArgumentBuilder
340// =============================================================================
341
342/// Builder for creating `Argument` instances.
343///
344/// Use `Argument::new(name)` to create a builder, then chain methods
345/// to configure the argument, and finally call `build()` to create
346/// the `Argument`.
347///
348/// # Example
349///
350/// ```
351/// use click::argument::Argument;
352/// use click::parameter::Nargs;
353///
354/// let arg = Argument::new("files")
355///     .help("Files to process")
356///     .nargs(Nargs::Variadic)
357///     .required(false)
358///     .build();
359/// ```
360pub struct ArgumentBuilder {
361    config: ParameterConfig,
362    default_value: Option<String>,
363    type_converter: Option<Box<dyn AnyTypeConverter>>,
364    /// Track if the user explicitly set required
365    required_explicitly_set: bool,
366    /// Custom shell completion callback
367    shell_complete_callback: Option<ShellCompleteCallback>,
368}
369
370impl ArgumentBuilder {
371    /// Create a new argument builder with the given name.
372    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    /// Set the help text for this argument.
383    pub fn help(mut self, help: &str) -> Self {
384        self.config.help = Some(help.to_string());
385        self
386    }
387
388    /// Set whether this argument is required.
389    ///
390    /// By default, arguments are required unless a default value is provided.
391    pub fn required(mut self, required: bool) -> Self {
392        self.config.required = required;
393        self.required_explicitly_set = true;
394        self
395    }
396
397    /// Set the default value for this argument.
398    ///
399    /// Setting a default value automatically makes the argument optional
400    /// (unless `required(true)` is explicitly called).
401    pub fn default(mut self, value: impl Into<String>) -> Self {
402        self.default_value = Some(value.into());
403        self
404    }
405
406    /// Set an environment variable for this argument.
407    ///
408    /// If the argument is not provided on the command line, the value
409    /// will be read from this environment variable.
410    pub fn envvar(mut self, name: &str) -> Self {
411        self.config.envvar = Some(vec![name.to_string()]);
412        self
413    }
414
415    /// Set multiple environment variables for this argument.
416    ///
417    /// The first non-empty environment variable value is used.
418    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    /// Set how many values this argument consumes.
424    pub fn nargs(mut self, n: Nargs) -> Self {
425        self.config.nargs = n;
426        // Variadic automatically enables multiple
427        if matches!(n, Nargs::Variadic) {
428            self.config.multiple = true;
429        }
430        self
431    }
432
433    /// Set a callback invoked after conversion.
434    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    /// Make this argument variadic (consume all remaining arguments).
447    ///
448    /// Equivalent to `nargs(Nargs::Variadic)`.
449    pub fn multiple(mut self) -> Self {
450        self.config.nargs = Nargs::Variadic;
451        self.config.multiple = true;
452        self
453    }
454
455    /// Set the type converter for this argument.
456    ///
457    /// By default, arguments use `STRING` which passes values through unchanged.
458    /// This method accepts any TypeConverter - the converted value will be stored
459    /// as `Box<dyn Any>` in the context and can be retrieved with `ctx.get_param::<T>()`.
460    ///
461    /// # Example
462    ///
463    /// ```
464    /// use click::argument::Argument;
465    /// use click::types::{INT, FileType};
466    ///
467    /// // Integer argument
468    /// let count = Argument::new("count")
469    ///     .type_(INT)
470    ///     .build();
471    ///
472    /// // File argument (opens the file)
473    /// let input = Argument::new("input")
474    ///     .type_(FileType::new())
475    ///     .build();
476    /// ```
477    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    /// Set a custom shell completion callback for this argument.
487    ///
488    /// The callback receives the current context and the incomplete string
489    /// being typed, and should return a list of completion items.
490    ///
491    /// # Example
492    ///
493    /// ```
494    /// use click::argument::Argument;
495    /// use click::types::CompletionItem;
496    ///
497    /// let arg = Argument::new("filename")
498    ///     .shell_complete(|_ctx, incomplete| {
499    ///         // Return file completions matching the prefix
500    ///         vec![
501    ///             CompletionItem::new(format!("{}.txt", incomplete)),
502    ///             CompletionItem::new(format!("{}.md", incomplete)),
503    ///         ]
504    ///     })
505    ///     .build();
506    /// ```
507    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    /// Set a custom metavar for help text.
516    ///
517    /// By default, the uppercase name is used.
518    pub fn metavar(mut self, metavar: &str) -> Self {
519        self.config.metavar = Some(metavar.to_string());
520        self
521    }
522
523    /// Hide this argument from help output.
524    pub fn hidden(mut self, hidden: bool) -> Self {
525        self.config.hidden = hidden;
526        self
527    }
528
529    /// Set whether this argument is eager (processed before others).
530    pub fn eager(mut self, eager: bool) -> Self {
531        self.config.is_eager = eager;
532        self
533    }
534
535    /// Set whether this argument's value is exposed in ctx.params.
536    pub fn expose_value(mut self, expose: bool) -> Self {
537        self.config.expose_value = expose;
538        self
539    }
540
541    /// Mark this argument as deprecated.
542    pub fn deprecated(mut self, deprecated: bool) -> Self {
543        self.config = self.config.deprecated(deprecated);
544        self
545    }
546
547    /// Mark this argument as deprecated with a custom message.
548    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    /// Build the `Argument`.
554    ///
555    /// This method applies the following defaults:
556    /// - If `required` was not explicitly set:
557    ///   - If a default value is provided, the argument is optional
558    ///   - Otherwise, the argument is required (if nargs > 0)
559    /// - The type converter defaults to `STRING`
560    pub fn build(mut self) -> Argument {
561        // Apply Click's auto-detection logic for required status
562        if !self.required_explicitly_set {
563            if self.default_value.is_some() {
564                // If a default is provided, argument is optional
565                self.config.required = false;
566            } else {
567                // Otherwise, required if nargs > 0
568                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        // Use STRING type by default
577        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// =============================================================================
590// Tests
591// =============================================================================
592
593#[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        // Default: uppercase name
614        let arg = Argument::new("filename").build();
615        assert_eq!(arg.human_readable_name(), "FILENAME");
616
617        // Custom metavar
618        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        // Can still make it required even with a default
633        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        // Optional nargs means the argument itself is not required
671        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        // nargs=0 means not required
680        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    // =========================================================================
802    // Tests for Gap 1: Non-String type converters
803    // =========================================================================
804
805    #[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        // Convert using convert_any
812        let result = arg.convert_any("42");
813        assert!(result.is_ok());
814
815        // Downcast to i64
816        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        // Invalid choice
880        let result = arg.convert_any("csv");
881        assert!(result.is_err());
882    }
883
884    #[test]
885    fn test_argument_string_convert_fallback() {
886        // The convert() method should still work for String types
887        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        // When using convert() on non-String type, it should return an error
898        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    // =========================================================================
905    // Tests for Gap 2: Custom shell_complete callback
906    // =========================================================================
907
908    #[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        // Without custom callback, completions come from the type converter
937        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        // Custom callback should override type's completions
956        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        // Should use custom callback, not Choice completions
967        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        // Default STRING type has no completions
976        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        // Test that the callback receives the context
991        let arg = Argument::new("name")
992            .shell_complete(|ctx, incomplete| {
993                // Use info_name from context in completion
994                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}