diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-14 01:36:27 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-14 01:36:27 +0800 |
| commit | 4665fc94ab9508d115298dd988e2381354f46c01 (patch) | |
| tree | a2b14f5684c68d88a0a77570d0ac3ac8d863867b | |
| parent | 5ac0f0e98d05e95f7e3c58995f8d79670cbac772 (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.rs | 10 | ||||
| -rw-r--r-- | mingling_picker/src/lib.rs | 20 | ||||
| -rw-r--r-- | mingling_picker/src/pickable.rs | 39 | ||||
| -rw-r--r-- | mingling_picker/src/pickable/implements.rs | 43 | ||||
| -rw-r--r-- | mingling_picker/src/picker.rs | 232 | ||||
| -rw-r--r-- | mingling_picker/src/picker/patterns.rs | 12 | ||||
| -rw-r--r-- | mingling_picker/src/requirement.rs | 150 | ||||
| -rw-r--r-- | mingling_picker/src/result.rs | 59 | ||||
| -rw-r--r-- | mingling_picker_macros/src/internal_repeat.rs | 204 | ||||
| -rw-r--r-- | mingling_picker_macros/src/lib.rs | 29 | ||||
| -rw-r--r-- | mingling_picker_macros/src/req.rs | 214 |
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() +} |
