diff options
| -rw-r--r-- | mingling_picker/src/builtin.rs | 2 | ||||
| -rw-r--r-- | mingling_picker/src/builtin/pick_numbers.rs | 20 | ||||
| -rw-r--r-- | mingling_picker/src/builtin/pick_string.rs | 11 | ||||
| -rw-r--r-- | mingling_picker/src/parselib.rs | 6 | ||||
| -rw-r--r-- | mingling_picker/src/parselib/arg_matcher.rs | 36 | ||||
| -rw-r--r-- | mingling_picker/src/parselib/pos_matcher.rs | 71 | ||||
| -rw-r--r-- | mingling_picker/src/parselib/single_matcher.rs | 39 | ||||
| -rw-r--r-- | mingling_picker/src/parselib/utils.rs | 25 | ||||
| -rw-r--r-- | mingling_picker/src/pickable.rs | 3 | ||||
| -rw-r--r-- | mingling_picker/src/pickable/single_pickable.rs | 69 | ||||
| -rw-r--r-- | mingling_picker/src/picker.rs | 16 | ||||
| -rw-r--r-- | mingling_picker/src/value.rs | 13 | ||||
| -rw-r--r-- | mingling_picker/test/src/test.rs | 2 | ||||
| -rw-r--r-- | mingling_picker/test/src/test/arg_matcher_test.rs | 39 | ||||
| -rw-r--r-- | mingling_picker/test/src/test/pos_matcher_test.rs | 141 | ||||
| -rw-r--r-- | mingling_picker/test/src/test/value_string_test.rs | 171 | ||||
| -rw-r--r-- | mingling_picker_macros/src/arg.rs | 9 |
17 files changed, 654 insertions, 19 deletions
diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs index 00fe1e5..e855b08 100644 --- a/mingling_picker/src/builtin.rs +++ b/mingling_picker/src/builtin.rs @@ -1,2 +1,4 @@ mod pick_bool; mod pick_flag; +mod pick_numbers; +mod pick_string; diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs new file mode 100644 index 0000000..6c0f0b7 --- /dev/null +++ b/mingling_picker/src/builtin/pick_numbers.rs @@ -0,0 +1,20 @@ +use crate::{SinglePickable, pickable_needed::*}; + +macro_rules! impl_single_pickable_num { + ($($t:ty),+ $(,)?) => { + $(impl SinglePickable for $t { + fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { + match str { + Some(s) => s.parse::<$t>().map(PickerArgResult::Parsed).unwrap_or(PickerArgResult::NotFound), + None => PickerArgResult::NotFound, + } + } + })+ + }; +} + +impl_single_pickable_num! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64, +} diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs new file mode 100644 index 0000000..c96f667 --- /dev/null +++ b/mingling_picker/src/builtin/pick_string.rs @@ -0,0 +1,11 @@ +use crate::PickerArgResult::NotFound; +use crate::{SinglePickable, pickable_needed::*}; + +impl SinglePickable for String { + fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { + match str { + Some(str) => PickerArgResult::Parsed(str.to_string()), + None => NotFound, + } + } +} diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs index 49e3cc2..456a34d 100644 --- a/mingling_picker/src/parselib.rs +++ b/mingling_picker/src/parselib.rs @@ -4,6 +4,12 @@ pub use flag_matcher::*; mod arg_matcher; pub use arg_matcher::*; +mod pos_matcher; +pub use pos_matcher::*; + +mod single_matcher; +pub use single_matcher::*; + mod style; pub use style::*; diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs index 808e4a2..38bb9cc 100644 --- a/mingling_picker/src/parselib/arg_matcher.rs +++ b/mingling_picker/src/parselib/arg_matcher.rs @@ -27,25 +27,28 @@ use crate::{ pub struct ArgMatcher; impl ArgMatcher { - /// Check whether `raw` matches `flag_str` (exact or eq-separated). + /// Check whether `raw` matches `flag_str`, optionally with an inline value + /// separated by the style's value separator (`=` for Unix, `:` for PowerShell). #[inline(always)] - fn matches(raw: &str, flag_str: &str, case_sensitive: bool) -> bool { + fn matches(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { + let eq_match = + |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); + if case_sensitive { - raw == flag_str - || raw.starts_with(flag_str) && raw.as_bytes().get(flag_str.len()) == Some(&b'=') + raw == flag_str || (raw.starts_with(flag_str) && eq_match(raw, flag_str)) } else { raw.eq_ignore_ascii_case(flag_str) || (raw.len() > flag_str.len() && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) - && raw.as_bytes()[flag_str.len()] == b'=') + && raw.as_bytes()[flag_str.len()] == sep as u8) } } - /// Check whether the argument at the given position (in the masked slice) - /// contains its value inline (eq mode), so no extra slot is needed. + /// Check whether the argument contains its value inline via the style's + /// value separator (eq mode), so no extra mask slot is needed. #[inline(always)] - fn is_eq_mode(raw: &str, flag_str: &str) -> bool { - raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&b'=') + fn is_inline_value(raw: &str, flag_str: &str, sep: char) -> bool { + raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&(sep as u8)) } } @@ -61,6 +64,7 @@ impl Matcher for ArgMatcher { let possible_flags = build_possible_flags(style, arg_info); let end = seek_end_of_options(args, style); + let sep = style.value_separator; for arg in args { if end.is_some_and(|e| arg.raw_idx >= e) { @@ -69,7 +73,7 @@ impl Matcher for ArgMatcher { let matched = possible_flags .iter() - .any(|f| Self::matches(arg.raw, f, style.case_sensitive)); + .any(|f| Self::matches(arg.raw, f, style.case_sensitive, sep)); if matched { return Some(arg.raw_idx); } @@ -94,6 +98,7 @@ impl Matcher for ArgMatcher { let possible_flags = build_possible_flags(style, arg_info); let end = seek_end_of_options(args, style); + let sep = style.value_separator; let mut result = Vec::new(); let mut i = 0; @@ -104,18 +109,21 @@ impl Matcher for ArgMatcher { let matched = possible_flags .iter() - .any(|f| Self::matches(args[i].raw, f, style.case_sensitive)); + .any(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)); if matched { let flag_str = possible_flags .iter() - .find(|f| Self::matches(args[i].raw, f, style.case_sensitive)) + .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)) .expect("already matched"); result.push(args[i].raw_idx); - if !Self::is_eq_mode(args[i].raw, flag_str) { - if i + 1 < args.len() { + if !Self::is_inline_value(args[i].raw, flag_str, sep) { + if i + 1 < args.len() + // Don't consume `--` (end-of-options marker) as a value. + && end.is_none_or(|e| args[i + 1].raw_idx < e) + { result.push(args[i + 1].raw_idx); i += 2; continue; diff --git a/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs new file mode 100644 index 0000000..a7052d4 --- /dev/null +++ b/mingling_picker/src/parselib/pos_matcher.rs @@ -0,0 +1,71 @@ +use crate::{matcher_needed::*, parselib::seek_end_of_options}; + +/// `PositionalMatcher` matches positional arguments — values not associated +/// with any named flag. +/// +/// # Rules +/// +/// * Before `--`: skips any argument that starts with the style's long or short +/// prefix (those belong to named matchers). +/// * After `--`: takes **everything** — the `--` marker signals that all +/// remaining values are positional, even if they look like flags. +/// * Runs at the lowest priority (see [`PickerArgAttr::Positional`]). +pub struct PositionalMatcher; + +impl PositionalMatcher { + /// Check whether `raw` looks like a named flag (starts with a prefix). + #[inline(always)] + fn is_flag_like(raw: &str, style: &ParserStyle) -> bool { + raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix) + } +} + +impl Matcher for PositionalMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Option<usize> { + let end = seek_end_of_options(args, style); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + // Hit `--`: everything from here on is positional, + // including the first arg after `--`. + continue; + } + if end.is_some_and(|e| arg.raw_idx > e) { + // After `--`: accept everything. + return Some(arg.raw_idx); + } + // Before `--`: skip flag-like args. + if !Self::is_flag_like(arg.raw, style) { + return Some(arg.raw_idx); + } + } + + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Vec<usize> { + let end = seek_end_of_options(args, style); + let mut after_end = false; + let mut result = Vec::new(); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + after_end = true; + continue; + } + if after_end || !Self::is_flag_like(arg.raw, style) { + result.push(arg.raw_idx); + } + } + + result + } +} diff --git a/mingling_picker/src/parselib/single_matcher.rs b/mingling_picker/src/parselib/single_matcher.rs new file mode 100644 index 0000000..3606d3a --- /dev/null +++ b/mingling_picker/src/parselib/single_matcher.rs @@ -0,0 +1,39 @@ +use crate::TagPhaseContext; +use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher}; + +/// `SingleMatcher` is a composite matcher for single-value parameters. +/// +/// It delegates to [`PositionalMatcher`] for positional args and +/// [`ArgMatcher`] for named args, adding a guard: if a named flag +/// captures only itself with no inline value (eq mode), the result +/// is cleared so that [`Pickable::pick`] receives `[]` → `NotFound`. +/// +/// This is the standard tag implementation for all `Single`-type +/// `Pickable` implementations (e.g., `String`, `i32`, `u64`). +pub struct SingleMatcher; + +impl SingleMatcher { + /// Match a single positional value or a named flag+value pair. + /// + /// For named args, returns `[]` when the flag has no following + /// value and no inline separator — indicating a missing value. + #[inline(always)] + pub fn tag(ctx: TagPhaseContext) -> Vec<usize> { + if ctx.arg_info.positional { + PositionalMatcher::match_one(ctx.into()) + .map(|i| vec![i]) + .unwrap_or_default() + } else { + let args = ctx.args; + let positions = ArgMatcher::match_all(ctx.into()); + if positions.len() == 1 { + let sep = ParserStyle::global_style().value_separator; + if let Some(raw) = args.get(positions[0]) + && !raw.contains(sep) { + return vec![]; + } + } + positions + } + } +} diff --git a/mingling_picker/src/parselib/utils.rs b/mingling_picker/src/parselib/utils.rs index ae3c203..4fd34b7 100644 --- a/mingling_picker/src/parselib/utils.rs +++ b/mingling_picker/src/parselib/utils.rs @@ -26,6 +26,31 @@ pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Ve possible_flags } +/// Extract a single value from the raw strings tagged by [`SingleMatcher`]. +/// +/// Returns `None` if no value is available (empty slice), +/// the inline value after the style separator if present (eq mode), +/// or the value directly (positional or flag-following). +/// +/// This is the standard `pick` helper for all `Single`-type +/// [`Pickable`](crate::Pickable) implementations. +#[must_use] +pub fn seek_single<'a>(raw_strs: &'a [&'a str]) -> Option<&'a str> { + match raw_strs.len() { + 0 => None, + 1 => { + let s = raw_strs[0]; + let sep = ParserStyle::global_style().value_separator; + if let Some(pos) = s.rfind(sep) { + Some(&s[pos + 1..]) + } else { + Some(s) + } + } + _ => Some(raw_strs[1]), + } +} + /// Seeks the index of the end-of-options marker (`--`) in the argument list. /// /// This function searches for the standard end-of-options separator (`--`) diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs index c88e49b..463edc0 100644 --- a/mingling_picker/src/pickable.rs +++ b/mingling_picker/src/pickable.rs @@ -1,5 +1,8 @@ use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs}; +mod single_pickable; +pub use single_pickable::*; + /// `Pickable` trait defines how to parse a type instance from command-line arguments. /// /// This trait is the core abstraction of the `Picker` argument parsing system, dividing the diff --git a/mingling_picker/src/pickable/single_pickable.rs b/mingling_picker/src/pickable/single_pickable.rs new file mode 100644 index 0000000..8a5b3e6 --- /dev/null +++ b/mingling_picker/src/pickable/single_pickable.rs @@ -0,0 +1,69 @@ +use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; + +/// `SinglePickable` trait defines how to parse a type from a single command-line argument. +/// +/// This trait provides a simplified interface for types that consume exactly one argument value. +/// It is automatically implemented by the blanket `impl` of [`Pickable`], so types implementing +/// `SinglePickable` will work with the full `Pickable` argument parsing system. +/// +/// Additionally, `Option<S>` where `S: SinglePickable` also implements [`Pickable`], allowing +/// optional arguments to be parsed naturally. +/// +/// # Type Parameters +/// +/// * `Self` - The type to be parsed from a single argument string. +pub trait SinglePickable +where + Self: Sized, +{ + /// Parse a single optional string value into an instance of `Self`. + /// + /// # Parameters + /// + /// * `str` - An `Option<&str>` representing the raw argument value. If `None`, + /// it indicates that no argument value was provided (e.g., for flag-like arguments). + /// + /// # Returns + /// + /// Returns [`PickerArgResult<Self>`], i.e., the parsed `Self` instance on success, + /// or an appropriate error message on failure. + fn pick_single(str: Option<&str>) -> PickerArgResult<Self>; +} + +impl<'a, S> Pickable<'a> for S +where + S: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + crate::parselib::SingleMatcher::tag(ctx) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + Self::pick_single(crate::parselib::seek_single(raw_strs)) + } +} + +impl<'a, S> Pickable<'a> for Option<S> +where + S: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + crate::parselib::SingleMatcher::tag(ctx) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + match S::pick(raw_strs) { + PickerArgResult::Unparsed => PickerArgResult::Unparsed, + PickerArgResult::Parsed(r) => PickerArgResult::Parsed(Some(r)), + PickerArgResult::NotFound => PickerArgResult::Parsed(None), + } + } +} diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs index 4473d76..4efb532 100644 --- a/mingling_picker/src/picker.rs +++ b/mingling_picker/src/picker.rs @@ -47,6 +47,22 @@ impl<'a> Picker<'a> { args: PickerArgs::Owned(args), } } + + /// Changes the route (phantom type parameter) of the `Picker`. + /// + /// This method allows converting a `Picker` from one route type to another, + /// while preserving the same underlying arguments. The route type is typically + /// used to distinguish different parsing contexts or to carry compile-time + /// state information through the picking chain. + pub fn with_route<NewRoute>(self) -> Picker<'a, NewRoute> + where + Self: Sized, + { + Picker { + route_phantom: PhantomData, + args: self.args, + } + } } /// Internal arguments of Picker diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs index 7d52442..ee0d6ee 100644 --- a/mingling_picker/src/value.rs +++ b/mingling_picker/src/value.rs @@ -1,6 +1,6 @@ use std::{ fmt::{Debug, Display}, - ops::Deref, + ops::{Deref, Not}, }; /// Parsed result of a boolean-style command-line flag. @@ -132,3 +132,14 @@ impl Deref for Flag { } } } + +impl Not for Flag { + type Output = Flag; + + fn not(self) -> Flag { + match self { + Flag::Active => Flag::Inactive, + Flag::Inactive => Flag::Active, + } + } +} diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs index eb53229..e27615a 100644 --- a/mingling_picker/test/src/test.rs +++ b/mingling_picker/test/src/test.rs @@ -1,5 +1,7 @@ mod arg_matcher_test; mod basic_test; +mod pos_matcher_test; mod route_test; mod style_test; mod value_flag_test; +mod value_string_test; diff --git a/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs index 947e20f..7b37bc8 100644 --- a/mingling_picker/test/src/test/arg_matcher_test.rs +++ b/mingling_picker/test/src/test/arg_matcher_test.rs @@ -248,3 +248,42 @@ fn test_match_all_end_of_options_not_matched() { let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); assert!(result.is_empty()); } + +// `--` should never be consumed as a value by a named flag. + +#[test] +fn test_match_all_named_does_not_consume_end_marker() { + // `--flag` before `--`, but the next position IS `--`. + // Only tag the flag, don't consume the end-of-options marker. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0], "should NOT consume -- as a value"); +} + +#[test] +fn test_match_all_named_does_not_consume_end_marker_flag_before_end() { + // Simulates: --flag -- you where -- is end-of-options. + // The flag should be tagged but -- should NOT be consumed as its value. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + + let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + vec![0], + "flag before --: only tag flag, leave -- for positional" + ); +} + +#[test] +fn test_match_all_flag_after_end_has_value() { + // Flag after `--` should not match at all. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} diff --git a/mingling_picker/test/src/test/pos_matcher_test.rs b/mingling_picker/test/src/test/pos_matcher_test.rs new file mode 100644 index 0000000..b5319f5 --- /dev/null +++ b/mingling_picker/test/src/test/pos_matcher_test.rs @@ -0,0 +1,141 @@ +use mingling_picker::PickerArgInfo; +use mingling_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE}; + +fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> { + pairs + .iter() + .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) + .collect() +} + +// on_match_one — basic + +#[test] +fn test_pos_one_takes_first_non_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_takes_first_if_no_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("file.txt", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_pos_one_all_flags_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("--name", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_pos_one_empty_returns_none() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — after `--` + +#[test] +fn test_pos_one_after_end_takes_even_flag_like() { + // After `--`, accept everything including `--verbose`. + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_only_end_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — Windows style prefix + +#[test] +fn test_pos_one_windows_skips_slash_prefix() { + let info = PickerArgInfo::new(); + let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(1)); +} + +// on_match_all — basic + +#[test] +fn test_pos_all_collects_non_flags() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3]); +} + +#[test] +fn test_pos_all_only_flags_returns_empty() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--a", 0), ("--b", 1)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +#[test] +fn test_pos_all_empty() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — after `--` + +#[test] +fn test_pos_all_after_end_accepts_everything() { + // After `--`, even `--verbose` is accepted as positional. + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("--verbose", 0), + ("a.txt", 1), + ("--", 2), + ("--flag", 3), + ("b.txt", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3, 4]); +} + +#[test] +fn test_pos_all_only_after_end() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 2]); +} + +// on_match_all — mixed before/after `--` + +#[test] +fn test_pos_all_mixed() { + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("infile", 0), + ("--verbose", 1), + ("--", 2), + ("outfile", 3), + ("--extra", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + // Before `--`: "infile" (skip --verbose). + // After `--`: everything — "outfile", "--extra". + assert_eq!(result, vec![0, 3, 4]); +} diff --git a/mingling_picker/test/src/test/value_string_test.rs b/mingling_picker/test/src/test/value_string_test.rs new file mode 100644 index 0000000..b1e5c0b --- /dev/null +++ b/mingling_picker/test/src/test/value_string_test.rs @@ -0,0 +1,171 @@ +use mingling_picker::{IntoPicker, macros::arg}; + +// Basic named String — present / absent + +#[test] +fn test_string_named_present() { + let val: String = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +#[test] +fn test_string_named_absent_uses_default() { + let val: String = Vec::<&str>::new() + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Named String — eq mode + +#[test] +fn test_string_named_eq_mode() { + let val: String = vec!["--name=Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +// Named String — short flag + +#[test] +fn test_string_named_short_flag() { + let val: String = vec!["-n", "Alice"] + .to_picker() + .pick(&arg![name: String, 'n']) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +// Named String — no value after flag + +#[test] +fn test_string_named_missing_value_triggers_default() { + // --name at end with no following arg → pick returns NotFound → or_default gives "" + let val: String = vec!["--name"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Positional String + +#[test] +fn test_string_positional() { + let val: String = vec!["file.txt"] + .to_picker() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(val, "file.txt"); +} + +#[test] +fn test_string_positional_takes_first() { + let val: String = vec!["first", "second"] + .to_picker() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(val, "first"); +} + +// Multiple occurrences (Single only tags one occurrence per Parseable) + +#[test] +fn test_string_two_named_flags() { + let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .pick(&arg![greeting: String]) + .or_default() + .unwrap(); + assert_eq!(a, "Alice"); + assert_eq!(b, "Hello"); +} + +// Mixed named + positional + +#[test] +fn test_string_named_and_positional() { + let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(file, "file.txt"); +} + +// After `--` (end-of-options) + +#[test] +fn test_string_named_after_end_of_options() { + // Named arg after `--` should not be matched → default + let val: String = vec!["--", "--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Unrelated flag should not match → default + +#[test] +fn test_string_unrelated_flag() { + let val: String = vec!["--other", "value"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// to_result / to_option + +#[test] +fn test_string_to_result() { + let result: Result<String, ()> = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .to_result(); + assert_eq!(result, Ok("Alice".to_string())); +} + +#[test] +fn test_string_to_option() { + let opt: Option<String> = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .to_option(); + assert_eq!(opt, Some("Alice".to_string())); +} + +// Custom default via .or() + +#[test] +fn test_string_custom_default() { + let val: String = Vec::<&str>::new() + .to_picker() + .pick(&arg![name: String]) + .or(|| "default_name".to_string()) + .unwrap(); + assert_eq!(val, "default_name"); +} diff --git a/mingling_picker_macros/src/arg.rs b/mingling_picker_macros/src/arg.rs index e273d23..70b27b0 100644 --- a/mingling_picker_macros/src/arg.rs +++ b/mingling_picker_macros/src/arg.rs @@ -161,11 +161,12 @@ fn extract_char(tokens: &[TokenTree]) -> Option<char> { let cs: Vec<char> = s.chars().collect(); if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' { let inner: String = cs[1..cs.len() - 1].iter().collect(); + // inner is the character between the quotes of a char literal. + // For a literal `'n'`, inner is `"n"` (the character n). + // For an escape `'\n'`, inner is the actual newline character. + // The catch-all handles both literal single chars and escape + // sequences (the escaped char IS the actual control character). match inner.as_str() { - "n" => Some('\n'), - "t" => Some('\t'), - "r" => Some('\r'), - "0" => Some('\0'), "\\\\" => Some('\\'), "\\'" => Some('\''), _ => inner.chars().next(), |
