flag_rs/flag.rs
1//! Flag system for command-line argument parsing
2//!
3//! This module provides a flexible flag parsing system that supports:
4//! - Multiple value types (string, bool, int, float, string slice)
5//! - Short and long flag names
6//! - Required and optional flags
7//! - Default values
8//! - Hierarchical flag inheritance from parent commands
9
10use crate::completion::{CompletionFunc, CompletionResult};
11use crate::error::{Error, Result};
12use std::collections::HashSet;
13
14/// Represents the value of a parsed flag
15///
16/// `FlagValue` is an enum that can hold different types of values
17/// that flags can have. This allows for type-safe access to flag values.
18///
19/// # Examples
20///
21/// ```
22/// use flag_rs::flag::{FlagValue, FlagType, Flag};
23///
24/// // Parse different types of values
25/// let string_flag = Flag::new("name").value_type(FlagType::String);
26/// let value = string_flag.parse_value("John").unwrap();
27/// assert_eq!(value.as_string().unwrap(), "John");
28///
29/// let bool_flag = Flag::new("verbose").value_type(FlagType::Bool);
30/// let value = bool_flag.parse_value("true").unwrap();
31/// assert!(value.as_bool().unwrap());
32/// ```
33#[derive(Clone, Debug, PartialEq)]
34pub enum FlagValue {
35 /// A string value
36 String(String),
37 /// A boolean value
38 Bool(bool),
39 /// An integer value
40 Int(i64),
41 /// A floating-point value
42 Float(f64),
43 /// A slice of strings (for repeated flags)
44 StringSlice(Vec<String>),
45}
46
47impl FlagValue {
48 /// Returns the value as a string reference
49 ///
50 /// # Errors
51 ///
52 /// Returns `Error::FlagParsing` if the value is not a string
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// use flag_rs::flag::FlagValue;
58 ///
59 /// let value = FlagValue::String("hello".to_string());
60 /// assert_eq!(value.as_string().unwrap(), "hello");
61 ///
62 /// let value = FlagValue::Bool(true);
63 /// assert!(value.as_string().is_err());
64 /// ```
65 pub fn as_string(&self) -> Result<&String> {
66 match self {
67 Self::String(s) => Ok(s),
68 _ => Err(Error::flag_parsing("Flag value is not a string")),
69 }
70 }
71
72 /// Returns the value as a boolean
73 ///
74 /// # Errors
75 ///
76 /// Returns `Error::FlagParsing` if the value is not a boolean
77 pub fn as_bool(&self) -> Result<bool> {
78 match self {
79 Self::Bool(b) => Ok(*b),
80 _ => Err(Error::flag_parsing("Flag value is not a boolean")),
81 }
82 }
83
84 /// Returns the value as an integer
85 ///
86 /// # Errors
87 ///
88 /// Returns `Error::FlagParsing` if the value is not an integer
89 pub fn as_int(&self) -> Result<i64> {
90 match self {
91 Self::Int(i) => Ok(*i),
92 _ => Err(Error::flag_parsing("Flag value is not an integer")),
93 }
94 }
95
96 /// Returns the value as a float
97 ///
98 /// # Errors
99 ///
100 /// Returns `Error::FlagParsing` if the value is not a float
101 pub fn as_float(&self) -> Result<f64> {
102 match self {
103 Self::Float(f) => Ok(*f),
104 _ => Err(Error::flag_parsing("Flag value is not a float")),
105 }
106 }
107
108 /// Returns the value as a string slice reference
109 ///
110 /// # Errors
111 ///
112 /// Returns `Error::FlagParsing` if the value is not a string slice
113 pub fn as_string_slice(&self) -> Result<&Vec<String>> {
114 match self {
115 Self::StringSlice(v) => Ok(v),
116 _ => Err(Error::flag_parsing("Flag value is not a string slice")),
117 }
118 }
119}
120
121/// Represents constraints that can be applied to flags
122///
123/// Flag constraints allow you to define relationships between flags,
124/// such as mutual exclusivity or dependencies.
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub enum FlagConstraint {
127 /// This flag is required if another flag is set
128 RequiredIf(String),
129 /// This flag conflicts with other flags (mutually exclusive)
130 ConflictsWith(Vec<String>),
131 /// This flag requires other flags to be set
132 Requires(Vec<String>),
133}
134
135/// Represents a command-line flag
136///
137/// A `Flag` defines a command-line option that can be passed to a command.
138/// Flags can have both long names (e.g., `--verbose`) and short names (e.g., `-v`).
139///
140/// # Examples
141///
142/// ```
143/// use flag_rs::flag::{Flag, FlagType, FlagValue};
144///
145/// // Create a boolean flag
146/// let verbose = Flag::new("verbose")
147/// .short('v')
148/// .usage("Enable verbose output")
149/// .value_type(FlagType::Bool)
150/// .default(FlagValue::Bool(false));
151///
152/// // Create a string flag with validation
153/// let name = Flag::new("name")
154/// .short('n')
155/// .usage("Name of the resource")
156/// .value_type(FlagType::String)
157/// .required();
158/// ```
159pub struct Flag {
160 /// The long name of the flag (e.g., "verbose" for --verbose)
161 pub name: String,
162 /// The optional short name of the flag (e.g., 'v' for -v)
163 pub short: Option<char>,
164 /// A description of what the flag does
165 pub usage: String,
166 /// The default value if the flag is not provided
167 pub default: Option<FlagValue>,
168 /// Whether this flag must be provided
169 pub required: bool,
170 /// The type of value this flag accepts
171 pub value_type: FlagType,
172 /// Constraints applied to this flag
173 pub constraints: Vec<FlagConstraint>,
174 /// Optional completion function for this flag's values
175 pub completion: Option<CompletionFunc>,
176}
177
178/// Represents the type of value a flag accepts
179///
180/// This enum determines how flag values are parsed from string input.
181#[derive(Clone, Debug, PartialEq, Eq)]
182pub enum FlagType {
183 /// Accepts any string value
184 String,
185 /// Accepts boolean values (true/false, yes/no, 1/0)
186 Bool,
187 /// Accepts integer values
188 Int,
189 /// Accepts floating-point values
190 Float,
191 /// Accepts multiple string values (can be specified multiple times)
192 StringSlice,
193 /// Accepts multiple string values with accumulation (--tag=a --tag=b)
194 StringArray,
195 /// Must be one of a predefined set of values
196 Choice(Vec<String>),
197 /// Numeric value within a specific range
198 Range(i64, i64),
199 /// Must be a valid file path
200 File,
201 /// Must be a valid directory path
202 Directory,
203}
204
205impl Flag {
206 /// Creates a new flag with the given name
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use flag_rs::flag::Flag;
212 ///
213 /// let flag = Flag::new("verbose");
214 /// assert_eq!(flag.name, "verbose");
215 /// ```
216 #[must_use]
217 pub fn new(name: impl Into<String>) -> Self {
218 Self {
219 name: name.into(),
220 short: None,
221 usage: String::new(),
222 default: None,
223 required: false,
224 value_type: FlagType::String,
225 constraints: Vec::new(),
226 completion: None,
227 }
228 }
229
230 /// Creates a new boolean flag
231 ///
232 /// # Examples
233 ///
234 /// ```
235 /// use flag_rs::flag::Flag;
236 ///
237 /// let flag = Flag::bool("verbose");
238 /// ```
239 #[must_use]
240 pub fn bool(name: impl Into<String>) -> Self {
241 Self::new(name).value_type(FlagType::Bool)
242 }
243
244 /// Creates a new integer flag
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use flag_rs::flag::Flag;
250 ///
251 /// let flag = Flag::int("port");
252 /// ```
253 #[must_use]
254 pub fn int(name: impl Into<String>) -> Self {
255 Self::new(name).value_type(FlagType::Int)
256 }
257
258 /// Creates a new float flag
259 ///
260 /// # Examples
261 ///
262 /// ```
263 /// use flag_rs::flag::Flag;
264 ///
265 /// let flag = Flag::float("ratio");
266 /// ```
267 #[must_use]
268 pub fn float(name: impl Into<String>) -> Self {
269 Self::new(name).value_type(FlagType::Float)
270 }
271
272 /// Creates a new string flag
273 ///
274 /// # Examples
275 ///
276 /// ```
277 /// use flag_rs::flag::Flag;
278 ///
279 /// let flag = Flag::string("name");
280 /// ```
281 #[must_use]
282 pub fn string(name: impl Into<String>) -> Self {
283 Self::new(name) // String is the default type
284 }
285
286 /// Creates a new string slice flag (can be specified multiple times)
287 ///
288 /// # Examples
289 ///
290 /// ```
291 /// use flag_rs::flag::Flag;
292 ///
293 /// let flag = Flag::string_slice("tag");
294 /// ```
295 #[must_use]
296 pub fn string_slice(name: impl Into<String>) -> Self {
297 Self::new(name).value_type(FlagType::StringSlice)
298 }
299
300 /// Creates a new choice flag with allowed values
301 ///
302 /// # Examples
303 ///
304 /// ```
305 /// use flag_rs::flag::Flag;
306 ///
307 /// let flag = Flag::choice("format", &["json", "yaml", "xml"]);
308 /// ```
309 #[must_use]
310 pub fn choice(name: impl Into<String>, choices: &[&str]) -> Self {
311 let choices: Vec<String> = choices.iter().map(|&s| s.to_string()).collect();
312 Self::new(name).value_type(FlagType::Choice(choices))
313 }
314
315 /// Creates a new range flag with min and max values
316 ///
317 /// # Examples
318 ///
319 /// ```
320 /// use flag_rs::flag::Flag;
321 ///
322 /// let flag = Flag::range("workers", 1, 16);
323 /// ```
324 #[must_use]
325 pub fn range(name: impl Into<String>, min: i64, max: i64) -> Self {
326 Self::new(name).value_type(FlagType::Range(min, max))
327 }
328
329 /// Creates a new file flag
330 ///
331 /// # Examples
332 ///
333 /// ```
334 /// use flag_rs::flag::Flag;
335 ///
336 /// let flag = Flag::file("config");
337 /// ```
338 #[must_use]
339 pub fn file(name: impl Into<String>) -> Self {
340 Self::new(name).value_type(FlagType::File)
341 }
342
343 /// Creates a new directory flag
344 ///
345 /// # Examples
346 ///
347 /// ```
348 /// use flag_rs::flag::Flag;
349 ///
350 /// let flag = Flag::directory("output");
351 /// ```
352 #[must_use]
353 pub fn directory(name: impl Into<String>) -> Self {
354 Self::new(name).value_type(FlagType::Directory)
355 }
356
357 /// Sets the short name for this flag
358 ///
359 /// # Examples
360 ///
361 /// ```
362 /// use flag_rs::flag::Flag;
363 ///
364 /// let flag = Flag::new("verbose").short('v');
365 /// assert_eq!(flag.short, Some('v'));
366 /// ```
367 #[must_use]
368 pub const fn short(mut self, short: char) -> Self {
369 self.short = Some(short);
370 self
371 }
372
373 /// Sets the usage description for this flag
374 ///
375 /// # Examples
376 ///
377 /// ```
378 /// use flag_rs::flag::Flag;
379 ///
380 /// let flag = Flag::new("verbose").usage("Enable verbose output");
381 /// assert_eq!(flag.usage, "Enable verbose output");
382 /// ```
383 #[must_use]
384 pub fn usage(mut self, usage: impl Into<String>) -> Self {
385 self.usage = usage.into();
386 self
387 }
388
389 /// Sets the default value for this flag
390 ///
391 /// # Examples
392 ///
393 /// ```
394 /// use flag_rs::flag::{Flag, FlagValue};
395 ///
396 /// let flag = Flag::new("count").default(FlagValue::Int(10));
397 /// assert_eq!(flag.default, Some(FlagValue::Int(10)));
398 /// ```
399 #[must_use]
400 pub fn default(mut self, value: FlagValue) -> Self {
401 self.default = Some(value);
402 self
403 }
404
405 /// Sets a default boolean value
406 ///
407 /// # Examples
408 ///
409 /// ```
410 /// use flag_rs::flag::{Flag, FlagValue};
411 ///
412 /// let flag = Flag::bool("verbose").default_bool(true);
413 /// assert_eq!(flag.default, Some(FlagValue::Bool(true)));
414 /// ```
415 #[must_use]
416 pub fn default_bool(self, value: bool) -> Self {
417 self.default(FlagValue::Bool(value))
418 }
419
420 /// Sets a default string value
421 ///
422 /// # Examples
423 ///
424 /// ```
425 /// use flag_rs::flag::{Flag, FlagValue};
426 ///
427 /// let flag = Flag::string("name").default_str("anonymous");
428 /// assert_eq!(flag.default, Some(FlagValue::String("anonymous".to_string())));
429 /// ```
430 #[must_use]
431 pub fn default_str(self, value: &str) -> Self {
432 self.default(FlagValue::String(value.to_string()))
433 }
434
435 /// Sets a default integer value
436 ///
437 /// # Examples
438 ///
439 /// ```
440 /// use flag_rs::flag::{Flag, FlagValue};
441 ///
442 /// let flag = Flag::int("port").default_int(8080);
443 /// assert_eq!(flag.default, Some(FlagValue::Int(8080)));
444 /// ```
445 #[must_use]
446 pub fn default_int(self, value: i64) -> Self {
447 self.default(FlagValue::Int(value))
448 }
449
450 /// Sets a default float value
451 ///
452 /// # Examples
453 ///
454 /// ```
455 /// use flag_rs::flag::{Flag, FlagValue};
456 ///
457 /// let flag = Flag::float("ratio").default_float(0.5);
458 /// assert_eq!(flag.default, Some(FlagValue::Float(0.5)));
459 /// ```
460 #[must_use]
461 pub fn default_float(self, value: f64) -> Self {
462 self.default(FlagValue::Float(value))
463 }
464
465 /// Marks this flag as required
466 ///
467 /// # Examples
468 ///
469 /// ```
470 /// use flag_rs::flag::Flag;
471 ///
472 /// let flag = Flag::new("name").required();
473 /// assert!(flag.required);
474 /// ```
475 #[must_use]
476 pub const fn required(mut self) -> Self {
477 self.required = true;
478 self
479 }
480
481 /// Sets the value type for this flag
482 ///
483 /// # Examples
484 ///
485 /// ```
486 /// use flag_rs::flag::{Flag, FlagType};
487 ///
488 /// let flag = Flag::new("count").value_type(FlagType::Int);
489 /// ```
490 #[must_use]
491 pub fn value_type(mut self, value_type: FlagType) -> Self {
492 self.value_type = value_type;
493 self
494 }
495
496 /// Adds a constraint to this flag
497 ///
498 /// # Examples
499 ///
500 /// ```
501 /// use flag_rs::flag::{Flag, FlagConstraint};
502 ///
503 /// let flag = Flag::new("ssl")
504 /// .constraint(FlagConstraint::RequiredIf("port".to_string()))
505 /// .constraint(FlagConstraint::ConflictsWith(vec!["no-ssl".to_string()]));
506 /// ```
507 #[must_use]
508 pub fn constraint(mut self, constraint: FlagConstraint) -> Self {
509 self.constraints.push(constraint);
510 self
511 }
512
513 /// Sets a completion function for this flag's values
514 ///
515 /// # Arguments
516 ///
517 /// * `completion` - A function that generates completions for flag values
518 ///
519 /// # Examples
520 ///
521 /// ```
522 /// use flag_rs::flag::Flag;
523 /// use flag_rs::completion::CompletionResult;
524 ///
525 /// let flag = Flag::file("config")
526 /// .completion(|ctx, prefix| {
527 /// // In a real application, you might list config files
528 /// let configs = vec!["default.conf", "production.conf", "test.conf"];
529 /// Ok(CompletionResult::new().extend(
530 /// configs.into_iter()
531 /// .filter(|c| c.starts_with(prefix))
532 /// .map(String::from)
533 /// ))
534 /// });
535 /// ```
536 #[must_use]
537 pub fn completion<F>(mut self, completion: F) -> Self
538 where
539 F: Fn(&crate::Context, &str) -> Result<CompletionResult> + Send + Sync + 'static,
540 {
541 self.completion = Some(Box::new(completion));
542 self
543 }
544
545 /// Parses a string value according to this flag's type
546 ///
547 /// # Arguments
548 ///
549 /// * `input` - The string value to parse
550 ///
551 /// # Returns
552 ///
553 /// Returns the parsed `FlagValue` on success
554 ///
555 /// # Errors
556 ///
557 /// Returns `Error::FlagParsing` if the input cannot be parsed as the expected type
558 ///
559 /// # Examples
560 ///
561 /// ```
562 /// use flag_rs::flag::{Flag, FlagType, FlagValue};
563 ///
564 /// let int_flag = Flag::new("count").value_type(FlagType::Int);
565 /// match int_flag.parse_value("42") {
566 /// Ok(FlagValue::Int(n)) => assert_eq!(n, 42),
567 /// _ => panic!("Expected Int value"),
568 /// }
569 ///
570 /// let bool_flag = Flag::new("verbose").value_type(FlagType::Bool);
571 /// match bool_flag.parse_value("true") {
572 /// Ok(FlagValue::Bool(b)) => assert!(b),
573 /// _ => panic!("Expected Bool value"),
574 /// }
575 /// ```
576 pub fn parse_value(&self, input: &str) -> Result<FlagValue> {
577 match &self.value_type {
578 FlagType::String => Ok(FlagValue::String(input.to_string())),
579 FlagType::Bool => match input.to_lowercase().as_str() {
580 "true" | "t" | "1" | "yes" | "y" => Ok(FlagValue::Bool(true)),
581 "false" | "f" | "0" | "no" | "n" => Ok(FlagValue::Bool(false)),
582 _ => Err(Error::flag_parsing_with_suggestions(
583 format!("Invalid boolean value: '{input}'"),
584 self.name.clone(),
585 vec![
586 "true, false".to_string(),
587 "yes, no".to_string(),
588 "1, 0".to_string(),
589 ],
590 )),
591 },
592 FlagType::Int => input.parse::<i64>().map(FlagValue::Int).map_err(|_| {
593 Error::flag_parsing_with_suggestions(
594 format!("Invalid integer value: '{input}'"),
595 self.name.clone(),
596 vec!["a whole number (e.g., 42, -10, 0)".to_string()],
597 )
598 }),
599 FlagType::Float => input.parse::<f64>().map(FlagValue::Float).map_err(|_| {
600 Error::flag_parsing_with_suggestions(
601 format!("Invalid float value: '{input}'"),
602 self.name.clone(),
603 vec!["a decimal number (e.g., 3.14, -0.5, 1e10)".to_string()],
604 )
605 }),
606 FlagType::StringSlice | FlagType::StringArray => {
607 Ok(FlagValue::StringSlice(vec![input.to_string()]))
608 }
609 FlagType::Choice(choices) => {
610 if choices.contains(&input.to_string()) {
611 Ok(FlagValue::String(input.to_string()))
612 } else {
613 Err(Error::flag_parsing_with_suggestions(
614 format!("Invalid choice: '{input}'"),
615 self.name.clone(),
616 choices.clone(),
617 ))
618 }
619 }
620 FlagType::Range(min, max) => {
621 let value = input.parse::<i64>().map_err(|_| {
622 Error::flag_parsing_with_suggestions(
623 format!("Invalid integer value: '{input}'"),
624 self.name.clone(),
625 vec![format!("a number between {min} and {max}")],
626 )
627 })?;
628 if value >= *min && value <= *max {
629 Ok(FlagValue::Int(value))
630 } else {
631 Err(Error::flag_parsing_with_suggestions(
632 format!("Value {value} is out of range"),
633 self.name.clone(),
634 vec![format!("a number between {min} and {max} (inclusive)")],
635 ))
636 }
637 }
638 FlagType::File => Self::parse_path(input, &self.name, PathKind::File),
639 FlagType::Directory => Self::parse_path(input, &self.name, PathKind::Directory),
640 }
641 }
642
643 fn parse_path(input: &str, name: &str, kind: PathKind) -> Result<FlagValue> {
644 use std::path::Path;
645 let path = Path::new(input);
646 if path.exists() && kind.matches(path) {
647 Ok(FlagValue::String(input.to_string()))
648 } else if !path.exists() {
649 Err(Error::flag_parsing_with_suggestions(
650 format!("{} not found: '{input}'", kind.capitalized()),
651 name.to_string(),
652 vec![kind.not_found_suggestion().to_string()],
653 ))
654 } else {
655 Err(Error::flag_parsing_with_suggestions(
656 format!("Path exists but is not a {}: '{input}'", kind.lowercase()),
657 name.to_string(),
658 vec![kind.wrong_kind_suggestion().to_string()],
659 ))
660 }
661 }
662
663 /// Validates this flag's constraints against the provided flags
664 ///
665 /// # Arguments
666 ///
667 /// * `flag_name` - The name of this flag
668 /// * `provided_flags` - Set of flag names that were provided
669 ///
670 /// # Returns
671 ///
672 /// Returns `Ok(())` if all constraints are satisfied
673 ///
674 /// # Errors
675 ///
676 /// Returns `Error::FlagParsing` if any constraint is violated
677 pub fn validate_constraints(
678 &self,
679 flag_name: &str,
680 provided_flags: &HashSet<String>,
681 ) -> Result<()> {
682 for constraint in &self.constraints {
683 match constraint {
684 FlagConstraint::RequiredIf(other_flag) => {
685 if provided_flags.contains(other_flag) && !provided_flags.contains(flag_name) {
686 return Err(Error::flag_parsing_with_suggestions(
687 format!(
688 "Flag '--{flag_name}' is required when '--{other_flag}' is set"
689 ),
690 flag_name.to_string(),
691 vec![format!("add --{flag_name} <value>")],
692 ));
693 }
694 }
695 FlagConstraint::ConflictsWith(conflicting_flags) => {
696 if provided_flags.contains(flag_name) {
697 for conflict in conflicting_flags {
698 if provided_flags.contains(conflict) {
699 return Err(Error::flag_parsing_with_suggestions(
700 format!("Flag '--{flag_name}' conflicts with '--{conflict}'"),
701 flag_name.to_string(),
702 vec![format!(
703 "use either --{flag_name} or --{conflict}, not both"
704 )],
705 ));
706 }
707 }
708 }
709 }
710 FlagConstraint::Requires(required_flags) => {
711 if provided_flags.contains(flag_name) {
712 for required in required_flags {
713 if !provided_flags.contains(required) {
714 return Err(Error::flag_parsing_with_suggestions(
715 format!(
716 "Flag '--{flag_name}' requires '--{required}' to be set"
717 ),
718 flag_name.to_string(),
719 vec![format!("add --{required} <value>")],
720 ));
721 }
722 }
723 }
724 }
725 }
726 }
727 Ok(())
728 }
729}
730
731impl Clone for Flag {
732 fn clone(&self) -> Self {
733 Self {
734 name: self.name.clone(),
735 short: self.short,
736 usage: self.usage.clone(),
737 default: self.default.clone(),
738 required: self.required,
739 value_type: self.value_type.clone(),
740 constraints: self.constraints.clone(),
741 completion: None, // Don't clone the completion function
742 }
743 }
744}
745
746#[derive(Clone, Copy)]
747enum PathKind {
748 File,
749 Directory,
750}
751
752impl PathKind {
753 fn matches(self, path: &std::path::Path) -> bool {
754 match self {
755 Self::File => path.is_file(),
756 Self::Directory => path.is_dir(),
757 }
758 }
759
760 const fn capitalized(self) -> &'static str {
761 match self {
762 Self::File => "File",
763 Self::Directory => "Directory",
764 }
765 }
766
767 const fn lowercase(self) -> &'static str {
768 match self {
769 Self::File => "file",
770 Self::Directory => "directory",
771 }
772 }
773
774 const fn not_found_suggestion(self) -> &'static str {
775 match self {
776 Self::File => "path to an existing file",
777 Self::Directory => "path to an existing directory",
778 }
779 }
780
781 const fn wrong_kind_suggestion(self) -> &'static str {
782 match self {
783 Self::File => "path to a regular file (not a directory)",
784 Self::Directory => "path to a directory (not a file)",
785 }
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792 #[allow(clippy::approx_constant)]
793 const PI: f64 = 3.14;
794
795 #[test]
796 fn test_flag_value_conversions() {
797 let string_val = FlagValue::String("hello".to_string());
798 assert_eq!(string_val.as_string().unwrap(), "hello");
799 assert!(string_val.as_bool().is_err());
800
801 let bool_val = FlagValue::Bool(true);
802 assert!(bool_val.as_bool().unwrap());
803 assert!(bool_val.as_string().is_err());
804
805 let int_val = FlagValue::Int(42);
806 assert_eq!(int_val.as_int().unwrap(), 42);
807 assert!(int_val.as_float().is_err());
808
809 let float_val = FlagValue::Float(PI);
810 assert!((float_val.as_float().unwrap() - PI).abs() < f64::EPSILON);
811 assert!(float_val.as_int().is_err());
812
813 let slice_val = FlagValue::StringSlice(vec!["a".to_string(), "b".to_string()]);
814 assert_eq!(
815 slice_val.as_string_slice().unwrap(),
816 &vec!["a".to_string(), "b".to_string()]
817 );
818 assert!(slice_val.as_string().is_err());
819 }
820
821 #[test]
822 fn test_flag_parsing() {
823 let string_flag = Flag::new("name").value_type(FlagType::String);
824 assert_eq!(
825 string_flag.parse_value("test").unwrap(),
826 FlagValue::String("test".to_string())
827 );
828
829 let bool_flag = Flag::new("verbose").value_type(FlagType::Bool);
830 assert_eq!(
831 bool_flag.parse_value("true").unwrap(),
832 FlagValue::Bool(true)
833 );
834 assert_eq!(
835 bool_flag.parse_value("false").unwrap(),
836 FlagValue::Bool(false)
837 );
838 assert_eq!(bool_flag.parse_value("1").unwrap(), FlagValue::Bool(true));
839 assert_eq!(bool_flag.parse_value("0").unwrap(), FlagValue::Bool(false));
840 assert_eq!(bool_flag.parse_value("yes").unwrap(), FlagValue::Bool(true));
841 assert_eq!(bool_flag.parse_value("no").unwrap(), FlagValue::Bool(false));
842 assert!(bool_flag.parse_value("invalid").is_err());
843
844 let int_flag = Flag::new("count").value_type(FlagType::Int);
845 assert_eq!(int_flag.parse_value("42").unwrap(), FlagValue::Int(42));
846 assert_eq!(int_flag.parse_value("-10").unwrap(), FlagValue::Int(-10));
847 assert!(int_flag.parse_value("not_a_number").is_err());
848
849 let float_flag = Flag::new("ratio").value_type(FlagType::Float);
850 assert_eq!(
851 float_flag.parse_value("3.14").unwrap(),
852 FlagValue::Float(PI)
853 );
854 assert_eq!(
855 float_flag.parse_value("-2.5").unwrap(),
856 FlagValue::Float(-2.5)
857 );
858 assert!(float_flag.parse_value("not_a_float").is_err());
859 }
860
861 #[test]
862 fn test_flag_builder() {
863 let flag = Flag::new("verbose")
864 .short('v')
865 .usage("Enable verbose output")
866 .default(FlagValue::Bool(false))
867 .value_type(FlagType::Bool);
868
869 assert_eq!(flag.name, "verbose");
870 assert_eq!(flag.short, Some('v'));
871 assert_eq!(flag.usage, "Enable verbose output");
872 assert_eq!(flag.default, Some(FlagValue::Bool(false)));
873 assert!(!flag.required);
874 }
875
876 #[test]
877 fn test_choice_flag() {
878 let choice_flag = Flag::new("environment").value_type(FlagType::Choice(vec![
879 "dev".to_string(),
880 "staging".to_string(),
881 "prod".to_string(),
882 ]));
883
884 assert_eq!(
885 choice_flag.parse_value("dev").unwrap(),
886 FlagValue::String("dev".to_string())
887 );
888 assert_eq!(
889 choice_flag.parse_value("staging").unwrap(),
890 FlagValue::String("staging".to_string())
891 );
892 assert!(choice_flag.parse_value("test").is_err());
893 }
894
895 #[test]
896 fn test_range_flag() {
897 let range_flag = Flag::new("port").value_type(FlagType::Range(1024, 65535));
898
899 assert_eq!(
900 range_flag.parse_value("8080").unwrap(),
901 FlagValue::Int(8080)
902 );
903 assert_eq!(
904 range_flag.parse_value("1024").unwrap(),
905 FlagValue::Int(1024)
906 );
907 assert_eq!(
908 range_flag.parse_value("65535").unwrap(),
909 FlagValue::Int(65535)
910 );
911 assert!(range_flag.parse_value("80").is_err());
912 assert!(range_flag.parse_value("70000").is_err());
913 assert!(range_flag.parse_value("not_a_number").is_err());
914 }
915
916 #[test]
917 fn test_file_flag() {
918 use std::fs::File;
919 use std::io::Write;
920 let temp_file = "test_file_flag.tmp";
921 let mut file = File::create(temp_file).unwrap();
922 writeln!(file, "test").unwrap();
923
924 let file_flag = Flag::new("config").value_type(FlagType::File);
925 assert_eq!(
926 file_flag.parse_value(temp_file).unwrap(),
927 FlagValue::String(temp_file.to_string())
928 );
929 assert!(file_flag.parse_value("nonexistent.file").is_err());
930
931 std::fs::remove_file(temp_file).unwrap();
932 }
933
934 #[test]
935 fn test_directory_flag() {
936 let dir_flag = Flag::new("output").value_type(FlagType::Directory);
937
938 // Test with current directory
939 assert_eq!(
940 dir_flag.parse_value(".").unwrap(),
941 FlagValue::String(".".to_string())
942 );
943
944 // Test with src directory (should exist in the project)
945 assert_eq!(
946 dir_flag.parse_value("src").unwrap(),
947 FlagValue::String("src".to_string())
948 );
949
950 assert!(dir_flag.parse_value("nonexistent_directory").is_err());
951 }
952
953 #[test]
954 fn test_string_array_flag() {
955 let array_flag = Flag::new("tags").value_type(FlagType::StringArray);
956
957 assert_eq!(
958 array_flag.parse_value("tag1").unwrap(),
959 FlagValue::StringSlice(vec!["tag1".to_string()])
960 );
961 }
962
963 #[test]
964 fn test_flag_constraints() {
965 let mut provided_flags = HashSet::new();
966
967 // Test RequiredIf constraint
968 let ssl_flag = Flag::new("ssl").constraint(FlagConstraint::RequiredIf("port".to_string()));
969
970 // Should pass when port flag is not set
971 assert!(
972 ssl_flag
973 .validate_constraints("ssl", &provided_flags)
974 .is_ok()
975 );
976
977 // Should fail when port is set but ssl is not
978 provided_flags.insert("port".to_string());
979 assert!(
980 ssl_flag
981 .validate_constraints("ssl", &provided_flags)
982 .is_err()
983 );
984
985 // Should pass when both are set
986 provided_flags.insert("ssl".to_string());
987 assert!(
988 ssl_flag
989 .validate_constraints("ssl", &provided_flags)
990 .is_ok()
991 );
992
993 // Test ConflictsWith constraint
994 let encrypt_flag = Flag::new("encrypt").constraint(FlagConstraint::ConflictsWith(vec![
995 "no-encrypt".to_string(),
996 ]));
997
998 provided_flags.clear();
999 provided_flags.insert("encrypt".to_string());
1000 assert!(
1001 encrypt_flag
1002 .validate_constraints("encrypt", &provided_flags)
1003 .is_ok()
1004 );
1005
1006 provided_flags.insert("no-encrypt".to_string());
1007 assert!(
1008 encrypt_flag
1009 .validate_constraints("encrypt", &provided_flags)
1010 .is_err()
1011 );
1012
1013 // Test Requires constraint
1014 let output_flag =
1015 Flag::new("output").constraint(FlagConstraint::Requires(vec!["format".to_string()]));
1016
1017 provided_flags.clear();
1018 provided_flags.insert("output".to_string());
1019 assert!(
1020 output_flag
1021 .validate_constraints("output", &provided_flags)
1022 .is_err()
1023 );
1024
1025 provided_flags.insert("format".to_string());
1026 assert!(
1027 output_flag
1028 .validate_constraints("output", &provided_flags)
1029 .is_ok()
1030 );
1031 }
1032}