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
546impl Clone for Flag {
547 fn clone(&self) -> Self {
548 Self {
549 name: self.name.clone(),
550 short: self.short,
551 usage: self.usage.clone(),
552 default: self.default.clone(),
553 required: self.required,
554 value_type: self.value_type.clone(),
555 constraints: self.constraints.clone(),
556 completion: None, // Don't clone the completion function
557 }
558 }
559}
560
561impl Flag {
562 /// Parses a string value according to this flag's type
563 ///
564 /// # Arguments
565 ///
566 /// * `input` - The string value to parse
567 ///
568 /// # Returns
569 ///
570 /// Returns the parsed `FlagValue` on success
571 ///
572 /// # Errors
573 ///
574 /// Returns `Error::FlagParsing` if the input cannot be parsed as the expected type
575 ///
576 /// # Examples
577 ///
578 /// ```
579 /// use flag_rs::flag::{Flag, FlagType, FlagValue};
580 ///
581 /// let int_flag = Flag::new("count").value_type(FlagType::Int);
582 /// match int_flag.parse_value("42") {
583 /// Ok(FlagValue::Int(n)) => assert_eq!(n, 42),
584 /// _ => panic!("Expected Int value"),
585 /// }
586 ///
587 /// let bool_flag = Flag::new("verbose").value_type(FlagType::Bool);
588 /// match bool_flag.parse_value("true") {
589 /// Ok(FlagValue::Bool(b)) => assert!(b),
590 /// _ => panic!("Expected Bool value"),
591 /// }
592 /// ```
593 pub fn parse_value(&self, input: &str) -> Result<FlagValue> {
594 match &self.value_type {
595 FlagType::String => Ok(FlagValue::String(input.to_string())),
596 FlagType::Bool => match input.to_lowercase().as_str() {
597 "true" | "t" | "1" | "yes" | "y" => Ok(FlagValue::Bool(true)),
598 "false" | "f" | "0" | "no" | "n" => Ok(FlagValue::Bool(false)),
599 _ => Err(Error::flag_parsing_with_suggestions(
600 format!("Invalid boolean value: '{input}'"),
601 self.name.clone(),
602 vec![
603 "true, false".to_string(),
604 "yes, no".to_string(),
605 "1, 0".to_string(),
606 ],
607 )),
608 },
609 FlagType::Int => input.parse::<i64>().map(FlagValue::Int).map_err(|_| {
610 Error::flag_parsing_with_suggestions(
611 format!("Invalid integer value: '{input}'"),
612 self.name.clone(),
613 vec!["a whole number (e.g., 42, -10, 0)".to_string()],
614 )
615 }),
616 FlagType::Float => input.parse::<f64>().map(FlagValue::Float).map_err(|_| {
617 Error::flag_parsing_with_suggestions(
618 format!("Invalid float value: '{input}'"),
619 self.name.clone(),
620 vec!["a decimal number (e.g., 3.14, -0.5, 1e10)".to_string()],
621 )
622 }),
623 FlagType::StringSlice | FlagType::StringArray => {
624 Ok(FlagValue::StringSlice(vec![input.to_string()]))
625 }
626 FlagType::Choice(choices) => {
627 if choices.contains(&input.to_string()) {
628 Ok(FlagValue::String(input.to_string()))
629 } else {
630 Err(Error::flag_parsing_with_suggestions(
631 format!("Invalid choice: '{input}'"),
632 self.name.clone(),
633 choices.clone(),
634 ))
635 }
636 }
637 FlagType::Range(min, max) => {
638 let value = input.parse::<i64>().map_err(|_| {
639 Error::flag_parsing_with_suggestions(
640 format!("Invalid integer value: '{input}'"),
641 self.name.clone(),
642 vec![format!("a number between {min} and {max}")],
643 )
644 })?;
645 if value >= *min && value <= *max {
646 Ok(FlagValue::Int(value))
647 } else {
648 Err(Error::flag_parsing_with_suggestions(
649 format!("Value {value} is out of range"),
650 self.name.clone(),
651 vec![format!("a number between {min} and {max} (inclusive)")],
652 ))
653 }
654 }
655 FlagType::File => {
656 use std::path::Path;
657 let path = Path::new(input);
658 if path.exists() && path.is_file() {
659 Ok(FlagValue::String(input.to_string()))
660 } else if !path.exists() {
661 Err(Error::flag_parsing_with_suggestions(
662 format!("File not found: '{input}'"),
663 self.name.clone(),
664 vec!["path to an existing file".to_string()],
665 ))
666 } else {
667 Err(Error::flag_parsing_with_suggestions(
668 format!("Path exists but is not a file: '{input}'"),
669 self.name.clone(),
670 vec!["path to a regular file (not a directory)".to_string()],
671 ))
672 }
673 }
674 FlagType::Directory => {
675 use std::path::Path;
676 let path = Path::new(input);
677 if path.exists() && path.is_dir() {
678 Ok(FlagValue::String(input.to_string()))
679 } else if !path.exists() {
680 Err(Error::flag_parsing_with_suggestions(
681 format!("Directory not found: '{input}'"),
682 self.name.clone(),
683 vec!["path to an existing directory".to_string()],
684 ))
685 } else {
686 Err(Error::flag_parsing_with_suggestions(
687 format!("Path exists but is not a directory: '{input}'"),
688 self.name.clone(),
689 vec!["path to a directory (not a file)".to_string()],
690 ))
691 }
692 }
693 }
694 }
695
696 /// Validates this flag's constraints against the provided flags
697 ///
698 /// # Arguments
699 ///
700 /// * `flag_name` - The name of this flag
701 /// * `provided_flags` - Set of flag names that were provided
702 ///
703 /// # Returns
704 ///
705 /// Returns `Ok(())` if all constraints are satisfied
706 ///
707 /// # Errors
708 ///
709 /// Returns `Error::FlagParsing` if any constraint is violated
710 pub fn validate_constraints(
711 &self,
712 flag_name: &str,
713 provided_flags: &HashSet<String>,
714 ) -> Result<()> {
715 for constraint in &self.constraints {
716 match constraint {
717 FlagConstraint::RequiredIf(other_flag) => {
718 if provided_flags.contains(other_flag) && !provided_flags.contains(flag_name) {
719 return Err(Error::flag_parsing_with_suggestions(
720 format!(
721 "Flag '--{flag_name}' is required when '--{other_flag}' is set"
722 ),
723 flag_name.to_string(),
724 vec![format!("add --{flag_name} <value>")],
725 ));
726 }
727 }
728 FlagConstraint::ConflictsWith(conflicting_flags) => {
729 if provided_flags.contains(flag_name) {
730 for conflict in conflicting_flags {
731 if provided_flags.contains(conflict) {
732 return Err(Error::flag_parsing_with_suggestions(
733 format!("Flag '--{flag_name}' conflicts with '--{conflict}'"),
734 flag_name.to_string(),
735 vec![format!(
736 "use either --{flag_name} or --{conflict}, not both"
737 )],
738 ));
739 }
740 }
741 }
742 }
743 FlagConstraint::Requires(required_flags) => {
744 if provided_flags.contains(flag_name) {
745 for required in required_flags {
746 if !provided_flags.contains(required) {
747 return Err(Error::flag_parsing_with_suggestions(
748 format!(
749 "Flag '--{flag_name}' requires '--{required}' to be set"
750 ),
751 flag_name.to_string(),
752 vec![format!("add --{required} <value>")],
753 ));
754 }
755 }
756 }
757 }
758 }
759 }
760 Ok(())
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 #[allow(clippy::approx_constant)]
768 const PI: f64 = 3.14;
769
770 #[test]
771 fn test_flag_value_conversions() {
772 let string_val = FlagValue::String("hello".to_string());
773 assert_eq!(string_val.as_string().unwrap(), "hello");
774 assert!(string_val.as_bool().is_err());
775
776 let bool_val = FlagValue::Bool(true);
777 assert!(bool_val.as_bool().unwrap());
778 assert!(bool_val.as_string().is_err());
779
780 let int_val = FlagValue::Int(42);
781 assert_eq!(int_val.as_int().unwrap(), 42);
782 assert!(int_val.as_float().is_err());
783
784 let float_val = FlagValue::Float(PI);
785 assert!((float_val.as_float().unwrap() - PI).abs() < f64::EPSILON);
786 assert!(float_val.as_int().is_err());
787
788 let slice_val = FlagValue::StringSlice(vec!["a".to_string(), "b".to_string()]);
789 assert_eq!(
790 slice_val.as_string_slice().unwrap(),
791 &vec!["a".to_string(), "b".to_string()]
792 );
793 assert!(slice_val.as_string().is_err());
794 }
795
796 #[test]
797 fn test_flag_parsing() {
798 let string_flag = Flag::new("name").value_type(FlagType::String);
799 assert_eq!(
800 string_flag.parse_value("test").unwrap(),
801 FlagValue::String("test".to_string())
802 );
803
804 let bool_flag = Flag::new("verbose").value_type(FlagType::Bool);
805 assert_eq!(
806 bool_flag.parse_value("true").unwrap(),
807 FlagValue::Bool(true)
808 );
809 assert_eq!(
810 bool_flag.parse_value("false").unwrap(),
811 FlagValue::Bool(false)
812 );
813 assert_eq!(bool_flag.parse_value("1").unwrap(), FlagValue::Bool(true));
814 assert_eq!(bool_flag.parse_value("0").unwrap(), FlagValue::Bool(false));
815 assert_eq!(bool_flag.parse_value("yes").unwrap(), FlagValue::Bool(true));
816 assert_eq!(bool_flag.parse_value("no").unwrap(), FlagValue::Bool(false));
817 assert!(bool_flag.parse_value("invalid").is_err());
818
819 let int_flag = Flag::new("count").value_type(FlagType::Int);
820 assert_eq!(int_flag.parse_value("42").unwrap(), FlagValue::Int(42));
821 assert_eq!(int_flag.parse_value("-10").unwrap(), FlagValue::Int(-10));
822 assert!(int_flag.parse_value("not_a_number").is_err());
823
824 let float_flag = Flag::new("ratio").value_type(FlagType::Float);
825 assert_eq!(
826 float_flag.parse_value("3.14").unwrap(),
827 FlagValue::Float(PI)
828 );
829 assert_eq!(
830 float_flag.parse_value("-2.5").unwrap(),
831 FlagValue::Float(-2.5)
832 );
833 assert!(float_flag.parse_value("not_a_float").is_err());
834 }
835
836 #[test]
837 fn test_flag_builder() {
838 let flag = Flag::new("verbose")
839 .short('v')
840 .usage("Enable verbose output")
841 .default(FlagValue::Bool(false))
842 .value_type(FlagType::Bool);
843
844 assert_eq!(flag.name, "verbose");
845 assert_eq!(flag.short, Some('v'));
846 assert_eq!(flag.usage, "Enable verbose output");
847 assert_eq!(flag.default, Some(FlagValue::Bool(false)));
848 assert!(!flag.required);
849 }
850
851 #[test]
852 fn test_choice_flag() {
853 let choice_flag = Flag::new("environment").value_type(FlagType::Choice(vec![
854 "dev".to_string(),
855 "staging".to_string(),
856 "prod".to_string(),
857 ]));
858
859 assert_eq!(
860 choice_flag.parse_value("dev").unwrap(),
861 FlagValue::String("dev".to_string())
862 );
863 assert_eq!(
864 choice_flag.parse_value("staging").unwrap(),
865 FlagValue::String("staging".to_string())
866 );
867 assert!(choice_flag.parse_value("test").is_err());
868 }
869
870 #[test]
871 fn test_range_flag() {
872 let range_flag = Flag::new("port").value_type(FlagType::Range(1024, 65535));
873
874 assert_eq!(
875 range_flag.parse_value("8080").unwrap(),
876 FlagValue::Int(8080)
877 );
878 assert_eq!(
879 range_flag.parse_value("1024").unwrap(),
880 FlagValue::Int(1024)
881 );
882 assert_eq!(
883 range_flag.parse_value("65535").unwrap(),
884 FlagValue::Int(65535)
885 );
886 assert!(range_flag.parse_value("80").is_err());
887 assert!(range_flag.parse_value("70000").is_err());
888 assert!(range_flag.parse_value("not_a_number").is_err());
889 }
890
891 #[test]
892 fn test_file_flag() {
893 use std::fs::File;
894 use std::io::Write;
895 let temp_file = "test_file_flag.tmp";
896 let mut file = File::create(temp_file).unwrap();
897 writeln!(file, "test").unwrap();
898
899 let file_flag = Flag::new("config").value_type(FlagType::File);
900 assert_eq!(
901 file_flag.parse_value(temp_file).unwrap(),
902 FlagValue::String(temp_file.to_string())
903 );
904 assert!(file_flag.parse_value("nonexistent.file").is_err());
905
906 std::fs::remove_file(temp_file).unwrap();
907 }
908
909 #[test]
910 fn test_directory_flag() {
911 let dir_flag = Flag::new("output").value_type(FlagType::Directory);
912
913 // Test with current directory
914 assert_eq!(
915 dir_flag.parse_value(".").unwrap(),
916 FlagValue::String(".".to_string())
917 );
918
919 // Test with src directory (should exist in the project)
920 assert_eq!(
921 dir_flag.parse_value("src").unwrap(),
922 FlagValue::String("src".to_string())
923 );
924
925 assert!(dir_flag.parse_value("nonexistent_directory").is_err());
926 }
927
928 #[test]
929 fn test_string_array_flag() {
930 let array_flag = Flag::new("tags").value_type(FlagType::StringArray);
931
932 assert_eq!(
933 array_flag.parse_value("tag1").unwrap(),
934 FlagValue::StringSlice(vec!["tag1".to_string()])
935 );
936 }
937
938 #[test]
939 fn test_flag_constraints() {
940 let mut provided_flags = HashSet::new();
941
942 // Test RequiredIf constraint
943 let ssl_flag = Flag::new("ssl").constraint(FlagConstraint::RequiredIf("port".to_string()));
944
945 // Should pass when port flag is not set
946 assert!(
947 ssl_flag
948 .validate_constraints("ssl", &provided_flags)
949 .is_ok()
950 );
951
952 // Should fail when port is set but ssl is not
953 provided_flags.insert("port".to_string());
954 assert!(
955 ssl_flag
956 .validate_constraints("ssl", &provided_flags)
957 .is_err()
958 );
959
960 // Should pass when both are set
961 provided_flags.insert("ssl".to_string());
962 assert!(
963 ssl_flag
964 .validate_constraints("ssl", &provided_flags)
965 .is_ok()
966 );
967
968 // Test ConflictsWith constraint
969 let encrypt_flag = Flag::new("encrypt").constraint(FlagConstraint::ConflictsWith(vec![
970 "no-encrypt".to_string(),
971 ]));
972
973 provided_flags.clear();
974 provided_flags.insert("encrypt".to_string());
975 assert!(
976 encrypt_flag
977 .validate_constraints("encrypt", &provided_flags)
978 .is_ok()
979 );
980
981 provided_flags.insert("no-encrypt".to_string());
982 assert!(
983 encrypt_flag
984 .validate_constraints("encrypt", &provided_flags)
985 .is_err()
986 );
987
988 // Test Requires constraint
989 let output_flag =
990 Flag::new("output").constraint(FlagConstraint::Requires(vec!["format".to_string()]));
991
992 provided_flags.clear();
993 provided_flags.insert("output".to_string());
994 assert!(
995 output_flag
996 .validate_constraints("output", &provided_flags)
997 .is_err()
998 );
999
1000 provided_flags.insert("format".to_string());
1001 assert!(
1002 output_flag
1003 .validate_constraints("output", &provided_flags)
1004 .is_ok()
1005 );
1006 }
1007}