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)
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
97/// Custom shell completion callback type for arguments.
98pub type ShellCompleteCallback = Arc<dyn Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync>;
99
100// =============================================================================
101// Argument Struct
102// =============================================================================
103
104/// A positional command-line argument.
105///
106/// Arguments are positional parameters that appear without dashes. Unlike options,
107/// they are required by default and have a single name (no aliases).
108///
109/// # Key Differences from Options
110///
111/// - Arguments are positional (no `--` prefix)
112/// - Required by default (unless a default is provided)
113/// - Single name only (no short/long aliases)
114/// - Name is displayed in uppercase in help text
115///
116/// # Example
117///
118/// ```
119/// use click::argument::Argument;
120/// use click::parameter::Parameter;
121///
122/// // Required argument
123/// let filename = Argument::new("filename").build();
124/// assert!(filename.required());
125///
126/// // Optional argument with default
127/// let output = Argument::new("output")
128///     .default("out.txt")
129///     .build();
130/// assert!(!output.required());
131/// ```
132pub struct Argument {
133    /// Parameter configuration.
134    pub config: ParameterConfig,
135
136    /// The default value (if any).
137    pub default_value: Option<String>,
138
139    /// Type converter for this argument (type-erased to support any return type).
140    type_converter: Box<dyn AnyTypeConverter>,
141
142    /// Custom shell completion callback.
143    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    /// Create a new argument builder with the given name.
162    ///
163    /// # Example
164    ///
165    /// ```
166    /// use click::argument::Argument;
167    ///
168    /// let arg = Argument::builder("filename")
169    ///     .help("The file to process")
170    ///     .required(true)
171    ///     .build();
172    /// ```
173    #[allow(clippy::new_ret_no_self)]
174    pub fn new(name: &str) -> ArgumentBuilder {
175        ArgumentBuilder::new(name)
176    }
177
178    /// Alias for `new()` - create a new argument builder.
179    pub fn builder(name: &str) -> ArgumentBuilder {
180        ArgumentBuilder::new(name)
181    }
182
183    /// Get the type converter for this argument.
184    pub fn type_converter(&self) -> &dyn AnyTypeConverter {
185        self.type_converter.as_ref()
186    }
187
188    /// Convert a string value using this argument's type converter.
189    /// Returns the converted value as a boxed Any type.
190    pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
191        self.type_converter.convert_any(value)
192    }
193
194    /// Convert a string value using this argument's type converter.
195    /// This is a convenience method that attempts to downcast to String.
196    /// For non-String types, use `convert_any` instead.
197    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    /// Get shell completions for this argument.
205    ///
206    /// If a custom shell_complete callback is set, it will be used.
207    /// Otherwise, completions from the type converter are returned.
208    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    /// Check if this argument has a custom shell_complete callback.
217    pub fn has_shell_complete_callback(&self) -> bool {
218        self.shell_complete_callback.is_some()
219    }
220
221    /// Get the default value for this argument.
222    pub fn default_value(&self) -> Option<&str> {
223        self.default_value.as_deref()
224    }
225
226    /// Generate the metavar for this argument (used in help text).
227    ///
228    /// The metavar is formatted according to Click's conventions:
229    /// - Custom metavar if set, otherwise uppercase name
230    /// - Wrapped in `[]` if optional
231    /// - Suffixed with `...` if variadic or multiple
232    /// - Suffixed with `!` if deprecated
233    ///
234    /// Note: Unlike options, arguments use their name (uppercase) by default,
235    /// not the type's metavar. This matches Python Click's behavior.
236    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        // Add deprecation marker
254        if self.config.deprecated.is_some() {
255            var.push('!');
256        }
257
258        // Wrap in brackets if optional
259        if !self.config.required {
260            var = format!("[{}]", var);
261        }
262
263        // Add ellipsis for variadic or count > 1
264        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        // Use metavar if set, otherwise uppercase name
281        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        // Hidden arguments don't appear in help
326        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
341// =============================================================================
342// ArgumentBuilder
343// =============================================================================
344
345/// Builder for creating `Argument` instances.
346///
347/// Use `Argument::new(name)` to create a builder, then chain methods
348/// to configure the argument, and finally call `build()` to create
349/// the `Argument`.
350///
351/// # Example
352///
353/// ```
354/// use click::argument::Argument;
355/// use click::parameter::Nargs;
356///
357/// let arg = Argument::new("files")
358///     .help("Files to process")
359///     .nargs(Nargs::Variadic)
360///     .required(false)
361///     .build();
362/// ```
363pub struct ArgumentBuilder {
364    config: ParameterConfig,
365    default_value: Option<String>,
366    type_converter: Option<Box<dyn AnyTypeConverter>>,
367    /// Track if the user explicitly set required
368    required_explicitly_set: bool,
369    /// Custom shell completion callback
370    shell_complete_callback: Option<ShellCompleteCallback>,
371}
372
373impl ArgumentBuilder {
374    /// Create a new argument builder with the given name.
375    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    /// Set the help text for this argument.
386    pub fn help(mut self, help: &str) -> Self {
387        self.config.help = Some(help.to_string());
388        self
389    }
390
391    /// Set whether this argument is required.
392    ///
393    /// By default, arguments are required unless a default value is provided.
394    pub fn required(mut self, required: bool) -> Self {
395        self.config.required = required;
396        self.required_explicitly_set = true;
397        self
398    }
399
400    /// Set the default value for this argument.
401    ///
402    /// Setting a default value automatically makes the argument optional
403    /// (unless `required(true)` is explicitly called).
404    pub fn default(mut self, value: impl Into<String>) -> Self {
405        self.default_value = Some(value.into());
406        self
407    }
408
409    /// Set an environment variable for this argument.
410    ///
411    /// If the argument is not provided on the command line, the value
412    /// will be read from this environment variable.
413    pub fn envvar(mut self, name: &str) -> Self {
414        self.config.envvar = Some(vec![name.to_string()]);
415        self
416    }
417
418    /// Set multiple environment variables for this argument.
419    ///
420    /// The first non-empty environment variable value is used.
421    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    /// Set how many values this argument consumes.
427    pub fn nargs(mut self, n: Nargs) -> Self {
428        self.config.nargs = n;
429        // Variadic automatically enables multiple
430        if matches!(n, Nargs::Variadic) {
431            self.config.multiple = true;
432        }
433        self
434    }
435
436    /// Set a callback invoked after conversion.
437    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    /// Make this argument variadic (consume all remaining arguments).
453    ///
454    /// Equivalent to `nargs(Nargs::Variadic)`.
455    pub fn multiple(mut self) -> Self {
456        self.config.nargs = Nargs::Variadic;
457        self.config.multiple = true;
458        self
459    }
460
461    /// Set the type converter for this argument.
462    ///
463    /// By default, arguments use `STRING` which passes values through unchanged.
464    /// This method accepts any TypeConverter - the converted value will be stored
465    /// as `Box<dyn Any>` in the context and can be retrieved with `ctx.get_param::<T>()`.
466    ///
467    /// # Example
468    ///
469    /// ```
470    /// use click::argument::Argument;
471    /// use click::types::{INT, FileType};
472    ///
473    /// // Integer argument
474    /// let count = Argument::new("count")
475    ///     .type_(INT)
476    ///     .build();
477    ///
478    /// // File argument (opens the file)
479    /// let input = Argument::new("input")
480    ///     .type_(FileType::new())
481    ///     .build();
482    /// ```
483    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    /// Set a custom shell completion callback for this argument.
493    ///
494    /// The callback receives the current context and the incomplete string
495    /// being typed, and should return a list of completion items.
496    ///
497    /// # Example
498    ///
499    /// ```
500    /// use click::argument::Argument;
501    /// use click::types::CompletionItem;
502    ///
503    /// let arg = Argument::new("filename")
504    ///     .shell_complete(|_ctx, incomplete| {
505    ///         // Return file completions matching the prefix
506    ///         vec![
507    ///             CompletionItem::new(format!("{}.txt", incomplete)),
508    ///             CompletionItem::new(format!("{}.md", incomplete)),
509    ///         ]
510    ///     })
511    ///     .build();
512    /// ```
513    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    /// Set a custom metavar for help text.
522    ///
523    /// By default, the uppercase name is used.
524    pub fn metavar(mut self, metavar: &str) -> Self {
525        self.config.metavar = Some(metavar.to_string());
526        self
527    }
528
529    /// Hide this argument from help output.
530    pub fn hidden(mut self, hidden: bool) -> Self {
531        self.config.hidden = hidden;
532        self
533    }
534
535    /// Set whether this argument is eager (processed before others).
536    pub fn eager(mut self, eager: bool) -> Self {
537        self.config.is_eager = eager;
538        self
539    }
540
541    /// Set whether this argument's value is exposed in ctx.params.
542    pub fn expose_value(mut self, expose: bool) -> Self {
543        self.config.expose_value = expose;
544        self
545    }
546
547    /// Mark this argument as deprecated.
548    pub fn deprecated(mut self, deprecated: bool) -> Self {
549        self.config = self.config.deprecated(deprecated);
550        self
551    }
552
553    /// Mark this argument as deprecated with a custom message.
554    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    /// Build the `Argument`.
560    ///
561    /// This method applies the following defaults:
562    /// - If `required` was not explicitly set:
563    ///   - If a default value is provided, the argument is optional
564    ///   - Otherwise, the argument is required (if nargs > 0)
565    /// - The type converter defaults to `STRING`
566    pub fn build(mut self) -> Argument {
567        // Apply Click's auto-detection logic for required status
568        if !self.required_explicitly_set {
569            if self.default_value.is_some() {
570                // If a default is provided, argument is optional
571                self.config.required = false;
572            } else {
573                // Otherwise, required if nargs > 0
574                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        // Use STRING type by default
583        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// =============================================================================
596// Tests
597// =============================================================================
598
599#[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        // Default: uppercase name
620        let arg = Argument::new("filename").build();
621        assert_eq!(arg.human_readable_name(), "FILENAME");
622
623        // Custom metavar
624        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        // Can still make it required even with a default
639        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        // Optional nargs means the argument itself is not required
677        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        // nargs=0 means not required
686        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    // =========================================================================
808    // Tests for Gap 1: Non-String type converters
809    // =========================================================================
810
811    #[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        // Convert using convert_any
818        let result = arg.convert_any("42");
819        assert!(result.is_ok());
820
821        // Downcast to i64
822        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        // Invalid choice
886        let result = arg.convert_any("csv");
887        assert!(result.is_err());
888    }
889
890    #[test]
891    fn test_argument_string_convert_fallback() {
892        // The convert() method should still work for String types
893        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        // When using convert() on non-String type, it should return an error
904        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    // =========================================================================
911    // Tests for Gap 2: Custom shell_complete callback
912    // =========================================================================
913
914    #[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        // Without custom callback, completions come from the type converter
943        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        // Custom callback should override type's completions
962        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        // Should use custom callback, not Choice completions
971        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        // Default STRING type has no completions
980        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        // Test that the callback receives the context
995        let arg = Argument::new("name")
996            .shell_complete(|ctx, incomplete| {
997                // Use info_name from context in completion
998                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}