From 41fe0580212b3a681fd767d331fd8875ee59b019 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 16 Jul 2026 18:58:31 +0800 Subject: feat(picker): add `Flag` value type and `Pickable` implementation Add a new `Flag` enum in `mingling_picker::value` that explicitly distinguishes between an absent flag (`Inactive`) and a present flag (`Active`), along with its `Pickable` implementation. Rename internal fields from `flag_*` to `arg_*` for consistency, update the default naming case to `Kebab`, and enable the `mingling_support` feature in workspace settings. --- .vscode/settings.json | 1 + .zed/settings.json | 3 + mingling_picker/src/arg.rs | 14 ++ mingling_picker/src/builtin.rs | 1 + mingling_picker/src/builtin/pick_flag.rs | 21 +++ mingling_picker/src/lib.rs | 3 + mingling_picker/src/parselib.rs | 3 + mingling_picker/src/parselib/arg_matcher.rs | 21 +++ mingling_picker/src/parselib/style.rs | 4 +- mingling_picker/src/picker.rs | 8 +- mingling_picker/src/picker/parse.rs | 10 +- mingling_picker/src/picker/patterns.rs | 52 +++---- mingling_picker/src/value.rs | 134 +++++++++++++++++ mingling_picker/test/src/test.rs | 1 + mingling_picker/test/src/test/basic_test.rs | 8 +- mingling_picker/test/src/test/route_test.rs | 2 +- mingling_picker/test/src/test/style_test.rs | 95 ++++++++++++- mingling_picker/test/src/test/value_flag_test.rs | 174 +++++++++++++++++++++++ 18 files changed, 509 insertions(+), 46 deletions(-) create mode 100644 mingling_picker/src/builtin/pick_flag.rs create mode 100644 mingling_picker/src/parselib/arg_matcher.rs create mode 100644 mingling_picker/src/value.rs create mode 100644 mingling_picker/test/src/test/value_flag_test.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f1ff1d..7472e1c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,4 +7,5 @@ "mingling_pathf/test/Cargo.toml", "mingling_picker/test/Cargo.toml", ], + "rust-analyzer.cargo.features": ["mingling_support"], } diff --git a/.zed/settings.json b/.zed/settings.json index 3ecbbe3..12c901e 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -10,6 +10,9 @@ "mingling_pathf/test/Cargo.toml", "mingling_picker/test/Cargo.toml", ], + "cargo": { + "features": ["mingling_support"], + }, }, }, }, diff --git a/mingling_picker/src/arg.rs b/mingling_picker/src/arg.rs index 3b5f8b1..a18a616 100644 --- a/mingling_picker/src/arg.rs +++ b/mingling_picker/src/arg.rs @@ -39,6 +39,20 @@ where pub internal_type: PhantomData, } +impl<'a, Type> From<&'a PickerArg<'a, Type>> for PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + fn from(value: &'a PickerArg<'a, Type>) -> Self { + PickerArg { + full: value.full, + short: value.short, + positional: value.positional, + internal_type: PhantomData, + } + } +} + impl<'a, Type> PickerArg<'a, Type> where Type: Pickable<'a>, diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs index d6c4085..00fe1e5 100644 --- a/mingling_picker/src/builtin.rs +++ b/mingling_picker/src/builtin.rs @@ -1 +1,2 @@ mod pick_bool; +mod pick_flag; diff --git a/mingling_picker/src/builtin/pick_flag.rs b/mingling_picker/src/builtin/pick_flag.rs new file mode 100644 index 0000000..b642a9a --- /dev/null +++ b/mingling_picker/src/builtin/pick_flag.rs @@ -0,0 +1,21 @@ +use crate::parselib::{FlagMatcher, Matcher}; +use crate::pickable_needed::*; +use crate::value::Flag; + +impl<'a> Pickable<'a> for Flag { + fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::Flag + } + + fn tag(ctx: TagPhaseContext) -> Vec { + FlagMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult { + if raw_strs.is_empty() { + PickerArgResult::Parsed(Flag::Inactive) + } else { + PickerArgResult::Parsed(Flag::Active) + } + } +} diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs index c3c15b9..afc7aca 100644 --- a/mingling_picker/src/lib.rs +++ b/mingling_picker/src/lib.rs @@ -14,6 +14,8 @@ pub use infos::*; pub mod parselib; +pub mod value; + pub mod prelude { pub use crate::IntoPicker; } @@ -36,5 +38,6 @@ pub mod matcher_needed { #[cfg(feature = "mingling_support")] mod corebind; +#[allow(unused_imports)] #[cfg(feature = "mingling_support")] pub use corebind::*; diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs index cf550b7..49e3cc2 100644 --- a/mingling_picker/src/parselib.rs +++ b/mingling_picker/src/parselib.rs @@ -1,6 +1,9 @@ mod flag_matcher; pub use flag_matcher::*; +mod arg_matcher; +pub use arg_matcher::*; + mod style; pub use style::*; diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs new file mode 100644 index 0000000..e8be999 --- /dev/null +++ b/mingling_picker/src/parselib/arg_matcher.rs @@ -0,0 +1,21 @@ +use crate::matcher_needed::*; + +pub struct ArgMatcher; + +impl Matcher for ArgMatcher { + fn on_match_one( + _args: &[MaskedArg], + _style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Option { + todo!() + } + + fn on_match_all( + _args: &[MaskedArg], + _style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Vec { + todo!() + } +} diff --git a/mingling_picker/src/parselib/style.rs b/mingling_picker/src/parselib/style.rs index 434b545..4ea161f 100644 --- a/mingling_picker/src/parselib/style.rs +++ b/mingling_picker/src/parselib/style.rs @@ -1,7 +1,7 @@ use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; -use crate::parselib::ParserStyleNamingCase::{Pascal, Snake}; +use crate::parselib::ParserStyleNamingCase::{Kebab, Pascal}; /// Defines the style of command-line argument parsing (prefixes, separators, etc.). #[derive(Clone, Copy, PartialEq, Eq)] @@ -175,7 +175,7 @@ pub const UNIX_STYLE: ParserStyle = ParserStyle { value_separator: '=', case_sensitive: true, allow_combine: true, - naming_case: Snake, + naming_case: Kebab, }; /// PowerShell style (e.g., `-Verbose`, `-Name:value`) diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs index c6e40c8..4473d76 100644 --- a/mingling_picker/src/picker.rs +++ b/mingling_picker/src/picker.rs @@ -270,18 +270,18 @@ pub trait IntoPicker<'a> { /// ``` fn to_picker(self) -> Picker<'a, ()>; - /// Creates a `PickerPattern1` from the given flag for the `pick` method. + /// Creates a `PickerPattern1` from the given arg for the `pick` method. /// /// This method converts the value into a `Picker` and starts a parameter - /// picking chain with one flag. The result is initially `Unparsed`. - fn pick(self, flag: &'a PickerArg<'a, N>) -> PickerPattern1<'a, N, ()> + /// picking chain with one arg. The result is initially `Unparsed`. + fn pick(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()> where Self: Sized, N: Pickable<'a> + Default + Sized, { PickerPattern1 { args: self.to_picker().args, - flag_1: flag, + arg_1: arg.into(), result_1: PickerArgResult::Unparsed, default_1: None, route_1: None, diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs index 69de583..8f7d514 100644 --- a/mingling_picker/src/picker/parse.rs +++ b/mingling_picker/src/picker/parse.rs @@ -67,13 +67,13 @@ internal_repeat!(1..=32 => { // ArgInfos let arg_infos: [PickerArgInfo; $] = [ ( - PickerArgInfo::from(self.flag_$), + PickerArgInfo::from(self.arg_$), +) ]; let mut bundle: [ ( - // Flag Attr + // Arg Attr PickerArgAttr, // Tag Func @@ -88,8 +88,8 @@ internal_repeat!(1..=32 => { ; $] = [ ( ( - // Flag Attr - T$::get_attr(self.flag_$), + // Arg Attr + T$::get_attr(self.arg_$), // Tag Func Box::new(|args, mask| { @@ -143,7 +143,7 @@ internal_repeat!(1..=32 => { // Sort by Bundle Ord (descending) bundle.sort_by(|a, b| b.0.cmp(&a.0)); - // Mask — size = number of args (not flags), so use args length + // Mask — size = number of args (not args), so use args length let mut mask: Vec = vec![0u8; self.args.len()]; // Parsing diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs index df04ab9..3c6e73f 100644 --- a/mingling_picker/src/picker/patterns.rs +++ b/mingling_picker/src/picker/patterns.rs @@ -10,7 +10,7 @@ internal_repeat!(1..=32 => { pub args: PickerArgs<'a>, pub error_route: Option, ( - pub flag_$: &'a PickerArg<'a, T$>, + pub arg_$: &'a PickerArg<'a, T$>, pub result_$: PickerArgResult, pub default_$: Option T$>>, pub route_$: Option Route>>, @@ -23,16 +23,16 @@ internal_repeat!(1..=32 => { impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route> where (T$: Pickable<'a>,+) { - /// Sets a default value provider for this flag. + /// Sets a default value provider for this arg. /// - /// If the flag is not provided by the user at runtime, the given closure will be + /// If the arg is not provided by the user at runtime, the given closure will be /// called to produce a default value. The closure is expected to return `T$`. /// /// # Example /// /// ```ignore /// let pattern = picker - /// .pick(&my_flag) + /// .pick(&my_arg) /// .or(|| 42); /// ``` #[allow(clippy::type_complexity)] @@ -45,16 +45,16 @@ internal_repeat!(1..=32 => { self } - /// Uses the default value for this flag's type if the flag is not provided. + /// Uses the default value for this arg's type if the arg is not provided. /// - /// If the flag is not provided by the user at runtime, the default value for `T$` + /// If the arg is not provided by the user at runtime, the default value for `T$` /// (as defined by the `Default` trait) will be used. /// /// # Example /// /// ```ignore /// let pattern = picker - /// .pick(&my_flag) + /// .pick(&my_arg) /// .or_default(); /// ``` #[allow(clippy::type_complexity)] @@ -66,16 +66,16 @@ internal_repeat!(1..=32 => { self } - /// Sets a route for when the flag is not provided. + /// Sets a route for when the arg is not provided. /// - /// If the flag is not provided by the user at runtime, the given closure will be + /// If the arg is not provided by the user at runtime, the given closure will be /// called to produce a route value that will be returned early. /// /// # Example /// /// ```ignore /// let pattern = picker - /// .pick(&my_flag) + /// .pick(&my_arg) /// .or_route(|| Redirect::home()); /// ``` pub fn or_route(mut self, func: F) -> Self @@ -91,7 +91,7 @@ internal_repeat!(1..=32 => { /// Resets the route for this picker pattern, allowing a different route type. /// /// This method converts the current `PickerPattern` into a new one with a different - /// route type `NewRoute`. All existing flag configurations, defaults, and post- + /// route type `NewRoute`. All existing arg configurations, defaults, and post- /// processing functions are preserved, but the `error_route` and individual /// `route_$` fields are cleared (set to `None`). /// @@ -104,7 +104,7 @@ internal_repeat!(1..=32 => { args: self.args, error_route: None, ( - flag_$: self.flag_$, + arg_$: self.arg_$, result_$: self.result_$, default_$: self.default_$, route_$: None, @@ -113,9 +113,9 @@ internal_repeat!(1..=32 => { } } - /// Attaches a post-processing function to this flag. + /// Attaches a post-processing function to this arg. /// - /// After the flag's value is parsed (or defaulted), the given closure will be + /// After the arg's value is parsed (or defaulted), the given closure will be /// invoked with the parsed value and its return value will be used as the final /// result. This allows transforming or validating the parsed value. /// @@ -123,7 +123,7 @@ internal_repeat!(1..=32 => { /// /// ```ignore /// let pattern = picker - /// .pick(&my_flag) + /// .pick(&my_arg) /// .post(|val| val * 2); /// ``` #[allow(clippy::type_complexity)] @@ -143,12 +143,12 @@ internal_repeat!(1..32 => { where (T$: Pickable<'a>,+) { #[allow(clippy::type_complexity)] - /// Adds a new flag to the picking chain, returning a new `PickerPattern` with one more type parameter. + /// Adds a new arg to the picking chain, returning a new `PickerPattern` with one more type parameter. /// - /// This method extends the current picking pattern by appending an additional flag. - /// The previous flags and their results are preserved as part of the new pattern. - /// The new flag's result is initially `Unparsed`. - pub fn pick(self, flag: &'a PickerArg<'a, N>) -> PickerPattern$+<'a, (T$,+), N, Route> + /// This method extends the current picking pattern by appending an additional arg. + /// The previous args and their results are preserved as part of the new pattern. + /// The new arg's result is initially `Unparsed`. + pub fn pick(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route> where N: Pickable<'a>, { @@ -158,7 +158,7 @@ internal_repeat!(1..32 => { error_route: self.error_route, // Current - flag_$+: flag, + arg_$+: arg.into(), result_$+: PickerArgResult::Unparsed, default_$+: None, route_$+: None, @@ -166,7 +166,7 @@ internal_repeat!(1..32 => { // Prev ( - flag_$: self.flag_$, + arg_$: self.arg_$, result_$: self.result_$, default_$: self.default_$, route_$: self.route_$, @@ -178,18 +178,18 @@ internal_repeat!(1..32 => { }); impl<'a, Route> Picker<'a, Route> { - /// Creates a `PickerPattern1` from the given flag to start a picking chain. + /// Creates a `PickerPattern1` from the given arg to start a picking chain. /// - /// This method initiates a parameter picking chain with one flag. + /// This method initiates a parameter picking chain with one arg. /// The result is initially `Unparsed`. - pub fn pick(self, flag: &'a PickerArg<'a, N>) -> PickerPattern1<'a, N, Route> + pub fn pick(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route> where N: Pickable<'a>, { PickerPattern1 { args: self.args, error_route: None::, - flag_1: flag, + arg_1: arg.into(), result_1: PickerArgResult::Unparsed, route_1: None, default_1: None, diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs new file mode 100644 index 0000000..7d52442 --- /dev/null +++ b/mingling_picker/src/value.rs @@ -0,0 +1,134 @@ +use std::{ + fmt::{Debug, Display}, + ops::Deref, +}; + +/// Parsed result of a boolean-style command-line flag. +/// +/// `Flag` is a **value type** that can be declared in [`PickerArg`]. +/// When the user passes `--verbose` on the command line, the parsed result is `Flag::Active`; +/// when the flag is absent, the result is `Flag::Inactive`. +/// +/// # Why not just `bool`? +/// +/// Unlike a raw `bool`, `Flag` carries **explicit semantics** about whether +/// the flag was actually provided by the user (`Active`) or simply omitted +/// (`Inactive`). This distinction matters when you want to distinguish +/// "the user intentionally omitted the flag" from "the flag was processed but +/// resolved to false" — the `Pickable` implementation for `Flag` always +/// returns `Parsed(Flag::Inactive)` when no matching argument is found, +/// rather than `NotFound`, making it always succeed with a meaningful default. +/// +/// # Conversions +/// +/// `Flag` interoperates seamlessly with `bool`: `Flag::Active` is `true`, +/// `Flag::Inactive` is `false`. The [`Deref`] impl allows using a `Flag` +/// directly in boolean contexts: +/// +/// ``` +/// # use mingling_picker::value::Flag; +/// let flag = Flag::Active; +/// if *flag { /* runs */ } +/// ``` +/// +/// [`PickerArg`]: crate::PickerArg +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum Flag { + /// The flag was **not** present on the command line. + /// + /// This is the default state, equivalent to `false`. + #[default] + Inactive, + + /// The flag **was** present on the command line. + /// + /// Equivalent to `true`. + Active, +} + +impl Debug for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Inactive => write!(f, "inactive"), + Self::Active => write!(f, "active"), + } + } +} + +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Inactive => write!(f, "inactive"), + Self::Active => write!(f, "active"), + } + } +} + +impl Flag { + /// Converts this `Flag` into a `bool`. + /// + /// Returns `true` if the flag is [`Active`], `false` if [`Inactive`]. + /// + /// # Examples + /// + /// ``` + /// # use mingling_picker::value::Flag; + /// assert!(Flag::Active.bool()); + /// assert!(!Flag::Inactive.bool()); + /// ``` + /// + /// [`Active`]: Flag::Active + /// [`Inactive`]: Flag::Inactive + #[must_use] + #[inline(always)] + pub fn bool(&self) -> bool { + *self == Flag::Active + } +} + +impl PartialEq for Flag { + fn eq(&self, other: &bool) -> bool { + self.bool() == *other + } +} + +/// Compares `bool` with `Flag` using `==`. +impl PartialEq for bool { + fn eq(&self, other: &Flag) -> bool { + *self == other.bool() + } +} + +impl From for Flag { + fn from(value: bool) -> Self { + if value { Flag::Active } else { Flag::Inactive } + } +} + +impl From for bool { + fn from(val: Flag) -> Self { + val == Flag::Active + } +} + +/// Allows `Flag` to be used in boolean contexts via `*flag`. +/// +/// # Examples +/// +/// ``` +/// # use mingling_picker::value::Flag; +/// let flag = Flag::Active; +/// if *flag { +/// println!("flag is set"); +/// } +/// ``` +impl Deref for Flag { + type Target = bool; + + fn deref(&self) -> &bool { + match self { + Flag::Active => &true, + Flag::Inactive => &false, + } + } +} diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs index d3e19b3..29d52ec 100644 --- a/mingling_picker/test/src/test.rs +++ b/mingling_picker/test/src/test.rs @@ -1,3 +1,4 @@ mod basic_test; mod route_test; mod style_test; +mod value_flag_test; diff --git a/mingling_picker/test/src/test/basic_test.rs b/mingling_picker/test/src/test/basic_test.rs index bb67b8f..78b154e 100644 --- a/mingling_picker/test/src/test/basic_test.rs +++ b/mingling_picker/test/src/test/basic_test.rs @@ -41,8 +41,8 @@ fn test_bool_short_flag_present() { #[test] fn test_two_bool_flags_both_present() { - // The `arg!` macro expands the identifier `flag_a` into the string "flag_a" → --flag_a - let args = vec!["--flag_a", "--flag_b"]; + // UNIX_STYLE now uses Kebab naming: `flag_a` → "flag-a" → --flag-a + let args = vec!["--flag-a", "--flag-b"]; let (a, b) = args .to_picker() .pick(&arg![flag_a: bool]) @@ -56,7 +56,7 @@ fn test_two_bool_flags_both_present() { #[test] fn test_two_bool_flags_one_present() { - let args = vec!["--flag_a"]; + let args = vec!["--flag-a"]; let (a, b) = args .to_picker() .pick(&arg![flag_a: bool]) @@ -86,7 +86,7 @@ fn test_two_bool_flags_neither_present() { #[test] fn test_short_and_long_flags() { - let args = vec!["-a", "--long_b"]; + let args = vec!["-a", "--long-b"]; let (a, b) = args .to_picker() .pick(&arg![flag_a: bool, 'a']) diff --git a/mingling_picker/test/src/test/route_test.rs b/mingling_picker/test/src/test/route_test.rs index af5f169..9261db1 100644 --- a/mingling_picker/test/src/test/route_test.rs +++ b/mingling_picker/test/src/test/route_test.rs @@ -127,7 +127,7 @@ fn test_two_flags_second_missing_triggers_route() { #[test] fn test_two_flags_both_present_route_not_triggered() { // Both flags present → route not triggered → Ok((true, true)) - let args = vec!["--flag_a", "--flag_b"]; + let args = vec!["--flag-a", "--flag-b"]; let result: Result<(bool, bool), &'static str> = args .with_route::<&'static str>() .pick(&arg![flag_a: bool]) diff --git a/mingling_picker/test/src/test/style_test.rs b/mingling_picker/test/src/test/style_test.rs index dbe34b5..efb2185 100644 --- a/mingling_picker/test/src/test/style_test.rs +++ b/mingling_picker/test/src/test/style_test.rs @@ -185,17 +185,31 @@ fn test_snake_case_should_not_match_as_long_flag() { } #[test] -fn test_snake_case_naming_for_unix_style() { - // UNIX_STYLE uses snake case: `flag_a` stays `flag_a` → `--flag_a` +fn test_kebab_case_naming_for_unix_style() { + // UNIX_STYLE now uses Kebab case: `flag_a` → `flag-a` → `--flag-a` let mut info = PickerArgInfo::new(); info.set_long("flag_a"); - let args = vec![make_masked("--flag_a", 0)]; + let args = vec![make_masked("--flag-a", 0)]; let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); assert_eq!( result, Some(0), - "--flag_a should match with snake-style UNIX_STYLE" + "--flag-a should match with kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_snake_case_rejected_by_unix_style() { + // UNIX_STYLE uses Kebab: `--flag_a` (snake) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag_a", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, None, + "--flag_a should NOT match under kebab-style UNIX_STYLE" ); } @@ -213,3 +227,76 @@ fn test_powershell_pascal_case_naming() { "-Verbose should match verbose via Pascal case" ); } + +#[test] +fn test_flag_naming_kebab_matches_my_name() { + // UNIX_STYLE now uses Kebab: `my_name` → `my-name` → `--my-name` + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("--my-name", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + Some(0), + "--my-name should match via kebab conversion" + ); +} + +#[test] +fn test_flag_naming_kebab_rejects_my_name_underscore() { + // Kebab style: `--my_name` (snake) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("--my_name", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, None, + "--my_name should NOT match under kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_flag_naming_pascal_matches_my_name() { + // `my_name` under Pascal → `MyName` → `-MyName` + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-MyName", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-MyName should match via Pascal conversion" + ); +} + +#[test] +fn test_flag_naming_pascal_matches_lowercase() { + // PowerShell is case-insensitive: `-myname` should also match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-myname", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-myname (lowercase) should match via case-insensitive Pascal" + ); +} + +#[test] +fn test_flag_naming_pascal_rejects_my_name_underscore() { + // Pascal style: `-my_name` (underscore) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-my_name", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, None, + "-my_name should NOT match under Pascal-style naming" + ); +} diff --git a/mingling_picker/test/src/test/value_flag_test.rs b/mingling_picker/test/src/test/value_flag_test.rs new file mode 100644 index 0000000..d267a27 --- /dev/null +++ b/mingling_picker/test/src/test/value_flag_test.rs @@ -0,0 +1,174 @@ +use mingling_picker::value::Flag; +use mingling_picker::{IntoPicker, macros::arg}; + +// Basic Flag — present / absent + +#[test] +fn test_flag_present() { + let flag: Flag = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +#[test] +fn test_flag_absent_returns_inactive() { + // Unlike bool, Flag::pick returns Parsed(Inactive) when no match is found, + // so or_default() is NOT required — unwrap() works directly. + let flag: Flag = Vec::<&str>::new() + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// Short Flag + +#[test] +fn test_flag_short_present() { + let flag: Flag = vec!["-v"] + .to_picker() + .pick(&arg![verbose: Flag, 'v']) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +// Multiple Flags + +#[test] +fn test_two_flags_both_present() { + let (a, b): (Flag, Flag) = vec!["--flag-a", "--flag-b"] + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Active); + assert_eq!(b, Flag::Active); +} + +#[test] +fn test_two_flags_one_present() { + let (a, b): (Flag, Flag) = vec!["--flag-a"] + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Active); + assert_eq!(b, Flag::Inactive); +} + +#[test] +fn test_two_flags_neither_present() { + let (a, b): (Flag, Flag) = Vec::<&str>::new() + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Inactive); + assert_eq!(b, Flag::Inactive); +} + +// After `--` (end-of-options) + +#[test] +fn test_flag_after_end_of_options() { + let flag: Flag = vec!["--", "--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// Alias + +#[test] +fn test_flag_with_alias() { + let flag: Flag = vec!["--cfg"] + .to_picker() + .pick(&arg![config: Flag, "cfg"]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +#[test] +fn test_flag_primary_name() { + let flag: Flag = vec!["--config"] + .to_picker() + .pick(&arg![config: Flag, "cfg"]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +// Unrelated flag should not match + +#[test] +fn test_unrelated_flag_does_not_match() { + let flag: Flag = vec!["--other"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// to_result / to_option + +#[test] +fn test_flag_to_result() { + let result: Result = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .to_result(); + assert_eq!(result, Ok(Flag::Active)); +} + +#[test] +fn test_flag_to_option() { + let opt: Option = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .to_option(); + assert_eq!(opt, Some(Flag::Active)); +} + +// Bool conversions + +#[test] +fn test_flag_converts_to_bool() { + let flag = Flag::Active; + assert!(bool::from(flag)); + + let flag = Flag::Inactive; + assert!(!bool::from(flag)); +} + +#[test] +fn test_flag_from_bool() { + assert_eq!(Flag::from(true), Flag::Active); + assert_eq!(Flag::from(false), Flag::Inactive); +} + +#[test] +fn test_flag_deref_to_bool() { + let active = Flag::Active; + assert!(*active); + + let inactive = Flag::Inactive; + assert!(!*inactive); +} + +// Flag never triggers route (unlike bool) +// +// Flag::pick always returns Parsed, so the fallback chain +// (default → route) is never entered. + +#[test] +fn test_flag_absent_does_not_trigger_route() { + // Even without or_default / or_route, absent flag returns Inactive, not a route + let result: Result = Vec::<&str>::new() + .with_route::<&str>() + .pick(&arg![verbose: Flag]) + .or_route(|| "should_not_fire") + .to_result(); + assert_eq!(result, Ok(Flag::Inactive)); +} -- cgit