Skip to main content

click/
parameter.rs

1//! Base parameter abstraction for click-rs.
2//!
3//! This module provides the `Parameter` trait and common configuration types
4//! for command-line parameters. Options and Arguments implement this trait
5//! separately.
6//!
7//! # Reference
8//!
9//! Based on Python Click's `core.py:Parameter` class (line 2027+).
10
11use std::any::Any;
12use std::fmt;
13use std::sync::Arc;
14
15use crate::context::Context;
16use crate::error::ClickError;
17
18// =============================================================================
19// Nargs Enum
20// =============================================================================
21
22/// Specifies how many arguments a parameter consumes.
23///
24/// This enum corresponds to Python Click's `nargs` parameter:
25/// - `Count(1)` is the default (single value)
26/// - `Variadic` corresponds to `nargs=-1`
27/// - `Optional` corresponds to `nargs=?` (zero or one value)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum Nargs {
30    /// Exactly N values (default is 1).
31    Count(usize),
32    /// Zero or more values (Python's `nargs=-1`).
33    /// All remaining arguments are collected.
34    Variadic,
35    /// Optional single value (Python's `nargs=?`).
36    /// Zero or one value; if not provided, uses the default.
37    Optional,
38}
39
40impl Default for Nargs {
41    fn default() -> Self {
42        Nargs::Count(1)
43    }
44}
45
46impl Nargs {
47    /// Returns `true` if this nargs expects exactly one value.
48    pub fn is_single(&self) -> bool {
49        matches!(self, Nargs::Count(1))
50    }
51
52    /// Returns `true` if this nargs can accept multiple values.
53    pub fn is_multi(&self) -> bool {
54        match self {
55            Nargs::Variadic => true,
56            Nargs::Count(n) => *n > 1,
57            Nargs::Optional => false,
58        }
59    }
60
61    /// Returns `true` if this nargs accepts zero or more values.
62    pub fn is_variadic(&self) -> bool {
63        matches!(self, Nargs::Variadic)
64    }
65
66    /// Returns `true` if this nargs is optional (zero or one).
67    pub fn is_optional(&self) -> bool {
68        matches!(self, Nargs::Optional)
69    }
70
71    /// Returns the exact count if this is `Count(n)`, otherwise `None`.
72    pub fn count(&self) -> Option<usize> {
73        match self {
74            Nargs::Count(n) => Some(*n),
75            _ => None,
76        }
77    }
78}
79
80impl fmt::Display for Nargs {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Nargs::Count(1) => write!(f, "1"),
84            Nargs::Count(n) => write!(f, "{}", n),
85            Nargs::Variadic => write!(f, "-1"),
86            Nargs::Optional => write!(f, "?"),
87        }
88    }
89}
90
91// =============================================================================
92// Parameter Trait
93// =============================================================================
94
95/// A trait for command-line parameters (options and arguments).
96///
97/// This trait defines the common interface for all parameter types.
98/// Options and Arguments implement this trait with their specific behaviors.
99pub trait Parameter: Send + Sync + fmt::Debug {
100    /// The primary name of the parameter.
101    ///
102    /// For options, this is typically the long option name without dashes.
103    /// For arguments, this is the argument name.
104    fn name(&self) -> &str;
105
106    /// Human-readable name for errors and help text.
107    ///
108    /// For options, this is usually the option flags (e.g., "--name / -n").
109    /// For arguments, this is typically the metavar in uppercase.
110    fn human_readable_name(&self) -> String;
111
112    /// Number of arguments this parameter consumes.
113    fn nargs(&self) -> Nargs;
114
115    /// Whether this parameter can be specified multiple times.
116    ///
117    /// When true, the parameter collects values into a list/tuple.
118    fn multiple(&self) -> bool;
119
120    /// Whether this parameter should be processed before others.
121    ///
122    /// Eager parameters (like `--help` and `--version`) are processed
123    /// first and can short-circuit command execution.
124    fn is_eager(&self) -> bool;
125
126    /// Whether this parameter's value should be exposed in `ctx.params`.
127    ///
128    /// When false, the parameter is still processed but its value
129    /// is not stored in the context parameters.
130    fn expose_value(&self) -> bool;
131
132    /// Whether this parameter is required.
133    ///
134    /// Required parameters must be provided via CLI, environment, or default.
135    fn required(&self) -> bool;
136
137    /// Get environment variable name(s) for this parameter.
138    ///
139    /// Returns `None` if no environment variables are configured.
140    /// Multiple environment variables can be specified; the first
141    /// non-empty value is used.
142    fn envvar(&self) -> Option<&[String]>;
143
144    /// Get help text for this parameter.
145    fn help(&self) -> Option<&str>;
146
147    /// Whether this parameter is hidden from help.
148    fn hidden(&self) -> bool;
149
150    /// Get the metavar for help text (e.g., "FILE", "TEXT").
151    ///
152    /// If not explicitly set, the type's metavar is used.
153    fn get_metavar(&self) -> Option<String>;
154
155    /// Generate help record for formatting.
156    ///
157    /// Returns a tuple of (option_string, help_string) for help display,
158    /// or `None` if this parameter should not appear in help.
159    fn get_help_record(&self) -> Option<(String, String)>;
160
161    /// Get the parameter type name (for error messages).
162    fn param_type_name(&self) -> &str {
163        "parameter"
164    }
165}
166
167// =============================================================================
168// ParameterConfig Struct
169// =============================================================================
170
171/// Callback for parameter value processing.
172///
173/// Receives the current context, the parameter, and the converted value.
174/// Returns the (possibly transformed) value or an error.
175pub type ParameterCallback = Arc<
176    dyn Fn(&Context, &dyn Parameter, Arc<dyn Any + Send + Sync>)
177            -> Result<Arc<dyn Any + Send + Sync>, ClickError>
178        + Send
179        + Sync,
180>;
181
182/// Common configuration for all parameter types.
183///
184/// This struct holds the shared settings between Options and Arguments.
185/// It uses a builder pattern for convenient construction.
186#[derive(Clone)]
187pub struct ParameterConfig {
188    /// The parameter name.
189    pub name: String,
190    /// Number of arguments consumed.
191    pub nargs: Nargs,
192    /// Whether the parameter can be specified multiple times.
193    pub multiple: bool,
194    /// Whether this parameter should be processed before others.
195    pub is_eager: bool,
196    /// Whether this parameter's value is exposed in ctx.params.
197    pub expose_value: bool,
198    /// Whether this parameter is required.
199    pub required: bool,
200    /// Environment variable name(s) for this parameter.
201    pub envvar: Option<Vec<String>>,
202    /// Help text for this parameter.
203    pub help: Option<String>,
204    /// Whether this parameter is hidden from help.
205    pub hidden: bool,
206    /// Custom metavar for help text.
207    pub metavar: Option<String>,
208    /// Whether this parameter is deprecated.
209    pub deprecated: Option<DeprecationInfo>,
210    /// Optional callback invoked after conversion.
211    pub callback: Option<ParameterCallback>,
212}
213
214impl fmt::Debug for ParameterConfig {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        f.debug_struct("ParameterConfig")
217            .field("name", &self.name)
218            .field("nargs", &self.nargs)
219            .field("multiple", &self.multiple)
220            .field("is_eager", &self.is_eager)
221            .field("expose_value", &self.expose_value)
222            .field("required", &self.required)
223            .field("envvar", &self.envvar)
224            .field("help", &self.help)
225            .field("hidden", &self.hidden)
226            .field("metavar", &self.metavar)
227            .field("deprecated", &self.deprecated)
228            .field("has_callback", &self.callback.is_some())
229            .finish()
230    }
231}
232
233/// Information about a deprecated parameter.
234#[derive(Debug, Clone, Default)]
235pub struct DeprecationInfo {
236    /// Custom deprecation message (if not using the default).
237    pub message: Option<String>,
238}
239
240impl DeprecationInfo {
241    /// Create a new deprecation info with default message.
242    pub fn new() -> Self {
243        Self::default()
244    }
245
246    /// Create a new deprecation info with a custom message.
247    pub fn with_message(message: impl Into<String>) -> Self {
248        Self {
249            message: Some(message.into()),
250        }
251    }
252}
253
254impl Default for ParameterConfig {
255    fn default() -> Self {
256        Self {
257            name: String::new(),
258            nargs: Nargs::default(),
259            multiple: false,
260            is_eager: false,
261            expose_value: true,
262            required: false,
263            envvar: None,
264            help: None,
265            hidden: false,
266            metavar: None,
267            deprecated: None,
268            callback: None,
269        }
270    }
271}
272
273impl ParameterConfig {
274    /// Create a new parameter configuration with the given name.
275    pub fn new(name: impl Into<String>) -> Self {
276        Self {
277            name: name.into(),
278            ..Default::default()
279        }
280    }
281
282    /// Set the number of arguments consumed.
283    pub fn nargs(mut self, nargs: Nargs) -> Self {
284        self.nargs = nargs;
285        self
286    }
287
288    /// Set whether the parameter can be specified multiple times.
289    pub fn multiple(mut self, multiple: bool) -> Self {
290        self.multiple = multiple;
291        self
292    }
293
294    /// Set whether this parameter should be processed before others.
295    pub fn eager(mut self, eager: bool) -> Self {
296        self.is_eager = eager;
297        self
298    }
299
300    /// Set whether this parameter's value is exposed in ctx.params.
301    pub fn expose_value(mut self, expose: bool) -> Self {
302        self.expose_value = expose;
303        self
304    }
305
306    /// Set whether this parameter is required.
307    pub fn required(mut self, required: bool) -> Self {
308        self.required = required;
309        self
310    }
311
312    /// Set a single environment variable for this parameter.
313    pub fn envvar(mut self, var: impl Into<String>) -> Self {
314        self.envvar = Some(vec![var.into()]);
315        self
316    }
317
318    /// Set multiple environment variables for this parameter.
319    pub fn envvars(mut self, vars: impl IntoIterator<Item = impl Into<String>>) -> Self {
320        self.envvar = Some(vars.into_iter().map(|v| v.into()).collect());
321        self
322    }
323
324    /// Set the help text.
325    pub fn help(mut self, help: impl Into<String>) -> Self {
326        self.help = Some(help.into());
327        self
328    }
329
330    /// Set whether this parameter is hidden from help.
331    pub fn hidden(mut self, hidden: bool) -> Self {
332        self.hidden = hidden;
333        self
334    }
335
336    /// Set a custom metavar for help text.
337    pub fn metavar(mut self, metavar: impl Into<String>) -> Self {
338        self.metavar = Some(metavar.into());
339        self
340    }
341
342    /// Set a callback invoked after conversion.
343    pub fn callback(mut self, callback: ParameterCallback) -> Self {
344        self.callback = Some(callback);
345        self
346    }
347
348    /// Mark this parameter as deprecated.
349    pub fn deprecated(mut self, deprecated: bool) -> Self {
350        self.deprecated = if deprecated {
351            Some(DeprecationInfo::default())
352        } else {
353            None
354        };
355        self
356    }
357
358    /// Mark this parameter as deprecated with a custom message.
359    pub fn deprecated_with_message(mut self, message: impl Into<String>) -> Self {
360        self.deprecated = Some(DeprecationInfo::with_message(message));
361        self
362    }
363
364    /// Validate the configuration.
365    ///
366    /// Returns an error if the configuration is invalid.
367    pub fn validate(&self) -> Result<(), ClickError> {
368        // A deprecated parameter cannot be required
369        if self.deprecated.is_some() && self.required {
370            return Err(ClickError::usage(format!(
371                "The parameter '{}' is deprecated and required. \
372                 A deprecated parameter cannot be required.",
373                self.name
374            )));
375        }
376        Ok(())
377    }
378}
379
380// =============================================================================
381// Tests
382// =============================================================================
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_nargs_default() {
390        let nargs = Nargs::default();
391        assert_eq!(nargs, Nargs::Count(1));
392        assert!(nargs.is_single());
393        assert!(!nargs.is_multi());
394        assert!(!nargs.is_variadic());
395        assert!(!nargs.is_optional());
396    }
397
398    #[test]
399    fn test_nargs_count() {
400        let nargs = Nargs::Count(3);
401        assert!(!nargs.is_single());
402        assert!(nargs.is_multi());
403        assert!(!nargs.is_variadic());
404        assert!(!nargs.is_optional());
405        assert_eq!(nargs.count(), Some(3));
406    }
407
408    #[test]
409    fn test_nargs_variadic() {
410        let nargs = Nargs::Variadic;
411        assert!(!nargs.is_single());
412        assert!(nargs.is_multi());
413        assert!(nargs.is_variadic());
414        assert!(!nargs.is_optional());
415        assert_eq!(nargs.count(), None);
416    }
417
418    #[test]
419    fn test_nargs_optional() {
420        let nargs = Nargs::Optional;
421        assert!(!nargs.is_single());
422        assert!(!nargs.is_multi());
423        assert!(!nargs.is_variadic());
424        assert!(nargs.is_optional());
425        assert_eq!(nargs.count(), None);
426    }
427
428    #[test]
429    fn test_nargs_display() {
430        assert_eq!(Nargs::Count(1).to_string(), "1");
431        assert_eq!(Nargs::Count(3).to_string(), "3");
432        assert_eq!(Nargs::Variadic.to_string(), "-1");
433        assert_eq!(Nargs::Optional.to_string(), "?");
434    }
435
436    #[test]
437    fn test_parameter_config_builder() {
438        let config = ParameterConfig::new("name")
439            .nargs(Nargs::Count(2))
440            .multiple(true)
441            .eager(true)
442            .expose_value(false)
443            .required(true)
444            .envvar("MY_VAR")
445            .help("Help text")
446            .hidden(false)
447            .metavar("VALUE");
448
449        assert_eq!(config.name, "name");
450        assert_eq!(config.nargs, Nargs::Count(2));
451        assert!(config.multiple);
452        assert!(config.is_eager);
453        assert!(!config.expose_value);
454        assert!(config.required);
455        assert_eq!(config.envvar, Some(vec!["MY_VAR".to_string()]));
456        assert_eq!(config.help, Some("Help text".to_string()));
457        assert!(!config.hidden);
458        assert_eq!(config.metavar, Some("VALUE".to_string()));
459    }
460
461    #[test]
462    fn test_parameter_config_envvars() {
463        let config = ParameterConfig::new("name").envvars(["VAR1", "VAR2", "VAR3"]);
464
465        assert_eq!(
466            config.envvar,
467            Some(vec![
468                "VAR1".to_string(),
469                "VAR2".to_string(),
470                "VAR3".to_string()
471            ])
472        );
473    }
474
475    #[test]
476    fn test_parameter_config_default() {
477        let config = ParameterConfig::default();
478
479        assert_eq!(config.name, "");
480        assert_eq!(config.nargs, Nargs::Count(1));
481        assert!(!config.multiple);
482        assert!(!config.is_eager);
483        assert!(config.expose_value);
484        assert!(!config.required);
485        assert!(config.envvar.is_none());
486        assert!(config.help.is_none());
487        assert!(!config.hidden);
488        assert!(config.metavar.is_none());
489        assert!(config.deprecated.is_none());
490    }
491
492    #[test]
493    fn test_parameter_config_deprecated() {
494        let config = ParameterConfig::new("old_option").deprecated(true);
495        assert!(config.deprecated.is_some());
496        assert!(config.deprecated.as_ref().unwrap().message.is_none());
497
498        let config =
499            ParameterConfig::new("old_option").deprecated_with_message("Use --new-option instead");
500        assert!(config.deprecated.is_some());
501        assert_eq!(
502            config.deprecated.as_ref().unwrap().message,
503            Some("Use --new-option instead".to_string())
504        );
505    }
506
507    #[test]
508    fn test_parameter_config_validate_deprecated_required() {
509        let config = ParameterConfig::new("option")
510            .deprecated(true)
511            .required(true);
512
513        let result = config.validate();
514        assert!(result.is_err());
515        let err = result.unwrap_err();
516        assert!(err.to_string().contains("deprecated"));
517        assert!(err.to_string().contains("required"));
518    }
519
520    #[test]
521    fn test_parameter_config_validate_ok() {
522        let config = ParameterConfig::new("option").required(true);
523
524        assert!(config.validate().is_ok());
525
526        let config = ParameterConfig::new("option").deprecated(true);
527
528        assert!(config.validate().is_ok());
529    }
530
531    #[test]
532    fn test_deprecation_info() {
533        let info = DeprecationInfo::new();
534        assert!(info.message.is_none());
535
536        let info = DeprecationInfo::with_message("Custom message");
537        assert_eq!(info.message, Some("Custom message".to_string()));
538    }
539}