aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-14 01:36:27 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-14 01:36:27 +0800
commit4665fc94ab9508d115298dd988e2381354f46c01 (patch)
treea2b14f5684c68d88a0a77570d0ac3ac8d863867b
parent5ac0f0e98d05e95f7e3c58995f8d79670cbac772 (diff)
feat(picker): add core trait, types, and builder macro for argument
parsing Implement the foundational `mingling_picker` library along with its companion `mingling_picker_macros` crate. The picker provides: - `Pickable` trait for parsing types from raw strings - `PickerResult` enum modeling parse outcomes - `Picker` struct for storing and indexing command-line arguments - `PickerRequirement` struct for declarative parameter definitions - `PickerPattern` family (1..32) of typed pattern structs via the `internal_repeat` proc macro - `req!` proc macro as a succinct builder for `PickerRequirement` Re-export `mingling_picker::macros::*` from the `mingling` crate when the `picker` feature is enabled, replacing the previous wildcard re-export of `mingling_macros`.
-rw-r--r--mingling/src/lib.rs10
-rw-r--r--mingling_picker/src/lib.rs20
-rw-r--r--mingling_picker/src/pickable.rs39
-rw-r--r--mingling_picker/src/pickable/implements.rs43
-rw-r--r--mingling_picker/src/picker.rs232
-rw-r--r--mingling_picker/src/picker/patterns.rs12
-rw-r--r--mingling_picker/src/requirement.rs150
-rw-r--r--mingling_picker/src/result.rs59
-rw-r--r--mingling_picker_macros/src/internal_repeat.rs204
-rw-r--r--mingling_picker_macros/src/lib.rs29
-rw-r--r--mingling_picker_macros/src/req.rs214
11 files changed, 1006 insertions, 6 deletions
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 76d73b6..bee6fbb 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -170,7 +170,7 @@ pub mod macros {
pub use mingling_macros::suggest_enum;
/// New Parser provided by the `picker` feature
#[cfg(feature = "picker")]
- pub use mingling_macros::*;
+ pub use mingling_picker::macros::*;
}
/// derive macro `EnumTag`
@@ -222,10 +222,6 @@ pub mod res;
/// use mingling::prelude::*;
/// ```
pub mod prelude {
- /// Re-export of the `Groupped` derive macro for grouping types.
- pub use crate::Groupped;
- /// Re-export of the `RenderResult` struct for outputting rendering result
- pub use crate::RenderResult;
/// Re-export of the `chain` macro for defining a chain of commands.
pub use crate::macros::chain;
/// Re-export of the `dispatcher` macro for routing commands.
@@ -242,6 +238,10 @@ pub mod prelude {
pub use crate::macros::pack_err;
/// Re-export of the `renderer` macro for defining renderer functions.
pub use crate::macros::renderer;
+ /// Re-export of the `Groupped` derive macro for grouping types.
+ pub use crate::Groupped;
+ /// Re-export of the `RenderResult` struct for outputting rendering result
+ pub use crate::RenderResult;
/// Like `pack_err!` but also marks the type for structured output
#[cfg(all(feature = "structural_renderer", feature = "extra_macros"))]
pub use mingling_macros::pack_err_structural;
diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs
index 3c3251d..97020a4 100644
--- a/mingling_picker/src/lib.rs
+++ b/mingling_picker/src/lib.rs
@@ -1 +1,19 @@
-pub mod prelude {}
+mod picker;
+pub use picker::*;
+
+mod pickable;
+pub use pickable::*;
+
+mod requirement;
+pub use requirement::*;
+
+mod result;
+pub use result::*;
+
+pub mod prelude {
+ pub use crate::IntoPicker;
+}
+
+pub mod macros {
+ pub use mingling_picker_macros::*;
+}
diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs
new file mode 100644
index 0000000..9fe2c9d
--- /dev/null
+++ b/mingling_picker/src/pickable.rs
@@ -0,0 +1,39 @@
+use crate::PickerResult;
+
+mod implements;
+
+/// A trait for types that can be constructed from a raw string representation.
+///
+/// Implementing this trait allows a type to be "picked" or parsed from a string,
+/// enabling deserialization or configuration loading from textual input.
+///
+/// # Requirements
+///
+/// - The implementing type must be [`Sized`] and implement [`Default`].
+/// - The [`pick`] method performs the actual parsing and may fail.
+///
+/// # Errors
+///
+/// Returns a [`PickerResult`] which encapsulates either a successful parse
+/// or an error indicating why the input could not be parsed.
+///
+/// # Examples
+///
+/// ```
+/// # use mingling_picker::{Pickable, PickerResult};
+/// #[derive(Default)]
+/// struct MyType(String);
+///
+/// impl Pickable for MyType {
+/// fn pick(raw_str: &str) -> PickerResult<Self> {
+/// PickerResult::Parsed(MyType(raw_str.to_string()))
+/// }
+/// }
+/// ```
+pub trait Pickable
+where
+ Self: Sized + Default,
+{
+ /// Parses a `Self` value from the given raw string input.
+ fn pick(raw_str: &str) -> PickerResult<Self>;
+}
diff --git a/mingling_picker/src/pickable/implements.rs b/mingling_picker/src/pickable/implements.rs
new file mode 100644
index 0000000..34f9727
--- /dev/null
+++ b/mingling_picker/src/pickable/implements.rs
@@ -0,0 +1,43 @@
+use crate::{Pickable, PickerResult};
+use std::path::PathBuf;
+
+macro_rules! impl_pickable {
+ ($($t:ty),*) => {
+ $(
+ impl Pickable for $t {
+ fn pick(raw_str: &str) -> PickerResult<Self> {
+ raw_str.parse::<$t>().into()
+ }
+ }
+ )*
+ };
+}
+
+impl_pickable!(
+ i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, usize, f32, f64, isize
+);
+
+impl Pickable for String {
+ fn pick(raw_str: &str) -> PickerResult<Self> {
+ PickerResult::Parsed(raw_str.to_string())
+ }
+}
+
+impl Pickable for PathBuf {
+ fn pick(raw_str: &str) -> PickerResult<Self> {
+ PickerResult::Parsed(PathBuf::from(raw_str))
+ }
+}
+
+impl<T: Pickable> Pickable for Option<T> {
+ fn pick(raw_str: &str) -> PickerResult<Self> {
+ if raw_str.is_empty() {
+ PickerResult::Parsed(None)
+ } else {
+ match T::pick(raw_str) {
+ PickerResult::Parsed(v) => PickerResult::Parsed(Some(v)),
+ _ => PickerResult::Parsed(None),
+ }
+ }
+ }
+}
diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs
new file mode 100644
index 0000000..f8d3cb1
--- /dev/null
+++ b/mingling_picker/src/picker.rs
@@ -0,0 +1,232 @@
+use std::ops::Index;
+
+mod patterns;
+pub use patterns::*;
+
+/// Picker, used to record all states of a parameter parsing
+///
+/// Includes the following:
+///
+/// - Basic arguments
+/// - Parsing states
+/// - Parsing results
+pub struct Picker<'a> {
+ /// Internal arguments of Picker
+ args: PickerArguments<'a>,
+}
+
+/// Internal arguments of Picker
+///
+/// - `Slice` - borrowed slice of string slices
+/// - `Vec` - owned vector of borrowed string slices
+/// - `Owned` - owned vector of owned strings
+pub enum PickerArguments<'a> {
+ /// Borrowed slice of string slices
+ Slice(&'a [&'a str]),
+ /// Owned vector of borrowed string slices
+ Vec(Vec<&'a str>),
+ /// Owned vector of owned strings
+ Owned(Vec<String>),
+}
+
+impl<'a> From<&'a [&'a str]> for Picker<'a> {
+ fn from(value: &'a [&'a str]) -> Self {
+ Picker {
+ args: PickerArguments::Slice(value),
+ }
+ }
+}
+
+impl<'a> From<Vec<&'a str>> for Picker<'a> {
+ fn from(value: Vec<&'a str>) -> Self {
+ Picker {
+ args: PickerArguments::Vec(value),
+ }
+ }
+}
+
+impl<'a> From<Vec<String>> for Picker<'a> {
+ fn from(value: Vec<String>) -> Self {
+ Picker {
+ args: PickerArguments::Owned(value),
+ }
+ }
+}
+
+impl<'a> Picker<'a> {
+ /// Returns the number of arguments in the picker.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::Picker;
+ ///
+ /// let args = Picker::from(&["hello", "world"][..]);
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args = Picker::from(vec!["foo", "bar"]);
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args = Picker::from(vec!["a".to_string(), "b".to_string()]);
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let empty: Picker = Picker::from(&[][..]);
+ /// assert_eq!(empty.len(), 0);
+ /// ```
+ pub fn len(&self) -> usize {
+ match &self.args {
+ PickerArguments::Slice(items) => items.len(),
+ PickerArguments::Vec(items) => items.len(),
+ PickerArguments::Owned(items) => items.len(),
+ }
+ }
+
+ /// Returns an iterator over the arguments, yielding owned `String` values.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::Picker;
+ ///
+ /// let args = Picker::from(&["hello", "world"][..]);
+ /// let collected: Vec<String> = args.iter().collect();
+ /// assert_eq!(collected, vec!["hello", "world"]);
+ ///
+ /// let args = Picker::from(vec!["foo", "bar"]);
+ /// let collected: Vec<String> = args.iter().collect();
+ /// assert_eq!(collected, vec!["foo", "bar"]);
+ ///
+ /// let args = Picker::from(vec!["a".to_string(), "b".to_string()]);
+ /// let collected: Vec<String> = args.iter().collect();
+ /// assert_eq!(collected, vec!["a", "b"]);
+ /// ```
+ pub fn iter(&'a self) -> PickerIter<'a> {
+ match &self.args {
+ PickerArguments::Slice(items) => PickerIter::Slice(items.iter()),
+ PickerArguments::Vec(items) => PickerIter::Vec(items.iter()),
+ PickerArguments::Owned(items) => PickerIter::Owned(items.iter()),
+ }
+ }
+}
+
+impl<'a> Index<usize> for Picker<'a> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ match &self.args {
+ PickerArguments::Slice(items) => items[index],
+ PickerArguments::Vec(items) => items[index],
+ PickerArguments::Owned(items) => &items[index],
+ }
+ }
+}
+
+impl<'a> Index<usize> for &Picker<'a> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ match &self.args {
+ PickerArguments::Slice(items) => items[index],
+ PickerArguments::Vec(items) => items[index],
+ PickerArguments::Owned(items) => &items[index],
+ }
+ }
+}
+
+impl<'a> IntoIterator for &'a Picker<'a> {
+ type Item = String;
+ type IntoIter = PickerIter<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ match &self.args {
+ PickerArguments::Slice(items) => PickerIter::Slice(items.iter()),
+ PickerArguments::Vec(items) => PickerIter::Vec(items.iter()),
+ PickerArguments::Owned(items) => PickerIter::Owned(items.iter()),
+ }
+ }
+}
+
+/// Iterator for `Picker`, yielding owned `String` values.
+///
+/// This enum wraps the underlying slice iterators for each variant of
+/// `PickerArguments`, allowing uniform iteration over borrowed or owned data.
+pub enum PickerIter<'a> {
+ /// Iterates over a borrowed slice (`&[&str]`)
+ Slice(std::slice::Iter<'a, &'a str>),
+ /// Iterates over an owned vector of borrowed string slices (`Vec<&str>`)
+ Vec(std::slice::Iter<'a, &'a str>),
+ /// Iterates over an owned vector of owned strings (`Vec<String>`)
+ Owned(std::slice::Iter<'a, String>),
+}
+
+impl<'a> Iterator for PickerIter<'a> {
+ type Item = String;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ match self {
+ PickerIter::Slice(iter) => iter.next().map(|s| s.to_string()),
+ PickerIter::Vec(iter) => iter.next().map(|s| s.to_string()),
+ PickerIter::Owned(iter) => iter.next().cloned(),
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ match self {
+ PickerIter::Slice(iter) => iter.size_hint(),
+ PickerIter::Vec(iter) => iter.size_hint(),
+ PickerIter::Owned(iter) => iter.size_hint(),
+ }
+ }
+}
+
+impl<'a> ExactSizeIterator for PickerIter<'a> {}
+
+/// Trait for converting types into a `Picker`
+///
+/// Implemented for:
+/// - `&[&str]` (borrowed slice)
+/// - `Vec<&str>` (owned vector of borrowed strings)
+/// - `Vec<String>` (owned vector of owned strings)
+pub trait IntoPicker<'a> {
+ /// Converts the value into a `Picker`
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::{IntoPicker, Picker};
+ ///
+ /// let args: Picker = (&["hello", "world"][..]).to_picker();
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args: Picker = vec!["foo", "bar"].to_picker();
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args: Picker = vec!["a".to_string(), "b".to_string()].to_picker();
+ /// assert_eq!(args.len(), 2);
+ /// ```
+ fn to_picker(self) -> Picker<'a>;
+}
+
+impl<'a> IntoPicker<'a> for &'a [&'a str] {
+ fn to_picker(self) -> Picker<'a> {
+ Picker {
+ args: PickerArguments::Slice(self),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<&'a str> {
+ fn to_picker(self) -> Picker<'a> {
+ Picker {
+ args: PickerArguments::Vec(self),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<String> {
+ fn to_picker(self) -> Picker<'a> {
+ Picker {
+ args: PickerArguments::Owned(self),
+ }
+ }
+}
diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs
new file mode 100644
index 0000000..74a8cd8
--- /dev/null
+++ b/mingling_picker/src/picker/patterns.rs
@@ -0,0 +1,12 @@
+use crate::{Pickable, PickerArguments, PickerRequirement};
+
+mingling_picker_macros::internal_repeat! (1..=32 => {
+ pub struct PickerPattern$<'a, (Type$,)+>
+ where (Type$: Pickable + Default,)+
+ {
+ pub args: PickerArguments<'a>,
+ (
+ pub require_$: PickerRequirement<'a, Type$>,
+ )+
+ }
+});
diff --git a/mingling_picker/src/requirement.rs b/mingling_picker/src/requirement.rs
new file mode 100644
index 0000000..32fefff
--- /dev/null
+++ b/mingling_picker/src/requirement.rs
@@ -0,0 +1,150 @@
+use crate::{Pickable, PickerResult};
+
+/// Represents a constraint definition for a parameter selection.
+///
+/// This structure describes the constraints that a command-line parameter (Picker parameter item)
+/// should satisfy, including its full name list (with aliases), short name form, and whether it is
+/// positional.
+///
+/// # Field Descriptions
+///
+/// - `full`: Full name or alias list. For example, `["config", "cfg"]` means the parameter can be
+/// matched with either `--config` or `--cfg`. Must contain at least one non-empty string.
+///
+/// - `short`: Short name (single character). For example, `Some('c')` means it can be passed using
+/// the `-c` form. If set to `None`, the short name form is not supported.
+///
+/// - `positional`: Whether the parameter is positional (i.e., an argument without a flag).
+/// - `true`: The parameter is positional; it is matched by its position in the command line rather
+/// than by a `--name` or `-n` flag.
+/// - `false`: The parameter is a named (flag-based) parameter.
+///
+/// - `result`: The parsed result of this parameter requirement. Initially set to
+/// `PickerResult::Unparsed`. After parsing, contains either the successfully parsed value or an
+/// error.
+#[derive(Default)]
+pub struct PickerRequirement<'a, Type>
+where
+ Type: Default + Pickable,
+{
+ /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`.
+ full: &'a [&'a str],
+
+ /// Short name, e.g., `'c'`.
+ short: Option<char>,
+
+ /// Whether the parameter is positional (no flag, matched by position).
+ positional: bool,
+
+ /// The parsed result of this parameter requirement.
+ result: PickerResult<Type>,
+}
+
+impl<'a, Type> PickerRequirement<'a, Type>
+where
+ Type: Default + Pickable,
+{
+ /// Creates a new `PickerRequirement` with the provided parameters.
+ pub fn new(full: &'a [&'a str], short: Option<char>, positional: bool) -> Self {
+ Self {
+ full,
+ short,
+ positional,
+ result: PickerResult::Unparsed,
+ }
+ }
+
+ /// Returns the full name list (including aliases).
+ pub fn full(&self) -> &'a [&'a str] {
+ self.full
+ }
+
+ /// Returns the short name, if any.
+ pub fn short(&self) -> Option<char> {
+ self.short
+ }
+
+ /// Returns whether the parameter is positional.
+ ///
+ /// If `full` is empty or `short` is `None`, the parameter is considered positional
+ /// regardless of the stored value.
+ pub fn is_positional(&self) -> bool {
+ if self.full.is_empty() && self.short.is_none() {
+ true
+ } else {
+ self.positional
+ }
+ }
+
+ /// Sets the full name list.
+ pub fn set_full(&mut self, full: &'a [&'a str]) {
+ self.full = full;
+ }
+
+ /// Sets the short name.
+ pub fn set_short(&mut self, short: Option<char>) {
+ self.short = short;
+ }
+
+ /// Sets whether the parameter is positional.
+ pub fn set_positional(&mut self, positional: bool) {
+ self.positional = positional;
+ }
+
+ /// Sets the full name list and returns self.
+ pub fn with_full(mut self, full: &'a [&'a str]) -> Self {
+ self.full = full;
+ self
+ }
+
+ /// Clears the full name list (sets it to an empty slice) and returns self.
+ pub fn without_full(mut self) -> Self {
+ self.full = &[];
+ self
+ }
+
+ /// Sets the short name to the given character and returns self.
+ pub fn with_short(mut self, short: char) -> Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Clears the short name (sets it to None) and returns self.
+ pub fn without_short(mut self) -> Self {
+ self.short = None;
+ self
+ }
+
+ /// Sets whether the parameter is positional and returns self.
+ pub fn with_positional(mut self, positional: bool) -> Self {
+ self.positional = positional;
+ self
+ }
+
+ /// Returns a reference to the current parse result.
+ pub fn result(&self) -> &PickerResult<Type> {
+ &self.result
+ }
+
+ /// Returns a mutable reference to the current parse result.
+ pub fn result_mut(&mut self) -> &mut PickerResult<Type> {
+ &mut self.result
+ }
+
+ /// Sets the parse result.
+ pub fn set_result(&mut self, result: PickerResult<Type>) {
+ self.result = result;
+ }
+
+ /// Replaces the parse result with `PickerResult::Unparsed` and returns self.
+ pub fn reset_result(mut self) -> Self {
+ self.result = PickerResult::Unparsed;
+ self
+ }
+
+ /// Sets the parse result and returns self.
+ pub fn with_result(mut self, result: PickerResult<Type>) -> Self {
+ self.result = result;
+ self
+ }
+}
diff --git a/mingling_picker/src/result.rs b/mingling_picker/src/result.rs
new file mode 100644
index 0000000..1fad4f5
--- /dev/null
+++ b/mingling_picker/src/result.rs
@@ -0,0 +1,59 @@
+use crate::Pickable;
+
+/// Represents the result of parsing or looking up a value.
+///
+/// This enum is generic over the type being parsed. It models four possible outcomes:
+/// - [`Unparsed`](PickerResult::Unparsed): The value has not yet been parsed (default).
+/// - [`Parsed`](PickerResult::Parsed): The value was successfully parsed into `Type`.
+/// - [`NotFound`](PickerResult::NotFound): The requested value could not be found.
+/// - [`FormatError`](PickerResult::FormatError): The input could not be parsed due to a format error.
+#[derive(Default)]
+pub enum PickerResult<Type>
+where
+ Type: Default + Pickable,
+{
+ /// The value has not yet been parsed (default).
+ #[default]
+ Unparsed,
+
+ /// The value was successfully parsed into `Type`.
+ Parsed(Type),
+
+ /// The requested value could not be found.
+ NotFound,
+
+ /// The input could not be parsed due to a format error.
+ FormatError,
+}
+
+impl<Type, E> From<Result<Type, E>> for PickerResult<Type>
+where
+ Type: Default + Pickable,
+{
+ /// Converts a `Result<Type, E>` into a `PickerResult<Type>`.
+ ///
+ /// - `Ok(value)` maps to [`Parsed(value)`](PickerResult::Parsed).
+ /// - `Err(_)` maps to [`FormatError`](PickerResult::FormatError).
+ fn from(result: Result<Type, E>) -> Self {
+ match result {
+ Ok(value) => PickerResult::Parsed(value),
+ Err(_) => PickerResult::FormatError,
+ }
+ }
+}
+
+impl<Type> From<Option<Type>> for PickerResult<Type>
+where
+ Type: Default + Pickable,
+{
+ /// Converts an `Option<Type>` into a `PickerResult<Type>`.
+ ///
+ /// - `Some(value)` maps to [`Parsed(value)`](PickerResult::Parsed).
+ /// - `None` maps to [`NotFound`](PickerResult::NotFound).
+ fn from(option: Option<Type>) -> Self {
+ match option {
+ Some(value) => PickerResult::Parsed(value),
+ None => PickerResult::NotFound,
+ }
+ }
+}
diff --git a/mingling_picker_macros/src/internal_repeat.rs b/mingling_picker_macros/src/internal_repeat.rs
new file mode 100644
index 0000000..9feedfd
--- /dev/null
+++ b/mingling_picker_macros/src/internal_repeat.rs
@@ -0,0 +1,204 @@
+use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree};
+
+pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream {
+ let tokens: Vec<TokenTree> = input.into_iter().collect();
+ let (range_start, range_end, body_start) = parse_range(&tokens);
+
+ let mut body: Vec<TokenTree> = tokens[body_start..].to_vec();
+ if body.len() == 1 {
+ if let TokenTree::Group(g) = &body[0] {
+ if g.delimiter() == Delimiter::Brace {
+ body = g.stream().into_iter().collect();
+ }
+ }
+ }
+
+ let mut result = Vec::new();
+ for i in range_start..=range_end {
+ result.extend(expand_body(&body, i));
+ }
+ result.into_iter().collect()
+}
+
+/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat).
+/// Returns `(start, end_inclusive, body_start_index)`.
+fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) {
+ // Find => separator
+ let arrow_pos = tokens.windows(2).position(|w| {
+ matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=')
+ && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>')
+ });
+
+ let (arrow_pos, body_start) = match arrow_pos {
+ Some(p) => (p, p + 2),
+ None => return (1, 12, 0), // fallback
+ };
+
+ let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect();
+
+ // Try to find `..` or `..=` pattern
+ // `..` is two Punct('.') tokens
+ let dotdot = before.windows(2).position(|w| {
+ matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.')
+ && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.')
+ });
+
+ if let Some(dd) = dotdot {
+ // Start value: tokens before `..`
+ let start = parse_usize_tokens(&before[..dd]);
+ let after_dd = &before[dd + 2..];
+
+ // Check for `..=` (inclusive range)
+ let (inclusive, end_tokens) = if after_dd.first().map_or(
+ false,
+ |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '='),
+ ) {
+ (true, &after_dd[1..])
+ } else {
+ (false, after_dd)
+ };
+
+ let end = parse_usize_tokens(end_tokens);
+
+ if inclusive {
+ (start, end, body_start)
+ } else {
+ // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1
+ if end > start {
+ (start, end - 1, body_start)
+ } else {
+ (1, 12, body_start) // fallback
+ }
+ }
+ } else {
+ // No `..` found — fallback to simple count
+ let count = parse_usize_tokens(&before);
+ (1, count, body_start)
+ }
+}
+
+/// Parse a sequence of tokens as a single usize value.
+fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize {
+ let s: String = tokens
+ .iter()
+ .map(|t| match t {
+ TokenTree::Literal(l) => l.to_string(),
+ TokenTree::Ident(id) => id.to_string(),
+ _ => String::new(),
+ })
+ .collect::<Vec<_>>()
+ .join("")
+ .replace(' ', "");
+
+ s.parse().unwrap_or(12)
+}
+
+/// Walk tokens, replacing `$` in identifier tails, and expanding
+/// `( … )+` / `( … ,)+` / `( … ;)+` groups.
+fn expand_body(tokens: &[TokenTree], outer: usize) -> Vec<TokenTree> {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i < tokens.len() {
+ // Check for a parenthesized repetition group: ( ... ) sep? +
+ if let Some(exp) = try_expand_paren_group(tokens, i, outer) {
+ let (items, consumed) = exp;
+ out.extend(items);
+ i += consumed;
+ continue;
+ }
+
+ // Identifier followed by `$` → combined ident + number
+ if let TokenTree::Ident(id) = &tokens[i] {
+ if i + 1 < tokens.len() {
+ if let TokenTree::Punct(p) = &tokens[i + 1] {
+ if p.as_char() == '$' {
+ let name = format!("{}{outer}", id.to_string());
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 2;
+ continue;
+ }
+ }
+ }
+ out.push(tokens[i].clone());
+ i += 1;
+ continue;
+ }
+
+ match &tokens[i] {
+ TokenTree::Punct(p) if p.as_char() == '$' => {
+ out.push(TokenTree::Literal(Literal::usize_suffixed(outer)));
+ }
+ TokenTree::Group(g) => {
+ let inner = expand_body_vec(&g.stream(), outer);
+ out.push(TokenTree::Group(Group::new(
+ g.delimiter(),
+ inner.into_iter().collect(),
+ )));
+ }
+ other => out.push(other.clone()),
+ }
+ i += 1;
+ }
+ out
+}
+
+fn expand_body_vec(stream: &TokenStream, outer: usize) -> Vec<TokenTree> {
+ let v: Vec<TokenTree> = stream.clone().into_iter().collect();
+ expand_body(&v, outer)
+}
+
+/// Try to expand `( … )+` / `( … ,)+` / `( … ;)+` at position `i`.
+///
+/// The `+` is AFTER the closing paren. An optional separator (`,` or `;`)
+/// may appear between `)` and `+`.
+/// Returns `(expanded_tokens, consumed_count)` or `None`.
+fn try_expand_paren_group(
+ tokens: &[TokenTree],
+ i: usize,
+ outer: usize,
+) -> Option<(Vec<TokenTree>, usize)> {
+ let group = match tokens.get(i)? {
+ TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g,
+ _ => return None,
+ };
+
+ // Check tokens AFTER the group for `+` (optionally preceded by `,` or `;`)
+ let rest = &tokens[i + 1..];
+ let sep: Option<&str> = match rest.first() {
+ Some(TokenTree::Punct(p)) if p.as_char() == ',' => {
+ if matches!(rest.get(1), Some(TokenTree::Punct(p)) if p.as_char() == '+') {
+ Some(",")
+ } else {
+ return None;
+ }
+ }
+ Some(TokenTree::Punct(p)) if p.as_char() == ';' => {
+ if matches!(rest.get(1), Some(TokenTree::Punct(p)) if p.as_char() == '+') {
+ Some(";")
+ } else {
+ return None;
+ }
+ }
+ Some(TokenTree::Punct(p)) if p.as_char() == '+' => None,
+ _ => return None,
+ };
+
+ let consumed = if sep.is_some() { 3 } else { 2 }; // group + sep? + +
+
+ let inner: Vec<TokenTree> = group.stream().into_iter().collect();
+
+ let mut out = Vec::new();
+ for n in 1..=outer {
+ if n > 1 {
+ if let Some(s) = sep {
+ out.push(TokenTree::Punct(proc_macro::Punct::new(
+ s.chars().next().unwrap(),
+ proc_macro::Spacing::Alone,
+ )));
+ }
+ }
+ out.extend(expand_body(&inner, n));
+ }
+
+ Some((out, consumed))
+}
diff --git a/mingling_picker_macros/src/lib.rs b/mingling_picker_macros/src/lib.rs
index 8b13789..a3c93eb 100644
--- a/mingling_picker_macros/src/lib.rs
+++ b/mingling_picker_macros/src/lib.rs
@@ -1 +1,30 @@
+use proc_macro::TokenStream;
+mod internal_repeat;
+mod req;
+
+/// Core proc-macro: repeats a template body `count` times.
+///
+/// Internal call signature: `internal_repeat!(count => { template })`
+#[proc_macro]
+pub fn internal_repeat(input: TokenStream) -> TokenStream {
+ internal_repeat::internal_repeat(input)
+}
+
+/// Quick builder for `PickerRequirement`.
+///
+/// # Syntax
+///
+/// ```ignore
+/// use mingling_picker_macros::req;
+///
+/// let basic = req![name: String];
+/// let with_short_name = req![name: String, 'n'];
+/// let with_short_alias = req![name: String, 'n', "alias"];
+/// let positional = req![String];
+/// let positional_with_name = req![String, 'n', "alias"];
+/// ```
+#[proc_macro]
+pub fn req(input: TokenStream) -> TokenStream {
+ req::req(input)
+}
diff --git a/mingling_picker_macros/src/req.rs b/mingling_picker_macros/src/req.rs
new file mode 100644
index 0000000..7e652fa
--- /dev/null
+++ b/mingling_picker_macros/src/req.rs
@@ -0,0 +1,214 @@
+use proc_macro::{TokenStream, TokenTree};
+use proc_macro2::TokenStream as TS2;
+use quote::quote;
+
+pub(crate) fn req(input: TokenStream) -> TokenStream {
+ let tokens: Vec<TokenTree> = input.into_iter().collect();
+ let args = split_at_commas(&tokens);
+ if args.is_empty() {
+ return quote! { compile_error!("req! requires at least one argument") }.into();
+ }
+
+ let first = &args[0];
+ let rest = &args[1..];
+
+ // Validate: at most one char literal
+ let char_count = rest.iter().filter(|a| is_char_literal(a)).count();
+ if char_count > 1 {
+ return quote! { compile_error!("req! only supports at most one short name") }.into();
+ }
+
+ // Extract short char and string aliases
+ let short_char: Option<char> = rest
+ .iter()
+ .find(|a| is_char_literal(a))
+ .and_then(|a| extract_char(a));
+ let aliases: Vec<String> = rest
+ .iter()
+ .filter(|a| is_string_literal(a))
+ .filter_map(|a| extract_string(a))
+ .collect();
+
+ // Parse first argument
+ let colon_pos = first
+ .iter()
+ .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':'));
+ let has_generics = first
+ .iter()
+ .any(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '<'));
+
+ let first_slice = first.as_slice();
+ let (name, ty, is_named) = match colon_pos {
+ Some(pos) if pos > 0 && !is_double_colon(first, pos) => {
+ // `name : Type`
+ (
+ Some(&first_slice[..pos]),
+ Some(&first_slice[pos + 1..]),
+ true,
+ )
+ }
+ _ => {
+ if has_generics || !is_single_ident(first) || !has_char_or_string(rest) {
+ // Type path, generic type, or bare type (no more args)
+ (None, Some(first_slice), false)
+ } else {
+ // Single ident followed by char/string → bare name
+ (Some(first_slice), None, true)
+ }
+ }
+ };
+
+ // Build full names
+ let full_names: Vec<String> = match name {
+ Some(n) => {
+ let mut v = vec![join_idents(n)];
+ v.extend(aliases);
+ v
+ }
+ None => aliases,
+ };
+
+ // Generate code
+ let import = quote! { ::mingling::picker::PickerRequirement };
+
+ // Type parameter
+ let ty_ts: TS2 = ty
+ .map(|t| {
+ let ts: TokenStream = t.iter().cloned().collect();
+ ts.to_string().parse().unwrap()
+ })
+ .unwrap_or(TS2::new());
+
+ let with_type = if ty.is_some() {
+ quote! { #import::<#ty_ts> }
+ } else {
+ quote! { #import::<_> }
+ };
+
+ // .with_full(...)
+ let with_full = if !full_names.is_empty() {
+ let strs: Vec<proc_macro2::Literal> = full_names
+ .iter()
+ .map(|s| proc_macro2::Literal::string(s))
+ .collect();
+ quote! { .with_full(&[#(#strs),*]) }
+ } else {
+ TS2::new()
+ };
+
+ // .with_short(...)
+ let with_short = short_char
+ .map(|c| {
+ let lit = proc_macro2::Literal::character(c);
+ quote! { .with_short(#lit) }
+ })
+ .unwrap_or(TS2::new());
+
+ // .with_positional(...)
+ let pos = !is_named;
+
+ let result = quote! {
+ #with_type::default()
+ #with_full
+ #with_short
+ .with_positional(#pos)
+ };
+
+ result.into()
+}
+
+fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> {
+ let mut result = vec![Vec::new()];
+ let mut depth = 0u32;
+ for t in tokens {
+ match t {
+ TokenTree::Group(_g) => {
+ depth += 1;
+ result.last_mut().unwrap().push(t.clone());
+ depth -= 1;
+ }
+ TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => {
+ result.push(Vec::new());
+ }
+ _ => result.last_mut().unwrap().push(t.clone()),
+ }
+ }
+ result
+}
+
+fn is_double_colon(tokens: &[TokenTree], pos: usize) -> bool {
+ pos > 0 && matches!(&tokens[pos - 1], TokenTree::Punct(p) if p.as_char() == ':')
+}
+
+fn is_single_ident(tokens: &[TokenTree]) -> bool {
+ tokens.len() == 1 && matches!(&tokens[0], TokenTree::Ident(_))
+}
+
+fn is_char_literal(tokens: &[TokenTree]) -> bool {
+ tokens.len() == 1
+ && matches!(&tokens[0], TokenTree::Literal(l) if {
+ let s = l.to_string();
+ s.starts_with('\'') && s.len() >= 3
+ })
+}
+
+fn is_string_literal(tokens: &[TokenTree]) -> bool {
+ tokens.len() == 1
+ && matches!(&tokens[0], TokenTree::Literal(l) if {
+ l.to_string().starts_with('"')
+ })
+}
+
+fn has_char_or_string(args: &[Vec<TokenTree>]) -> bool {
+ args.iter()
+ .any(|a| is_char_literal(a) || is_string_literal(a))
+}
+
+fn extract_char(tokens: &[TokenTree]) -> Option<char> {
+ match &tokens[0] {
+ TokenTree::Literal(l) => {
+ let s = l.to_string();
+ 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();
+ match inner.as_str() {
+ "n" => Some('\n'),
+ "t" => Some('\t'),
+ "r" => Some('\r'),
+ "0" => Some('\0'),
+ "\\\\" => Some('\\'),
+ "\\'" => Some('\''),
+ _ => inner.chars().next(),
+ }
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+}
+
+fn extract_string(tokens: &[TokenTree]) -> Option<String> {
+ match &tokens[0] {
+ TokenTree::Literal(l) => {
+ let s = l.to_string();
+ let cs: Vec<char> = s.chars().collect();
+ if cs.len() >= 2 && cs[0] == '"' && cs[cs.len() - 1] == '"' {
+ Some(cs[1..cs.len() - 1].iter().collect())
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+}
+
+fn join_idents(tokens: &[TokenTree]) -> String {
+ tokens
+ .iter()
+ .map(|t| match t {
+ TokenTree::Ident(id) => id.to_string(),
+ _ => String::new(),
+ })
+ .collect()
+}