diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-16 19:58:30 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-16 19:58:30 +0800 |
| commit | 81049b3abd83115374cfc51b26fb5d5054263f81 (patch) | |
| tree | bea8e56ebacf96d685b545bc7ea3c8d152910bf9 | |
| parent | 4f4a61d493547660260a5aa1db1af8a6ec42bbb6 (diff) | |
feat(picker): add string value support with inline value matching
Implement `Pickable` for `String` supporting both positional and named
flags, and generalize value matching to use style-specific separators
| -rw-r--r-- | mingling_picker/src/builtin.rs | 1 | ||||
| -rw-r--r-- | mingling_picker/src/builtin/pick_string.rs | 48 | ||||
| -rw-r--r-- | mingling_picker/src/parselib/arg_matcher.rs | 31 | ||||
| -rw-r--r-- | mingling_picker/test/src/test.rs | 1 | ||||
| -rw-r--r-- | mingling_picker/test/src/test/value_string_test.rs | 171 |
5 files changed, 239 insertions, 13 deletions
diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs index 00fe1e5..d179f86 100644 --- a/mingling_picker/src/builtin.rs +++ b/mingling_picker/src/builtin.rs @@ -1,2 +1,3 @@ mod pick_bool; mod pick_flag; +mod pick_string; diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs new file mode 100644 index 0000000..688ac0a --- /dev/null +++ b/mingling_picker/src/builtin/pick_string.rs @@ -0,0 +1,48 @@ +use crate::parselib::{ArgMatcher, Matcher, ParserStyle}; +use crate::pickable_needed::*; + +impl<'a> Pickable<'a> for String { + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + if ctx.arg_info.positional { + // Positional: take the first available position only. + ArgMatcher::match_one(ctx.into()) + .map(|i| vec![i]) + .unwrap_or_default() + } else { + // Named: find each flag + its single value. + ArgMatcher::match_all(ctx.into()) + } + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + match raw_strs.len() { + 0 => PickerArgResult::NotFound, + 1 => { + let s = raw_strs[0]; + let style = ParserStyle::global_style(); + + // Inline value via style separator (e.g., --name=Alice, -Name:Alice). + if let Some(pos) = s.rfind(style.value_separator) { + return PickerArgResult::Parsed(s[pos + 1..].to_string()); + } + + // If the single element looks like a named flag (starts with a prefix + // such as `--`, `-`, or `/`), it's a flag with no value → NotFound. + if s.starts_with(style.long_prefix) || s.starts_with(style.short_prefix) { + return PickerArgResult::NotFound; + } + + // Positional value. + PickerArgResult::Parsed(s.to_string()) + } + _ => { + // flag + value as separate args: take the second element. + PickerArgResult::Parsed(raw_strs[1].to_string()) + } + } + } +} diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs index 808e4a2..d4b8793 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,17 +109,17 @@ 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 !Self::is_inline_value(args[i].raw, flag_str, sep) { if i + 1 < args.len() { result.push(args[i + 1].raw_idx); i += 2; diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs index eb53229..dc251dd 100644 --- a/mingling_picker/test/src/test.rs +++ b/mingling_picker/test/src/test.rs @@ -3,3 +3,4 @@ mod basic_test; mod route_test; mod style_test; mod value_flag_test; +mod value_string_test; 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"); +} |
