Skip to main content

click/
types.rs

1//! Parameter types for click-rs.
2//!
3//! This module provides the `TypeConverter` trait and all built-in types for
4//! converting and validating command-line arguments.
5
6use std::fmt;
7use std::fs::{self, File as StdFile, OpenOptions};
8use std::io::{self, Read, Write};
9use std::path::{Path as StdPath, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use chrono::{DateTime as ChronoDateTime, NaiveDate, NaiveDateTime, Utc};
13use uuid::Uuid;
14
15/// Format a float value like Python does (always show decimal point for whole numbers).
16fn format_float(value: f64) -> String {
17    if value.fract() == 0.0 && value.is_finite() {
18        format!("{:.1}", value)
19    } else {
20        format!("{}", value)
21    }
22}
23
24// =============================================================================
25// TypeConverter Trait
26// =============================================================================
27
28/// Trait for parameter types that convert and validate command-line values.
29///
30/// Each type must define how to convert a string value from the command line
31/// into the appropriate Rust type.
32pub trait TypeConverter: fmt::Debug + Send + Sync {
33    /// The Rust type that this parameter type converts to.
34    type Value;
35
36    /// Returns the descriptive name of this type (used in error messages).
37    fn name(&self) -> &str;
38
39    /// Convert a string value to the target type.
40    ///
41    /// Returns an error message if conversion fails.
42    fn convert(&self, value: &str) -> Result<Self::Value, String>;
43
44    /// Returns the metavar for this type (used in help text).
45    ///
46    /// For example, `INT` might return `"INTEGER"`.
47    fn get_metavar(&self) -> Option<String> {
48        None
49    }
50
51    /// Returns an optional message when a required value is missing.
52    fn get_missing_message(&self) -> Option<String> {
53        None
54    }
55
56    /// Split an environment variable value into multiple values.
57    ///
58    /// By default, splits on whitespace. Path-based types override this
59    /// to split on the platform's path separator.
60    fn split_envvar_value(&self, value: &str) -> Vec<String> {
61        value.split_whitespace().map(|s| s.to_string()).collect()
62    }
63
64    /// Returns shell completion items for the given incomplete value.
65    ///
66    /// Most types return an empty list; types like `Choice` and `Path`
67    /// can provide completions.
68    fn shell_complete(&self, _incomplete: &str) -> Vec<CompletionItem> {
69        Vec::new()
70    }
71
72    /// Whether this type is a composite type (like Tuple).
73    fn is_composite(&self) -> bool {
74        false
75    }
76
77    /// The arity (number of values consumed) for composite types.
78    fn arity(&self) -> usize {
79        1
80    }
81}
82
83/// A shell completion item.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct CompletionItem {
86    /// The completion value.
87    pub value: String,
88    /// The type of completion (e.g., "file", "dir", "plain").
89    pub completion_type: String,
90    /// Optional help text for the completion.
91    pub help: Option<String>,
92}
93
94impl CompletionItem {
95    /// Create a new completion item with the given value.
96    pub fn new(value: impl Into<String>) -> Self {
97        Self {
98            value: value.into(),
99            completion_type: "plain".to_string(),
100            help: None,
101        }
102    }
103
104    /// Create a new completion item with a specific type.
105    pub fn with_type(value: impl Into<String>, completion_type: impl Into<String>) -> Self {
106        Self {
107            value: value.into(),
108            completion_type: completion_type.into(),
109            help: None,
110        }
111    }
112
113    /// Add help text to this completion item.
114    pub fn with_help(mut self, help: impl Into<String>) -> Self {
115        self.help = Some(help.into());
116        self
117    }
118}
119
120// =============================================================================
121// STRING Type
122// =============================================================================
123
124/// A string parameter type (the default).
125///
126/// Passes through string values unchanged.
127#[derive(Debug, Clone, Copy, Default)]
128pub struct StringType;
129
130impl TypeConverter for StringType {
131    type Value = String;
132
133    fn name(&self) -> &str {
134        "TEXT"
135    }
136
137    fn convert(&self, value: &str) -> Result<Self::Value, String> {
138        Ok(value.to_string())
139    }
140
141    fn get_metavar(&self) -> Option<String> {
142        Some("TEXT".to_string())
143    }
144}
145
146/// Singleton instance for string type.
147pub const STRING: StringType = StringType;
148
149// =============================================================================
150// INT Type
151// =============================================================================
152
153/// An integer parameter type.
154#[derive(Debug, Clone, Copy, Default)]
155pub struct IntType;
156
157impl TypeConverter for IntType {
158    type Value = i64;
159
160    fn name(&self) -> &str {
161        "INTEGER"
162    }
163
164    fn convert(&self, value: &str) -> Result<Self::Value, String> {
165        value
166            .trim()
167            .parse::<i64>()
168            .map_err(|_| format!("'{}' is not a valid integer.", value))
169    }
170
171    fn get_metavar(&self) -> Option<String> {
172        Some("INTEGER".to_string())
173    }
174}
175
176/// Singleton instance for integer type.
177pub const INT: IntType = IntType;
178
179// =============================================================================
180// FLOAT Type
181// =============================================================================
182
183/// A floating-point parameter type.
184#[derive(Debug, Clone, Copy, Default)]
185pub struct FloatType;
186
187impl TypeConverter for FloatType {
188    type Value = f64;
189
190    fn name(&self) -> &str {
191        "FLOAT"
192    }
193
194    fn convert(&self, value: &str) -> Result<Self::Value, String> {
195        value
196            .trim()
197            .parse::<f64>()
198            .map_err(|_| format!("'{}' is not a valid float.", value))
199    }
200
201    fn get_metavar(&self) -> Option<String> {
202        Some("FLOAT".to_string())
203    }
204}
205
206/// Singleton instance for float type.
207pub const FLOAT: FloatType = FloatType;
208
209// =============================================================================
210// BOOL Type
211// =============================================================================
212
213/// A boolean parameter type.
214///
215/// Accepts various string representations of boolean values:
216/// - True: "1", "true", "yes", "on", "t", "y"
217/// - False: "0", "false", "no", "off", "f", "n", ""
218#[derive(Debug, Clone, Copy, Default)]
219pub struct BoolType;
220
221impl BoolType {
222    /// Convert a string to a boolean, returning None if not recognized.
223    pub fn str_to_bool(value: &str) -> Option<bool> {
224        match value.trim().to_lowercase().as_str() {
225            "1" | "true" | "yes" | "on" | "t" | "y" => Some(true),
226            "0" | "false" | "no" | "off" | "f" | "n" | "" => Some(false),
227            _ => None,
228        }
229    }
230
231    /// List of recognized boolean string values (includes empty string at start).
232    pub const BOOL_STATES: &'static [&'static str] = &[
233        "", "0", "1", "f", "false", "n", "no", "off", "on", "t", "true", "y", "yes",
234    ];
235}
236
237impl TypeConverter for BoolType {
238    type Value = bool;
239
240    fn name(&self) -> &str {
241        "BOOLEAN"
242    }
243
244    fn convert(&self, value: &str) -> Result<Self::Value, String> {
245        Self::str_to_bool(value).ok_or_else(|| {
246            format!(
247                "'{}' is not a valid boolean. Recognized values: {}",
248                value,
249                Self::BOOL_STATES.join(", ")
250            )
251        })
252    }
253
254    fn get_metavar(&self) -> Option<String> {
255        Some("BOOLEAN".to_string())
256    }
257}
258
259/// Singleton instance for boolean type.
260pub const BOOL: BoolType = BoolType;
261
262// =============================================================================
263// UUID Type
264// =============================================================================
265
266/// A UUID parameter type.
267#[derive(Debug, Clone, Copy, Default)]
268pub struct UuidType;
269
270impl TypeConverter for UuidType {
271    type Value = Uuid;
272
273    fn name(&self) -> &str {
274        "UUID"
275    }
276
277    fn convert(&self, value: &str) -> Result<Self::Value, String> {
278        Uuid::parse_str(value.trim()).map_err(|_| format!("'{}' is not a valid UUID", value))
279    }
280
281    fn get_metavar(&self) -> Option<String> {
282        Some("UUID".to_string())
283    }
284}
285
286/// Singleton instance for UUID type.
287pub const UUID: UuidType = UuidType;
288
289// =============================================================================
290// UNPROCESSED Type
291// =============================================================================
292
293/// A type that passes through values without any processing.
294///
295/// This is useful when you want to defer processing to a callback
296/// or when working with raw byte paths.
297#[derive(Debug, Clone, Copy, Default)]
298pub struct UnprocessedType;
299
300impl TypeConverter for UnprocessedType {
301    type Value = String;
302
303    fn name(&self) -> &str {
304        "TEXT"
305    }
306
307    fn convert(&self, value: &str) -> Result<Self::Value, String> {
308        Ok(value.to_string())
309    }
310}
311
312/// Singleton instance for unprocessed type.
313pub const UNPROCESSED: UnprocessedType = UnprocessedType;
314
315// =============================================================================
316// IntRange Type
317// =============================================================================
318
319/// An integer type restricted to a range of values.
320///
321/// If `min` or `max` are `None`, the range is unbounded in that direction.
322/// If `clamp` is true, out-of-range values are clamped to the boundary
323/// instead of producing an error.
324#[derive(Debug, Clone, Copy)]
325pub struct IntRange {
326    /// Minimum allowed value (inclusive unless `min_open` is true).
327    pub min: Option<i64>,
328    /// Maximum allowed value (inclusive unless `max_open` is true).
329    pub max: Option<i64>,
330    /// If true, the minimum bound is exclusive (value must be > min).
331    pub min_open: bool,
332    /// If true, the maximum bound is exclusive (value must be < max).
333    pub max_open: bool,
334    /// If true, clamp out-of-range values instead of failing.
335    pub clamp: bool,
336}
337
338impl Default for IntRange {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344impl IntRange {
345    /// Create a new unbounded integer range.
346    pub const fn new() -> Self {
347        Self {
348            min: None,
349            max: None,
350            min_open: false,
351            max_open: false,
352            clamp: false,
353        }
354    }
355
356    /// Set the minimum value (inclusive).
357    pub const fn min(mut self, min: i64) -> Self {
358        self.min = Some(min);
359        self
360    }
361
362    /// Set the maximum value (inclusive).
363    pub const fn max(mut self, max: i64) -> Self {
364        self.max = Some(max);
365        self
366    }
367
368    /// Set both minimum and maximum values.
369    pub const fn range(mut self, min: i64, max: i64) -> Self {
370        self.min = Some(min);
371        self.max = Some(max);
372        self
373    }
374
375    /// Make the minimum bound exclusive (value must be > min).
376    pub const fn min_open(mut self, open: bool) -> Self {
377        self.min_open = open;
378        self
379    }
380
381    /// Make the maximum bound exclusive (value must be < max).
382    pub const fn max_open(mut self, open: bool) -> Self {
383        self.max_open = open;
384        self
385    }
386
387    /// Enable clamping of out-of-range values.
388    pub const fn clamp(mut self, clamp: bool) -> Self {
389        self.clamp = clamp;
390        self
391    }
392
393    /// Describe the range for error messages.
394    fn describe_range(&self) -> String {
395        match (self.min, self.max) {
396            (None, None) => "any integer".to_string(),
397            (Some(min), None) => {
398                let op = if self.min_open { ">" } else { ">=" };
399                format!("x{}{}", op, min)
400            }
401            (None, Some(max)) => {
402                let op = if self.max_open { "<" } else { "<=" };
403                format!("x{}{}", op, max)
404            }
405            (Some(min), Some(max)) => {
406                let lop = if self.min_open { "<" } else { "<=" };
407                let rop = if self.max_open { "<" } else { "<=" };
408                format!("{}{lop}x{rop}{}", min, max)
409            }
410        }
411    }
412
413    /// Clamp a value to the range bounds.
414    fn clamp_value(&self, value: i64) -> i64 {
415        let mut result = value;
416        if let Some(min) = self.min {
417            // Use saturating_add to avoid overflow when min == i64::MAX and min_open
418            let effective_min = if self.min_open {
419                min.saturating_add(1)
420            } else {
421                min
422            };
423            if result < effective_min {
424                result = effective_min;
425            }
426        }
427        if let Some(max) = self.max {
428            // Use saturating_sub to avoid underflow when max == i64::MIN and max_open
429            let effective_max = if self.max_open {
430                max.saturating_sub(1)
431            } else {
432                max
433            };
434            if result > effective_max {
435                result = effective_max;
436            }
437        }
438        result
439    }
440}
441
442impl TypeConverter for IntRange {
443    type Value = i64;
444
445    fn name(&self) -> &str {
446        "INTEGER RANGE"
447    }
448
449    fn convert(&self, value: &str) -> Result<Self::Value, String> {
450        let parsed: i64 = value
451            .trim()
452            .parse()
453            .map_err(|_| format!("'{}' is not a valid integer range.", value))?;
454
455        // Check if value is below minimum
456        let lt_min = self.min.is_some_and(|min| {
457            if self.min_open {
458                parsed <= min
459            } else {
460                parsed < min
461            }
462        });
463
464        // Check if value is above maximum
465        let gt_max = self.max.is_some_and(|max| {
466            if self.max_open {
467                parsed >= max
468            } else {
469                parsed > max
470            }
471        });
472
473        if self.clamp && (lt_min || gt_max) {
474            return Ok(self.clamp_value(parsed));
475        }
476
477        if lt_min || gt_max {
478            return Err(format!(
479                "{} is not in the range {}.",
480                parsed,
481                self.describe_range()
482            ));
483        }
484
485        Ok(parsed)
486    }
487
488    fn get_metavar(&self) -> Option<String> {
489        Some(format!("INTEGER RANGE {}", self.describe_range()))
490    }
491}
492
493// =============================================================================
494// FloatRange Type
495// =============================================================================
496
497/// A floating-point type restricted to a range of values.
498///
499/// If `min` or `max` are `None`, the range is unbounded in that direction.
500/// If `clamp` is true, out-of-range values are clamped to the boundary
501/// instead of producing an error. Note: clamping is not supported with
502/// open bounds.
503#[derive(Debug, Clone, Copy)]
504pub struct FloatRange {
505    /// Minimum allowed value (inclusive unless `min_open` is true).
506    pub min: Option<f64>,
507    /// Maximum allowed value (inclusive unless `max_open` is true).
508    pub max: Option<f64>,
509    /// If true, the minimum bound is exclusive (value must be > min).
510    pub min_open: bool,
511    /// If true, the maximum bound is exclusive (value must be < max).
512    pub max_open: bool,
513    /// If true, clamp out-of-range values instead of failing.
514    /// Not supported with open bounds.
515    pub clamp: bool,
516}
517
518impl Default for FloatRange {
519    fn default() -> Self {
520        Self::new()
521    }
522}
523
524impl FloatRange {
525    /// Create a new unbounded float range.
526    pub const fn new() -> Self {
527        Self {
528            min: None,
529            max: None,
530            min_open: false,
531            max_open: false,
532            clamp: false,
533        }
534    }
535
536    /// Set the minimum value (inclusive).
537    pub fn min(mut self, min: f64) -> Self {
538        self.min = Some(min);
539        self
540    }
541
542    /// Set the maximum value (inclusive).
543    pub fn max(mut self, max: f64) -> Self {
544        self.max = Some(max);
545        self
546    }
547
548    /// Set both minimum and maximum values.
549    pub fn range(mut self, min: f64, max: f64) -> Self {
550        self.min = Some(min);
551        self.max = Some(max);
552        self
553    }
554
555    /// Make the minimum bound exclusive (value must be > min).
556    ///
557    /// # Panics
558    /// Panics if clamping is enabled, as clamping is not supported for open bounds.
559    pub fn min_open(mut self, open: bool) -> Self {
560        if open && self.clamp {
561            panic!("Clamping is not supported for open bounds");
562        }
563        self.min_open = open;
564        self
565    }
566
567    /// Make the maximum bound exclusive (value must be < max).
568    ///
569    /// # Panics
570    /// Panics if clamping is enabled, as clamping is not supported for open bounds.
571    pub fn max_open(mut self, open: bool) -> Self {
572        if open && self.clamp {
573            panic!("Clamping is not supported for open bounds");
574        }
575        self.max_open = open;
576        self
577    }
578
579    /// Enable clamping of out-of-range values.
580    ///
581    /// # Panics
582    /// Panics if either bound is open, as clamping is not supported for open bounds.
583    pub fn clamp(mut self, clamp: bool) -> Self {
584        if clamp && (self.min_open || self.max_open) {
585            panic!("Clamping is not supported for open bounds");
586        }
587        self.clamp = clamp;
588        self
589    }
590
591    /// Describe the range for error messages.
592    fn describe_range(&self) -> String {
593        match (self.min, self.max) {
594            (None, None) => "any float".to_string(),
595            (Some(min), None) => {
596                let op = if self.min_open { ">" } else { ">=" };
597                format!("x{}{}", op, format_float(min))
598            }
599            (None, Some(max)) => {
600                let op = if self.max_open { "<" } else { "<=" };
601                format!("x{}{}", op, format_float(max))
602            }
603            (Some(min), Some(max)) => {
604                let lop = if self.min_open { "<" } else { "<=" };
605                let rop = if self.max_open { "<" } else { "<=" };
606                format!("{}{lop}x{rop}{}", format_float(min), format_float(max))
607            }
608        }
609    }
610}
611
612impl TypeConverter for FloatRange {
613    type Value = f64;
614
615    fn name(&self) -> &str {
616        "FLOAT RANGE"
617    }
618
619    fn convert(&self, value: &str) -> Result<Self::Value, String> {
620        let parsed: f64 = value
621            .trim()
622            .parse()
623            .map_err(|_| format!("'{}' is not a valid float range.", value))?;
624
625        // Check if value is below minimum
626        let lt_min = self.min.is_some_and(|min| {
627            if self.min_open {
628                parsed <= min
629            } else {
630                parsed < min
631            }
632        });
633
634        // Check if value is above maximum
635        let gt_max = self.max.is_some_and(|max| {
636            if self.max_open {
637                parsed >= max
638            } else {
639                parsed > max
640            }
641        });
642
643        if self.clamp {
644            if lt_min {
645                return Ok(self.min.unwrap());
646            }
647            if gt_max {
648                return Ok(self.max.unwrap());
649            }
650        }
651
652        if lt_min || gt_max {
653            return Err(format!(
654                "{} is not in the range {}.",
655                format_float(parsed),
656                self.describe_range()
657            ));
658        }
659
660        Ok(parsed)
661    }
662
663    fn get_metavar(&self) -> Option<String> {
664        Some(format!("FLOAT RANGE {}", self.describe_range()))
665    }
666}
667
668// =============================================================================
669// DateTime Type
670// =============================================================================
671
672/// A datetime parameter type that parses ISO 8601 format strings.
673///
674/// By default, attempts to parse using these formats (in order):
675/// - `%Y-%m-%d` (date only)
676/// - `%Y-%m-%dT%H:%M:%S` (datetime with T separator)
677/// - `%Y-%m-%d %H:%M:%S` (datetime with space separator)
678///
679/// Custom formats can be specified using the `formats` field.
680#[derive(Debug, Clone)]
681pub struct DateTimeType {
682    /// The datetime formats to try, in order.
683    pub formats: Vec<String>,
684}
685
686impl Default for DateTimeType {
687    fn default() -> Self {
688        Self::new()
689    }
690}
691
692impl DateTimeType {
693    /// Default datetime formats.
694    pub const DEFAULT_FORMATS: &'static [&'static str] = &[
695        "%Y-%m-%d",
696        "%Y-%m-%dT%H:%M:%S",
697        "%Y-%m-%d %H:%M:%S",
698        "%Y-%m-%dT%H:%M:%S%.f",
699        "%Y-%m-%dT%H:%M:%S%z",
700        "%Y-%m-%dT%H:%M:%S%.f%z",
701    ];
702
703    /// Create a new datetime type with default formats.
704    pub fn new() -> Self {
705        Self {
706            formats: Self::DEFAULT_FORMATS
707                .iter()
708                .map(|s| s.to_string())
709                .collect(),
710        }
711    }
712
713    /// Create a datetime type with custom formats.
714    pub fn with_formats(formats: impl IntoIterator<Item = impl Into<String>>) -> Self {
715        Self {
716            formats: formats.into_iter().map(|s| s.into()).collect(),
717        }
718    }
719}
720
721impl TypeConverter for DateTimeType {
722    type Value = NaiveDateTime;
723
724    fn name(&self) -> &str {
725        "DATETIME"
726    }
727
728    fn convert(&self, value: &str) -> Result<Self::Value, String> {
729        let value = value.trim();
730
731        // Try each format in order
732        for format in &self.formats {
733            // Try parsing as NaiveDateTime first
734            if let Ok(dt) = NaiveDateTime::parse_from_str(value, format) {
735                return Ok(dt);
736            }
737            // Try parsing as NaiveDate (date only formats)
738            if let Ok(date) = NaiveDate::parse_from_str(value, format) {
739                return Ok(date.and_hms_opt(0, 0, 0).unwrap());
740            }
741            // Try parsing as DateTime<Utc> (for timezone-aware formats)
742            if let Ok(dt) = ChronoDateTime::parse_from_str(value, format) {
743                return Ok(dt.with_timezone(&Utc).naive_utc());
744            }
745        }
746
747        Err(format!(
748            "'{}' does not match the formats: {}",
749            value,
750            self.formats.join(", ")
751        ))
752    }
753
754    fn get_metavar(&self) -> Option<String> {
755        Some(format!("[{}]", self.formats.join("|")))
756    }
757}
758
759// =============================================================================
760// Choice Type
761// =============================================================================
762
763/// A parameter type that restricts values to a fixed set of choices.
764///
765/// By default, matching is case-sensitive. Set `case_sensitive` to false
766/// to enable case-insensitive matching.
767#[derive(Debug, Clone)]
768pub struct Choice {
769    /// The valid choices.
770    pub choices: Vec<String>,
771    /// Whether matching is case-sensitive.
772    pub case_sensitive: bool,
773}
774
775impl Choice {
776    /// Create a new choice type with the given choices.
777    pub fn new<I, S>(choices: I) -> Self
778    where
779        I: IntoIterator<Item = S>,
780        S: Into<String>,
781    {
782        Self {
783            choices: choices.into_iter().map(|s| s.into()).collect(),
784            case_sensitive: true,
785        }
786    }
787
788    /// Set whether matching is case-sensitive.
789    pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
790        self.case_sensitive = case_sensitive;
791        self
792    }
793
794    /// Normalize a value for comparison.
795    fn normalize(&self, value: &str) -> String {
796        if self.case_sensitive {
797            value.to_string()
798        } else {
799            value.to_lowercase()
800        }
801    }
802}
803
804impl TypeConverter for Choice {
805    type Value = String;
806
807    fn name(&self) -> &str {
808        "CHOICE"
809    }
810
811    fn convert(&self, value: &str) -> Result<Self::Value, String> {
812        let normalized_value = self.normalize(value);
813
814        for choice in &self.choices {
815            if self.normalize(choice) == normalized_value {
816                // Return the original choice, not the input value
817                return Ok(choice.clone());
818            }
819        }
820
821        if self.choices.len() == 1 {
822            Err(format!("'{}' is not '{}'.", value, self.choices[0]))
823        } else {
824            let choices_str = self
825                .choices
826                .iter()
827                .map(|c| format!("'{}'", c))
828                .collect::<Vec<_>>()
829                .join(", ");
830            Err(format!("'{}' is not one of {}.", value, choices_str))
831        }
832    }
833
834    fn get_missing_message(&self) -> Option<String> {
835        Some(format!("Choose from:\n\t{}", self.choices.join(",\n\t")))
836    }
837
838    fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
839        let normalized_incomplete = self.normalize(incomplete);
840        self.choices
841            .iter()
842            .filter(|choice| {
843                let normalized_choice = self.normalize(choice);
844                normalized_choice.starts_with(&normalized_incomplete)
845            })
846            .map(|choice| CompletionItem::new(choice.clone()))
847            .collect()
848    }
849
850    fn get_metavar(&self) -> Option<String> {
851        if self.choices.is_empty() {
852            return Some("CHOICE".to_string());
853        }
854        Some(self.choices.join("|"))
855    }
856}
857
858// =============================================================================
859// Path Type
860// =============================================================================
861
862/// A parameter type for file system paths with optional validation.
863///
864/// Unlike the `File` type, this returns the path as a `PathBuf` rather
865/// than opening the file.
866#[derive(Debug, Clone)]
867pub struct PathType {
868    /// Whether the path must exist.
869    pub exists: bool,
870    /// Whether files are allowed.
871    pub file_okay: bool,
872    /// Whether directories are allowed.
873    pub dir_okay: bool,
874    /// Whether the path must be readable.
875    pub readable: bool,
876    /// Whether the path must be writable.
877    pub writable: bool,
878    /// Whether the path must be executable.
879    pub executable: bool,
880    /// Whether to resolve the path to an absolute path.
881    pub resolve_path: bool,
882    /// Whether to allow "-" to indicate stdin/stdout.
883    pub allow_dash: bool,
884}
885
886impl Default for PathType {
887    fn default() -> Self {
888        Self::new()
889    }
890}
891
892impl PathType {
893    /// Create a new path type with default settings.
894    pub const fn new() -> Self {
895        Self {
896            exists: false,
897            file_okay: true,
898            dir_okay: true,
899            readable: true,
900            writable: false,
901            executable: false,
902            resolve_path: false,
903            allow_dash: false,
904        }
905    }
906
907    /// Require the path to exist.
908    pub const fn exists(mut self, exists: bool) -> Self {
909        self.exists = exists;
910        self
911    }
912
913    /// Allow only files (not directories).
914    pub const fn file_okay(mut self, file_okay: bool) -> Self {
915        self.file_okay = file_okay;
916        self
917    }
918
919    /// Allow only directories (not files).
920    pub const fn dir_okay(mut self, dir_okay: bool) -> Self {
921        self.dir_okay = dir_okay;
922        self
923    }
924
925    /// Require the path to be readable.
926    pub const fn readable(mut self, readable: bool) -> Self {
927        self.readable = readable;
928        self
929    }
930
931    /// Require the path to be writable.
932    pub const fn writable(mut self, writable: bool) -> Self {
933        self.writable = writable;
934        self
935    }
936
937    /// Require the path to be executable.
938    pub const fn executable(mut self, executable: bool) -> Self {
939        self.executable = executable;
940        self
941    }
942
943    /// Resolve the path to an absolute path.
944    pub const fn resolve_path(mut self, resolve_path: bool) -> Self {
945        self.resolve_path = resolve_path;
946        self
947    }
948
949    /// Allow "-" to indicate stdin/stdout.
950    pub const fn allow_dash(mut self, allow_dash: bool) -> Self {
951        self.allow_dash = allow_dash;
952        self
953    }
954
955    /// Get the type name based on configuration.
956    fn type_name(&self) -> &str {
957        if self.file_okay && !self.dir_okay {
958            "File"
959        } else if self.dir_okay && !self.file_okay {
960            "Directory"
961        } else {
962            "Path"
963        }
964    }
965}
966
967impl TypeConverter for PathType {
968    type Value = PathBuf;
969
970    fn name(&self) -> &str {
971        if self.file_okay && !self.dir_okay {
972            "FILE"
973        } else if self.dir_okay && !self.file_okay {
974            "DIRECTORY"
975        } else {
976            "PATH"
977        }
978    }
979
980    fn convert(&self, value: &str) -> Result<Self::Value, String> {
981        // If neither files nor directories are allowed, reject everything
982        if !self.file_okay && !self.dir_okay {
983            return Err("No path is valid (file_okay=false and dir_okay=false)".to_string());
984        }
985
986        // Handle dash for stdin/stdout
987        if value == "-" {
988            if self.file_okay && self.allow_dash {
989                return Ok(PathBuf::from("-"));
990            }
991            return Err("'-' is not allowed".to_string());
992        }
993
994        let path = if self.resolve_path {
995            match std::fs::canonicalize(value) {
996                Ok(p) => p,
997                Err(_) if !self.exists => {
998                    // Path doesn't exist, but resolve_path still means make absolute
999                    // Use current dir + relative path
1000                    std::env::current_dir()
1001                        .map(|cwd| cwd.join(value))
1002                        .unwrap_or_else(|_| PathBuf::from(value))
1003                }
1004                Err(_) => return Err(format!("{} '{}' does not exist", self.type_name(), value)),
1005            }
1006        } else {
1007            PathBuf::from(value)
1008        };
1009
1010        // Check existence
1011        if self.exists && !path.exists() {
1012            return Err(format!("{} '{}' does not exist", self.type_name(), value));
1013        }
1014
1015        // Only perform these checks if the path exists
1016        if path.exists() {
1017            let metadata = std::fs::metadata(&path)
1018                .map_err(|e| format!("Cannot access '{}': {}", value, e))?;
1019
1020            // Check file/directory constraints
1021            if !self.file_okay && metadata.is_file() {
1022                return Err(format!("{} '{}' is a file", self.type_name(), value));
1023            }
1024            if !self.dir_okay && metadata.is_dir() {
1025                return Err(format!("{} '{}' is a directory", self.type_name(), value));
1026            }
1027
1028            // Check permissions (Unix-specific checks, simplified for cross-platform)
1029            #[cfg(unix)]
1030            {
1031                use std::os::unix::fs::PermissionsExt;
1032                let perms = metadata.permissions();
1033                let mode = perms.mode();
1034
1035                if self.readable && (mode & 0o444) == 0 {
1036                    return Err(format!("{} '{}' is not readable", self.type_name(), value));
1037                }
1038                if self.writable && (mode & 0o222) == 0 {
1039                    return Err(format!("{} '{}' is not writable", self.type_name(), value));
1040                }
1041                if self.executable && (mode & 0o111) == 0 {
1042                    return Err(format!(
1043                        "{} '{}' is not executable",
1044                        self.type_name(),
1045                        value
1046                    ));
1047                }
1048            }
1049        }
1050
1051        Ok(path)
1052    }
1053
1054    fn split_envvar_value(&self, value: &str) -> Vec<String> {
1055        // Use OS-specific path list separator (: on Unix, ; on Windows)
1056        std::env::split_paths(value)
1057            .map(|p| p.to_string_lossy().into_owned())
1058            .collect()
1059    }
1060
1061    fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
1062        let completion_type = if self.dir_okay && !self.file_okay {
1063            "dir"
1064        } else {
1065            "file"
1066        };
1067        vec![CompletionItem::with_type(incomplete, completion_type)]
1068    }
1069}
1070
1071// =============================================================================
1072// File Type
1073// =============================================================================
1074
1075/// The mode for opening a file.
1076#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1077pub enum FileMode {
1078    /// Open for reading (file must exist).
1079    #[default]
1080    Read,
1081    /// Open for writing (creates or truncates).
1082    Write,
1083    /// Open for appending (creates if doesn't exist).
1084    Append,
1085    /// Open for reading and writing.
1086    ReadWrite,
1087}
1088
1089impl FileMode {
1090    /// Parse a mode string (like Python's open modes).
1091    pub fn parse(s: &str) -> Option<Self> {
1092        match s {
1093            "r" | "rb" => Some(FileMode::Read),
1094            "w" | "wb" => Some(FileMode::Write),
1095            "a" | "ab" => Some(FileMode::Append),
1096            "r+" | "rb+" | "r+b" => Some(FileMode::ReadWrite),
1097            "w+" | "wb+" | "w+b" => Some(FileMode::ReadWrite),
1098            "a+" | "ab+" | "a+b" => Some(FileMode::ReadWrite), // read+append
1099            _ => None,
1100        }
1101    }
1102
1103    /// Returns true if this mode is for reading.
1104    pub fn is_read(&self) -> bool {
1105        matches!(self, FileMode::Read | FileMode::ReadWrite)
1106    }
1107
1108    /// Returns true if this mode is for writing.
1109    pub fn is_write(&self) -> bool {
1110        matches!(
1111            self,
1112            FileMode::Write | FileMode::Append | FileMode::ReadWrite
1113        )
1114    }
1115}
1116
1117/// The source of a file handle (regular file or stdio).
1118#[derive(Debug)]
1119enum FileSource {
1120    /// A regular file on disk.
1121    File(StdFile),
1122    /// Standard input.
1123    Stdin,
1124    /// Standard output.
1125    Stdout,
1126}
1127
1128/// Counter for generating unique temp file names.
1129static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
1130
1131/// A wrapper around a file that supports lazy opening and stdin/stdout.
1132///
1133/// When `atomic` is enabled for write operations, writes go to a temporary file
1134/// in the same directory. The temp file is renamed to the final path when
1135/// `close()` is called or the `LazyFile` is dropped.
1136#[derive(Debug)]
1137pub struct LazyFile {
1138    path: PathBuf,
1139    mode: FileMode,
1140    source: Option<FileSource>,
1141    is_stdio: bool,
1142    atomic: bool,
1143    /// Path to the temporary file when using atomic writes.
1144    temp_path: Option<PathBuf>,
1145}
1146
1147impl LazyFile {
1148    /// Create a new lazy file.
1149    pub fn new(path: PathBuf, mode: FileMode) -> Self {
1150        let is_stdio = path.as_os_str() == "-";
1151        Self {
1152            path,
1153            mode,
1154            source: None,
1155            is_stdio,
1156            atomic: false,
1157            temp_path: None,
1158        }
1159    }
1160
1161    /// Create a lazy file for stdin.
1162    pub fn stdin() -> Self {
1163        Self {
1164            path: PathBuf::from("-"),
1165            mode: FileMode::Read,
1166            source: None,
1167            is_stdio: true,
1168            atomic: false,
1169            temp_path: None,
1170        }
1171    }
1172
1173    /// Create a lazy file for stdout.
1174    pub fn stdout() -> Self {
1175        Self {
1176            path: PathBuf::from("-"),
1177            mode: FileMode::Write,
1178            source: None,
1179            is_stdio: true,
1180            atomic: false,
1181            temp_path: None,
1182        }
1183    }
1184
1185    /// Set whether to use atomic writes.
1186    pub fn atomic(mut self, atomic: bool) -> Self {
1187        self.atomic = atomic;
1188        self
1189    }
1190
1191    /// Get the path to this file.
1192    pub fn path(&self) -> &StdPath {
1193        &self.path
1194    }
1195
1196    /// Returns true if this is stdin or stdout.
1197    pub fn is_stdio(&self) -> bool {
1198        self.is_stdio
1199    }
1200
1201    /// Open the file (lazily).
1202    fn open(&mut self) -> io::Result<()> {
1203        if self.source.is_some() {
1204            return Ok(());
1205        }
1206
1207        let source = if self.is_stdio {
1208            if self.mode.is_read() {
1209                FileSource::Stdin
1210            } else {
1211                FileSource::Stdout
1212            }
1213        } else if self.atomic && self.mode == FileMode::Write {
1214            // For atomic writes, create a temp file in the same directory
1215            let parent = self.path.parent().unwrap_or(StdPath::new("."));
1216            let counter = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst);
1217            let temp_name = format!(
1218                ".{}.tmp.{}",
1219                self.path
1220                    .file_name()
1221                    .map(|n| n.to_string_lossy())
1222                    .unwrap_or_default(),
1223                counter
1224            );
1225            let temp_path = parent.join(&temp_name);
1226            let file = StdFile::create(&temp_path)?;
1227            self.temp_path = Some(temp_path);
1228            FileSource::File(file)
1229        } else {
1230            let file = match self.mode {
1231                FileMode::Read => StdFile::open(&self.path),
1232                FileMode::Write => StdFile::create(&self.path),
1233                FileMode::Append => OpenOptions::new()
1234                    .append(true)
1235                    .create(true)
1236                    .open(&self.path),
1237                FileMode::ReadWrite => OpenOptions::new()
1238                    .read(true)
1239                    .write(true)
1240                    .create(true)
1241                    .truncate(false)
1242                    .open(&self.path),
1243            }?;
1244            FileSource::File(file)
1245        };
1246        self.source = Some(source);
1247        Ok(())
1248    }
1249
1250    /// Close the file and finalize atomic writes.
1251    ///
1252    /// For atomic writes, this renames the temp file to the final path.
1253    /// Returns an error if the rename fails.
1254    ///
1255    /// Note: This is called automatically on drop, but errors during drop
1256    /// are silently ignored. Call this explicitly if you need to handle errors.
1257    pub fn close(&mut self) -> io::Result<()> {
1258        // Flush and drop the file handle first
1259        if let Some(FileSource::File(mut f)) = self.source.take() {
1260            f.flush()?;
1261            // File is dropped here, releasing the handle
1262        }
1263
1264        // Now rename the temp file to the final path
1265        if let Some(temp_path) = self.temp_path.take() {
1266            fs::rename(&temp_path, &self.path)?;
1267        }
1268
1269        Ok(())
1270    }
1271}
1272
1273impl Drop for LazyFile {
1274    fn drop(&mut self) {
1275        // Best-effort close on drop; errors are silently ignored
1276        let _ = self.close();
1277    }
1278}
1279
1280impl Read for LazyFile {
1281    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1282        self.open()?;
1283        match self.source.as_mut() {
1284            Some(FileSource::File(f)) => f.read(buf),
1285            Some(FileSource::Stdin) => io::stdin().read(buf),
1286            _ => Err(io::Error::new(
1287                io::ErrorKind::InvalidInput,
1288                "Cannot read from write-only file",
1289            )),
1290        }
1291    }
1292}
1293
1294impl Write for LazyFile {
1295    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1296        self.open()?;
1297        match self.source.as_mut() {
1298            Some(FileSource::File(f)) => f.write(buf),
1299            Some(FileSource::Stdout) => io::stdout().write(buf),
1300            _ => Err(io::Error::new(
1301                io::ErrorKind::InvalidInput,
1302                "Cannot write to read-only file",
1303            )),
1304        }
1305    }
1306
1307    fn flush(&mut self) -> io::Result<()> {
1308        match self.source.as_mut() {
1309            Some(FileSource::File(f)) => f.flush(),
1310            Some(FileSource::Stdout) => io::stdout().flush(),
1311            _ => Ok(()),
1312        }
1313    }
1314}
1315
1316/// A parameter type for files.
1317///
1318/// Unlike `PathType`, this opens the file and returns a handle.
1319/// The special value "-" indicates stdin (for reading) or stdout (for writing).
1320#[derive(Debug, Clone)]
1321pub struct FileType {
1322    /// The mode to open the file in.
1323    pub mode: FileMode,
1324    /// Whether to open the file lazily.
1325    pub lazy: Option<bool>,
1326    /// Whether to use atomic writes.
1327    pub atomic: bool,
1328}
1329
1330impl Default for FileType {
1331    fn default() -> Self {
1332        Self::new()
1333    }
1334}
1335
1336impl FileType {
1337    /// Create a new file type for reading.
1338    pub const fn new() -> Self {
1339        Self {
1340            mode: FileMode::Read,
1341            lazy: None,
1342            atomic: false,
1343        }
1344    }
1345
1346    /// Set the file mode.
1347    pub const fn mode(mut self, mode: FileMode) -> Self {
1348        self.mode = mode;
1349        self
1350    }
1351
1352    /// Set whether to open lazily.
1353    pub const fn lazy(mut self, lazy: bool) -> Self {
1354        self.lazy = Some(lazy);
1355        self
1356    }
1357
1358    /// Set whether to use atomic writes.
1359    pub const fn atomic(mut self, atomic: bool) -> Self {
1360        self.atomic = atomic;
1361        self
1362    }
1363
1364    /// Determine if the file should be opened lazily.
1365    fn resolve_lazy(&self, path: &str) -> bool {
1366        if let Some(lazy) = self.lazy {
1367            return lazy;
1368        }
1369        // Default: non-lazy for stdin/stdout and reading, lazy for writing
1370        if path == "-" {
1371            return false;
1372        }
1373        matches!(self.mode, FileMode::Write | FileMode::Append)
1374    }
1375}
1376
1377impl TypeConverter for FileType {
1378    type Value = LazyFile;
1379
1380    fn name(&self) -> &str {
1381        "FILENAME"
1382    }
1383
1384    fn convert(&self, value: &str) -> Result<Self::Value, String> {
1385        // Handle stdin/stdout via "-"
1386        if value == "-" {
1387            // ReadWrite mode is not supported for stdin/stdout
1388            if matches!(self.mode, FileMode::ReadWrite) {
1389                return Err("'-' (stdin/stdout) cannot be used with read+write mode".to_string());
1390            }
1391            let lazy_file = if self.mode.is_read() {
1392                LazyFile::stdin()
1393            } else {
1394                LazyFile::stdout()
1395            };
1396            return Ok(lazy_file);
1397        }
1398
1399        let path = PathBuf::from(value);
1400
1401        // Validate for read mode (file must exist)
1402        if self.mode.is_read() && !self.resolve_lazy(value) && !path.exists() {
1403            return Err(format!("'{}': No such file or directory", value));
1404        }
1405
1406        let mut lazy_file = LazyFile::new(path, self.mode);
1407        if self.atomic {
1408            lazy_file = lazy_file.atomic(true);
1409        }
1410
1411        // If not lazy, open immediately to catch errors
1412        if !self.resolve_lazy(value) {
1413            lazy_file
1414                .open()
1415                .map_err(|e| format!("'{}': {}", value, e))?;
1416        }
1417
1418        Ok(lazy_file)
1419    }
1420
1421    fn split_envvar_value(&self, value: &str) -> Vec<String> {
1422        // Use OS-specific path list separator (: on Unix, ; on Windows)
1423        std::env::split_paths(value)
1424            .map(|p| p.to_string_lossy().into_owned())
1425            .collect()
1426    }
1427
1428    fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
1429        vec![CompletionItem::with_type(incomplete, "file")]
1430    }
1431}
1432
1433// =============================================================================
1434// Tuple Type
1435// =============================================================================
1436
1437/// A boxed parameter type for runtime polymorphism.
1438pub type BoxedTypeConverter<T> = Box<dyn TypeConverter<Value = T> + Send + Sync>;
1439
1440/// A composite parameter type that collects multiple values with different types.
1441///
1442/// Unlike regular types with `nargs`, each position in a tuple can have
1443/// a different type.
1444#[derive(Debug)]
1445pub struct TupleType {
1446    /// The types for each position in the tuple.
1447    types: Vec<TupleElementType>,
1448}
1449
1450/// An element type within a tuple (erased to String for simplicity).
1451#[derive(Debug, Clone)]
1452enum TupleElementType {
1453    String,
1454    Int,
1455    Float,
1456    Bool,
1457}
1458
1459impl TupleType {
1460    /// Create a new tuple type from a list of type specifiers.
1461    ///
1462    /// Each specifier should be one of: "string", "int", "float", "bool"
1463    pub fn new<I, S>(types: I) -> Self
1464    where
1465        I: IntoIterator<Item = S>,
1466        S: AsRef<str>,
1467    {
1468        let types = types
1469            .into_iter()
1470            .map(|s| match s.as_ref().to_lowercase().as_str() {
1471                "string" | "str" | "text" => TupleElementType::String,
1472                "int" | "integer" => TupleElementType::Int,
1473                "float" => TupleElementType::Float,
1474                "bool" | "boolean" => TupleElementType::Bool,
1475                _ => TupleElementType::String,
1476            })
1477            .collect();
1478        Self { types }
1479    }
1480
1481    /// Create a tuple of strings.
1482    pub fn strings(count: usize) -> Self {
1483        Self {
1484            types: vec![TupleElementType::String; count],
1485        }
1486    }
1487
1488    /// Create a tuple of integers.
1489    pub fn ints(count: usize) -> Self {
1490        Self {
1491            types: vec![TupleElementType::Int; count],
1492        }
1493    }
1494}
1495
1496/// A converted tuple value with dynamic types.
1497#[derive(Debug, Clone, PartialEq)]
1498pub enum TupleValue {
1499    String(String),
1500    Int(i64),
1501    Float(f64),
1502    Bool(bool),
1503}
1504
1505impl TupleValue {
1506    /// Get as string, if this is a string value.
1507    pub fn as_string(&self) -> Option<&str> {
1508        match self {
1509            TupleValue::String(s) => Some(s),
1510            _ => None,
1511        }
1512    }
1513
1514    /// Get as integer, if this is an integer value.
1515    pub fn as_int(&self) -> Option<i64> {
1516        match self {
1517            TupleValue::Int(i) => Some(*i),
1518            _ => None,
1519        }
1520    }
1521
1522    /// Get as float, if this is a float value.
1523    pub fn as_float(&self) -> Option<f64> {
1524        match self {
1525            TupleValue::Float(f) => Some(*f),
1526            _ => None,
1527        }
1528    }
1529
1530    /// Get as bool, if this is a bool value.
1531    pub fn as_bool(&self) -> Option<bool> {
1532        match self {
1533            TupleValue::Bool(b) => Some(*b),
1534            _ => None,
1535        }
1536    }
1537}
1538
1539impl TupleType {
1540    /// Convert a single element at the given index.
1541    ///
1542    /// This is the preferred way to convert tuple elements - the parser
1543    /// should call this once for each argument consumed.
1544    pub fn convert_element(&self, index: usize, value: &str) -> Result<TupleValue, String> {
1545        let element_type = self.types.get(index).ok_or_else(|| {
1546            format!(
1547                "tuple index {} out of bounds (arity {})",
1548                index,
1549                self.types.len()
1550            )
1551        })?;
1552
1553        match element_type {
1554            TupleElementType::String => Ok(TupleValue::String(value.to_string())),
1555            TupleElementType::Int => {
1556                let i: i64 = value
1557                    .parse()
1558                    .map_err(|_| format!("'{}' is not a valid integer", value))?;
1559                Ok(TupleValue::Int(i))
1560            }
1561            TupleElementType::Float => {
1562                let f: f64 = value
1563                    .parse()
1564                    .map_err(|_| format!("'{}' is not a valid float", value))?;
1565                Ok(TupleValue::Float(f))
1566            }
1567            TupleElementType::Bool => {
1568                let b = BoolType::str_to_bool(value)
1569                    .ok_or_else(|| format!("'{}' is not a valid boolean", value))?;
1570                Ok(TupleValue::Bool(b))
1571            }
1572        }
1573    }
1574
1575    /// Convert a slice of pre-split values into a tuple.
1576    ///
1577    /// This is the standard Click-compatible conversion - the parser provides
1578    /// already-split argument values.
1579    pub fn convert_values(&self, values: &[&str]) -> Result<Vec<TupleValue>, String> {
1580        if values.len() != self.types.len() {
1581            return Err(format!(
1582                "{} values are required, but {} were given",
1583                self.types.len(),
1584                values.len()
1585            ));
1586        }
1587
1588        values
1589            .iter()
1590            .enumerate()
1591            .map(|(i, v)| self.convert_element(i, v))
1592            .collect()
1593    }
1594}
1595
1596impl TypeConverter for TupleType {
1597    type Value = Vec<TupleValue>;
1598
1599    fn name(&self) -> &str {
1600        "TUPLE"
1601    }
1602
1603    fn convert(&self, value: &str) -> Result<Self::Value, String> {
1604        // When called with a single value, this is typically for a single-element
1605        // tuple or when the entire tuple is provided as one string (e.g., from envvar).
1606        // For envvar compatibility, split on whitespace. For proper CLI parsing,
1607        // use convert_values() with pre-split arguments.
1608        let parts: Vec<&str> = value.split_whitespace().collect();
1609        self.convert_values(&parts)
1610    }
1611
1612    fn get_metavar(&self) -> Option<String> {
1613        let names: Vec<&str> = self
1614            .types
1615            .iter()
1616            .map(|t| match t {
1617                TupleElementType::String => "TEXT",
1618                TupleElementType::Int => "INTEGER",
1619                TupleElementType::Float => "FLOAT",
1620                TupleElementType::Bool => "BOOLEAN",
1621            })
1622            .collect();
1623        Some(format!("<{}>", names.join(" ")))
1624    }
1625
1626    fn is_composite(&self) -> bool {
1627        true
1628    }
1629
1630    fn arity(&self) -> usize {
1631        self.types.len()
1632    }
1633}
1634
1635// =============================================================================
1636// Tests
1637// =============================================================================
1638
1639#[cfg(test)]
1640mod tests {
1641    use super::*;
1642
1643    #[test]
1644    fn test_string_type() {
1645        assert_eq!(STRING.convert("hello").unwrap(), "hello");
1646        assert_eq!(STRING.convert("  spaces  ").unwrap(), "  spaces  ");
1647        assert_eq!(STRING.name(), "TEXT");
1648    }
1649
1650    #[test]
1651    fn test_int_type() {
1652        assert_eq!(INT.convert("42").unwrap(), 42);
1653        assert_eq!(INT.convert("-123").unwrap(), -123);
1654        assert_eq!(INT.convert("  456  ").unwrap(), 456);
1655        assert!(INT.convert("not a number").is_err());
1656        assert!(INT.convert("3.14").is_err());
1657    }
1658
1659    #[test]
1660    fn test_float_type() {
1661        assert_eq!(FLOAT.convert("3.14").unwrap(), 3.14);
1662        assert_eq!(FLOAT.convert("-2.5").unwrap(), -2.5);
1663        assert_eq!(FLOAT.convert("42").unwrap(), 42.0);
1664        assert!(FLOAT.convert("not a number").is_err());
1665    }
1666
1667    #[test]
1668    fn test_bool_type() {
1669        assert!(BOOL.convert("true").unwrap());
1670        assert!(BOOL.convert("True").unwrap());
1671        assert!(BOOL.convert("TRUE").unwrap());
1672        assert!(BOOL.convert("yes").unwrap());
1673        assert!(BOOL.convert("1").unwrap());
1674        assert!(BOOL.convert("on").unwrap());
1675        assert!(!BOOL.convert("false").unwrap());
1676        assert!(!BOOL.convert("no").unwrap());
1677        assert!(!BOOL.convert("0").unwrap());
1678        assert!(!BOOL.convert("off").unwrap());
1679        assert!(BOOL.convert("maybe").is_err());
1680    }
1681
1682    #[test]
1683    fn test_uuid_type() {
1684        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
1685        let result = UUID.convert(uuid_str).unwrap();
1686        assert_eq!(result.to_string(), uuid_str);
1687        assert!(UUID.convert("not-a-uuid").is_err());
1688    }
1689
1690    #[test]
1691    fn test_int_range() {
1692        let range = IntRange::new().range(0, 100);
1693        assert_eq!(range.convert("50").unwrap(), 50);
1694        assert_eq!(range.convert("0").unwrap(), 0);
1695        assert_eq!(range.convert("100").unwrap(), 100);
1696        assert!(range.convert("-1").is_err());
1697        assert!(range.convert("101").is_err());
1698    }
1699
1700    #[test]
1701    fn test_int_range_open() {
1702        let range = IntRange::new().min(0).max(10).min_open(true).max_open(true);
1703        assert!(range.convert("0").is_err()); // min is open
1704        assert!(range.convert("10").is_err()); // max is open
1705        assert_eq!(range.convert("1").unwrap(), 1);
1706        assert_eq!(range.convert("9").unwrap(), 9);
1707    }
1708
1709    #[test]
1710    fn test_int_range_clamp() {
1711        let range = IntRange::new().range(0, 100).clamp(true);
1712        assert_eq!(range.convert("-50").unwrap(), 0);
1713        assert_eq!(range.convert("150").unwrap(), 100);
1714        assert_eq!(range.convert("50").unwrap(), 50);
1715    }
1716
1717    #[test]
1718    fn test_float_range() {
1719        let range = FloatRange::new().range(0.0, 1.0);
1720        assert_eq!(range.convert("0.5").unwrap(), 0.5);
1721        assert_eq!(range.convert("0.0").unwrap(), 0.0);
1722        assert_eq!(range.convert("1.0").unwrap(), 1.0);
1723        assert!(range.convert("-0.1").is_err());
1724        assert!(range.convert("1.1").is_err());
1725    }
1726
1727    #[test]
1728    fn test_datetime_type() {
1729        let dt = DateTimeType::new();
1730
1731        // Date only
1732        let result = dt.convert("2024-01-15").unwrap();
1733        assert_eq!(result.date().to_string(), "2024-01-15");
1734
1735        // Datetime with T
1736        let result = dt.convert("2024-01-15T10:30:00").unwrap();
1737        assert_eq!(result.to_string(), "2024-01-15 10:30:00");
1738
1739        // Datetime with space
1740        let result = dt.convert("2024-01-15 10:30:00").unwrap();
1741        assert_eq!(result.to_string(), "2024-01-15 10:30:00");
1742
1743        assert!(dt.convert("not a date").is_err());
1744    }
1745
1746    #[test]
1747    fn test_choice() {
1748        let choice = Choice::new(["one", "two", "three"]);
1749        assert_eq!(choice.convert("one").unwrap(), "one");
1750        assert_eq!(choice.convert("two").unwrap(), "two");
1751        assert!(choice.convert("four").is_err());
1752    }
1753
1754    #[test]
1755    fn test_choice_case_insensitive() {
1756        let choice = Choice::new(["One", "Two", "Three"]).case_sensitive(false);
1757        assert_eq!(choice.convert("one").unwrap(), "One");
1758        assert_eq!(choice.convert("ONE").unwrap(), "One");
1759        assert_eq!(choice.convert("oNe").unwrap(), "One");
1760    }
1761
1762    #[test]
1763    fn test_choice_shell_complete() {
1764        let choice = Choice::new(["apple", "apricot", "banana"]);
1765        let completions = choice.shell_complete("ap");
1766        assert_eq!(completions.len(), 2);
1767        assert!(completions.iter().any(|c| c.value == "apple"));
1768        assert!(completions.iter().any(|c| c.value == "apricot"));
1769    }
1770
1771    #[test]
1772    fn test_path_type() {
1773        let path = PathType::new();
1774        // Basic path conversion (doesn't require existence by default)
1775        let result = path.convert("/some/path").unwrap();
1776        assert_eq!(result, PathBuf::from("/some/path"));
1777    }
1778
1779    #[test]
1780    fn test_tuple_type() {
1781        let tuple = TupleType::new(["string", "int", "bool"]);
1782        let result = tuple.convert("hello 42 true").unwrap();
1783        assert_eq!(result.len(), 3);
1784        assert_eq!(result[0].as_string(), Some("hello"));
1785        assert_eq!(result[1].as_int(), Some(42));
1786        assert_eq!(result[2].as_bool(), Some(true));
1787    }
1788
1789    #[test]
1790    fn test_tuple_type_wrong_count() {
1791        let tuple = TupleType::new(["string", "int"]);
1792        assert!(tuple.convert("hello").is_err());
1793        assert!(tuple.convert("hello 42 extra").is_err());
1794    }
1795
1796    #[test]
1797    fn test_unprocessed() {
1798        assert_eq!(UNPROCESSED.convert("raw value").unwrap(), "raw value");
1799        assert_eq!(UNPROCESSED.name(), "TEXT");
1800    }
1801
1802    #[test]
1803    fn test_completion_item() {
1804        let item = CompletionItem::new("test");
1805        assert_eq!(item.value, "test");
1806        assert_eq!(item.completion_type, "plain");
1807        assert!(item.help.is_none());
1808
1809        let item = CompletionItem::with_type("path", "file").with_help("A file path");
1810        assert_eq!(item.value, "path");
1811        assert_eq!(item.completion_type, "file");
1812        assert_eq!(item.help, Some("A file path".to_string()));
1813    }
1814}