aboutsummaryrefslogtreecommitdiff
path: root/mingling_picker_macros/src
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 /mingling_picker_macros/src
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`.
Diffstat (limited to 'mingling_picker_macros/src')
-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
3 files changed, 447 insertions, 0 deletions
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()
+}