From c23c590330af83afb6e146bcd9b0a274b3689d22 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 17 Aug 2026 03:27:46 +0800 Subject: refactor!: remove Node type and simplify dispatcher macro syntax The `dispatcher!` macro no longer requires a `CMD*` dispatcher type argument; the dispatcher struct is now generated internally as `__Dispatcher{Pascal}`. The `Node` type, `node!` macro, and `Dispatcher::node()` / `clone_dispatcher()` methods are removed. --- mingling_macros/src/attr/command.rs | 53 +++++------- mingling_macros/src/attr/dispatcher_clap.rs | 39 ++++----- mingling_macros/src/func.rs | 1 - mingling_macros/src/func/dispatcher.rs | 123 +++++++++++++-------------- mingling_macros/src/func/node.rs | 60 ------------- mingling_macros/src/func/program_comp_gen.rs | 4 +- mingling_macros/src/lib.rs | 49 +---------- 7 files changed, 101 insertions(+), 228 deletions(-) delete mode 100644 mingling_macros/src/func/node.rs (limited to 'mingling_macros') diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs index 4542bd7..6f2409c 100644 --- a/mingling_macros/src/attr/command.rs +++ b/mingling_macros/src/attr/command.rs @@ -12,12 +12,10 @@ use syn::{FnArg, Ident, ItemFn, LitStr, PatType, Token, Type}; /// /// Supports: /// - `node = "dot.separated.path"` — explicit command path -/// - `name = CMDName` — explicit CMD struct name /// - `entry = EntryName` — explicit Entry struct name /// - bare paths like `routeify`, `::mingling::macros::routeify` — extension attrs for the original fn struct CommandArgs { node: Option, - name: Option, entry: Option, exts: Vec, } @@ -25,7 +23,6 @@ struct CommandArgs { impl Parse for CommandArgs { fn parse(input: ParseStream) -> syn::Result { let mut node = None; - let mut name = None; let mut entry = None; let mut exts = Vec::new(); @@ -41,19 +38,18 @@ impl Parse for CommandArgs { } node = Some(input.parse()?); } else if key == "name" { - if name.is_some() { - return Err(input.error("duplicate `name` argument")); - } - name = Some(input.parse()?); + return Err(input.error( + "`name = ...` was removed in 0.5.0; the dispatcher struct is generated internally", + )); } else if key == "entry" { if entry.is_some() { return Err(input.error("duplicate `entry` argument")); } entry = Some(input.parse()?); } else { - return Err(input.error(format!( - "unknown key `{key}`; expected `node`, `name`, or `entry`" - ))); + return Err( + input.error(format!("unknown key `{key}`; expected `node` or `entry`")) + ); } } else { // Extension path (e.g. `routeify` or `::mingling::macros::routeify`) @@ -67,12 +63,7 @@ impl Parse for CommandArgs { } } - Ok(Self { - node, - name, - entry, - exts, - }) + Ok(Self { node, entry, exts }) } } @@ -127,17 +118,15 @@ fn handle_async(f: &ItemFn) -> Result<(TokenStream2, TokenStream2), TokenStream2 struct ResolvedNames { /// `node_str` as a string literal token node_lit: LitStr, - /// Whether the user supplied any explicit override (node/name/entry) + /// Whether the user supplied any explicit override (node/entry) has_overrides: bool, - /// CMD struct name (e.g. `CMDGreet`) - cmd_name: Ident, /// Entry struct name (e.g. `EntryGreet`) entry_type: Ident, /// Chain wrapper function name (e.g. `__command_chain_greet`) chain_fn_name: Ident, } -/// Resolves `node`, `cmd_name`, `entry_type`, and `chain_fn_name` from +/// Resolves `node`, `entry_type`, and `chain_fn_name` from /// the attribute args and the original function name. fn resolve_names(fn_name: &Ident, args: &CommandArgs) -> ResolvedNames { let fn_name_str = fn_name.to_string(); @@ -148,12 +137,7 @@ fn resolve_names(fn_name: &Ident, args: &CommandArgs) -> ResolvedNames { .map_or_else(|| default_node_from_fn(fn_name), syn::LitStr::value); let node_lit = syn::LitStr::new(&node_str, fn_name.span()); - let has_overrides = args.node.is_some() || args.name.is_some() || args.entry.is_some(); - - let cmd_name = args.name.clone().unwrap_or_else(|| { - let pascal = just_fmt::pascal_case!(&node_str); - Ident::new(&format!("CMD{pascal}"), fn_name.span()) - }); + let has_overrides = args.node.is_some() || args.entry.is_some(); let entry_type = args.entry.clone().unwrap_or_else(|| { let pascal = just_fmt::pascal_case!(&node_str); @@ -165,7 +149,6 @@ fn resolve_names(fn_name: &Ident, args: &CommandArgs) -> ResolvedNames { ResolvedNames { node_lit, has_overrides, - cmd_name, entry_type, chain_fn_name, } @@ -247,13 +230,12 @@ fn build_call_args(sig: &syn::Signature) -> Vec { /// Generates the `dispatcher!(...)` call. /// /// - No overrides → abbreviated form: `dispatcher!("node")` -/// - Any override → explicit form: `dispatcher!("node", CMDName => EntryName)` +/// - Any override → explicit form: `dispatcher!("node", EntryType)` fn build_dispatcher_invoke(names: &ResolvedNames) -> TokenStream2 { let node_lit = &names.node_lit; if names.has_overrides { - let cmd_name = &names.cmd_name; let entry_type = &names.entry_type; - quote! { ::mingling::macros::dispatcher!(#node_lit, #cmd_name => #entry_type); } + quote! { ::mingling::macros::dispatcher!(#node_lit, #entry_type); } } else { quote! { ::mingling::macros::dispatcher!(#node_lit); } } @@ -271,7 +253,6 @@ pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream let args: CommandArgs = if attr.is_empty() { CommandArgs { node: None, - name: None, entry: None, exts: Vec::new(), } @@ -332,7 +313,13 @@ pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream quote! { #vis use super::#ident; } }; - let cmd_name = &names.cmd_name; + // hidden dispatcher struct generated by the internal `dispatcher!` call + let hidden_dispatcher = { + let node_str = names.node_lit.value(); + let pascal = just_fmt::pascal_case!(&node_str); + Ident::new(&format!("__Dispatcher{pascal}"), fn_name.span()) + }; + let entry_type = &names.entry_type; // assemble output @@ -351,7 +338,7 @@ pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream // hidden module gathering all generated types for pathf / external access #[doc(hidden)] #vis mod #mod_name { - #vis use super::#cmd_name; + #vis use super::#hidden_dispatcher; #vis use super::#entry_type; #vis use super::#chain_internal; #dispatcher_internal diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 2f45b14..46238d6 100644 --- a/mingling_macros/src/attr/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -30,7 +30,13 @@ impl Parse for ClapOptions { } let key: Ident = input.parse()?; - input.parse::()?; + if input.parse::().is_err() { + return Err(syn::Error::new( + key.span(), + "expected `key = value`; note: the explicit CMD struct argument \ + was removed in 0.5.0, use `dispatcher_clap!(\"name\", help = ..., error = ...)`", + )); + } if key == "error" { let value: Ident = input.parse()?; @@ -63,18 +69,15 @@ impl Parse for ClapOptions { /// Input for the `dispatcher_clap` attribute struct DispatcherClapInput { - /// `("cmd", Disp, ...)` + /// `("cmd", options...)` command_name: LitStr, - dispatcher_struct: Ident, options: ClapOptions, } impl Parse for DispatcherClapInput { fn parse(input: ParseStream) -> syn::Result { - // Format: "cmd", Disp, ... + // Format: "cmd", options... let command_name: LitStr = input.parse()?; - input.parse::()?; - let dispatcher_struct: Ident = input.parse()?; let options = if input.is_empty() { ClapOptions { @@ -87,13 +90,13 @@ impl Parse for DispatcherClapInput { Ok(Self { command_name, - dispatcher_struct, options, }) } } #[cfg(feature = "clap")] +#[allow(clippy::too_many_lines)] pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let attr_input = parse_macro_input!(attr as DispatcherClapInput); let input_struct = parse_macro_input!(item as ItemStruct); @@ -102,7 +105,12 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke let program_path = crate::default_program_path(); let command_name_str = attr_input.command_name.value(); - let dispatcher_struct = &attr_input.dispatcher_struct; + + // The dispatcher struct is now generated internally. + let dispatcher_struct = Ident::new( + &format!("__Dispatcher{}", just_fmt::pascal_case!(&command_name_str)), + attr_input.command_name.span(), + ); let options = &attr_input.options; // Generate the `begin` method body @@ -141,8 +149,7 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke // Generate the #[help] block if help = true let help_gen = if options.help_enabled { - let dispatcher_name_str = dispatcher_struct.to_string(); - let help_fn_name_str = format!("__{}_help", just_fmt::snake_case!(&dispatcher_name_str)); + let help_fn_name_str = format!("__{}_help", just_fmt::snake_case!(&command_name_str)); let help_fn_name = Ident::new(&help_fn_name_str, proc_macro2::Span::call_site()); Some(quote! { @@ -175,7 +182,7 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke }; let compile_time_registration = - get_compile_time_registration(&command_name_str, dispatcher_struct, struct_name); + get_compile_time_registration(&command_name_str, &dispatcher_struct, struct_name); let expanded = quote! { // Keep the original struct definition @@ -196,10 +203,6 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke pub(crate) struct #dispatcher_struct; impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct { - fn node(&self) -> ::mingling::Node { - ::mingling::macros::node!(#command_name_str) - } - fn begin( &self, args: Vec, @@ -211,12 +214,6 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke #begin_body } - - fn clone_dispatcher( - &self, - ) -> Box> { - Box::new(#dispatcher_struct) - } } }; diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 834fa7a..57d0ec5 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -9,7 +9,6 @@ pub(crate) mod gen_program; pub(crate) mod group; #[cfg(all(feature = "structural_renderer", feature = "extras"))] pub(crate) mod group_structural; -pub(crate) mod node; pub(crate) mod pack; #[cfg(feature = "extras")] pub(crate) mod pack_err; diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index 6183993..c4d67a9 100644 --- a/mingling_macros/src/func/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -6,13 +6,14 @@ use syn::parse::{Parse, ParseStream}; use syn::{Attribute, Ident, LitStr, Token}; enum DispatcherChainInput { + /// `dispatcher!("name", EntryType)` — explicit entry type Default { cmd_attrs: Vec, entry_attrs: Vec, command_name: syn::LitStr, - command_struct: Ident, pack: Ident, }, + /// `dispatcher!("name")` — entry type derived from the command name #[cfg(feature = "extras")] Auto { cmd_attrs: Vec, @@ -22,106 +23,107 @@ enum DispatcherChainInput { impl Parse for DispatcherChainInput { fn parse(input: ParseStream) -> syn::Result { - // Collect outer attributes for the CMD struct + // Collect outer attributes for the hidden dispatcher struct let cmd_attrs = input.call(Attribute::parse_outer)?; - if input.peek(syn::LitStr) { - // Parse the command name string first - let command_name: LitStr = input.parse()?; - - // Check if this is the abbreviated form: just "command_name" without ", CMD => Entry" - if input.is_empty() { - #[cfg(feature = "extras")] - { - return Ok(Self::Auto { - cmd_attrs, - command_name, - }); - } - #[cfg(not(feature = "extras"))] - { - return Err(syn::Error::new( - command_name.span(), - "expected `, CommandStruct => EntryStruct` after command name", - )); - } + let command_name: LitStr = input.parse()?; + + if input.is_empty() { + // Abbreviated form: just "command_name" + #[cfg(feature = "extras")] + { + return Ok(Self::Auto { + cmd_attrs, + command_name, + }); + } + #[cfg(not(feature = "extras"))] + { + return Err(syn::Error::new( + command_name.span(), + "expected `, EntryType` after command name", + )); } + } - // Default format: "command_name", CommandStruct => ChainStruct - input.parse::()?; - let command_struct = input.parse()?; - input.parse::]>()?; - let entry_attrs = input.call(Attribute::parse_outer)?; - let pack = input.parse()?; - - Ok(Self::Default { - cmd_attrs, - entry_attrs, - command_name, - command_struct, - pack, - }) - } else { - Err(input.lookahead1().error()) + // Explicit form: "command_name", EntryType + input.parse::()?; + let entry_attrs = input.call(Attribute::parse_outer)?; + let pack: Ident = input.parse()?; + + // The old `"name", CMD => Entry` form was removed in 0.5.0. + if input.peek(Token![=>]) { + return Err(syn::Error::new( + pack.span(), + "the `dispatcher!(\"name\", CMD => Entry)` form was removed in 0.5.0; \ + use `dispatcher!(\"name\", Entry)` — the dispatcher struct is generated internally", + )); } + + Ok(Self::Default { + cmd_attrs, + entry_attrs, + command_name, + pack, + }) } } -// NOTICE: The token stream generation patterns in `dispatcher_chain` and `dispatcher_render` -// are nearly identical and could benefit from refactoring into common helper functions. - -#[allow(clippy::too_many_lines)] pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { - // Parse the input let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); #[cfg(not(feature = "extras"))] - let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { + let (command_name, pack, cmd_attrs, entry_attrs) = match dispatcher_input { DispatcherChainInput::Default { cmd_attrs, entry_attrs, command_name, - command_struct, pack, - } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), + } => (command_name, pack, cmd_attrs, entry_attrs), }; #[cfg(feature = "extras")] - let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { + let (command_name, pack, cmd_attrs, entry_attrs) = match dispatcher_input { DispatcherChainInput::Default { cmd_attrs, entry_attrs, command_name, - command_struct, pack, - } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), + } => (command_name, pack, cmd_attrs, entry_attrs), DispatcherChainInput::Auto { cmd_attrs, command_name, } => { let command_name_str = command_name.value(); let pascal = just_fmt::pascal_case!(&command_name_str); - let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span()); let pack = Ident::new(&format!("Entry{pascal}"), command_name.span()); - (command_name, command_struct, pack, cmd_attrs, Vec::new()) + (command_name, pack, cmd_attrs, Vec::new()) } }; let command_name_str = command_name.value(); + let hidden_dispatcher = Ident::new( + &format!("__Dispatcher{}", just_fmt::pascal_case!(&command_name_str)), + command_name.span(), + ); let comp_entry = get_comp_entry(&pack); let compile_time_registration = - get_compile_time_registration(&command_name_str, &command_struct, &pack); + get_compile_time_registration(&command_name_str, &hidden_dispatcher, &pack); let program_type = crate::default_program_path(); let expanded = quote! { + ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec); + #(#cmd_attrs)* + #[doc(hidden)] #[derive(Debug, Default)] - pub struct #command_struct; + #[allow(nonstandard_style)] + pub struct #hidden_dispatcher; - ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec); + #compile_time_registration impl From<#pack> for crate::Entry { fn from(value: #pack) -> Self { @@ -130,19 +132,12 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { } #comp_entry - #compile_time_registration - impl ::mingling::Dispatcher<#program_type> for #command_struct { - fn node(&self) -> ::mingling::Node { - ::mingling::macros::node!(#command_name_str) - } + impl ::mingling::Dispatcher<#program_type> for #hidden_dispatcher { fn begin(&self, args: Vec) -> ::mingling::ChainProcess<#program_type> { use ::mingling::Grouped; ::mingling::Routable::to_chain(#pack::new(args)) } - fn clone_dispatcher(&self) -> Box> { - Box::new(#command_struct) - } } }; @@ -173,11 +168,11 @@ fn get_comp_entry(_entry_name: &Ident) -> TokenStream2 { /// (trie vs. linear list) is generated later by `gen_program!`. fn get_compile_time_registration( command_name_str: &str, - command_struct: &Ident, + dispatcher_struct: &Ident, entry_name: &Ident, ) -> TokenStream2 { let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site()); quote! { - ::mingling::macros::register_dispatcher!(#node_name_lit, #command_struct, #entry_name); + ::mingling::macros::register_dispatcher!(#node_name_lit, #dispatcher_struct, #entry_name); } } diff --git a/mingling_macros/src/func/node.rs b/mingling_macros/src/func/node.rs deleted file mode 100644 index 9963037..0000000 --- a/mingling_macros/src/func/node.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Doc Not Optimize -use just_fmt::kebab_case; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{LitStr, Result as SynResult}; - -/// Parses a string literal input for the node macro -struct NodeInput { - path: LitStr, -} - -impl Parse for NodeInput { - fn parse(input: ParseStream) -> SynResult { - Ok(Self { - path: input.parse()?, - }) - } -} - -pub(crate) fn node(input: TokenStream) -> TokenStream { - // Parse the input as a string literal - let input_parsed = syn::parse_macro_input!(input as NodeInput); - let path_str = input_parsed.path.value(); - - // If the input string is empty, return an empty Node - if path_str.is_empty() { - return quote! { - mingling::Node::default() - } - .into(); - } - - // Split the path by dots - let parts: Vec = path_str - .split('.') - .map(|s| { - if s.starts_with('_') { - s.to_string() - } else { - kebab_case!(s) - } - }) - .collect(); - - // Build the expression starting from Node::default() - let mut expr: TokenStream2 = quote! { - mingling::Node::default() - }; - - // Add .join() calls for each part of the path - for part in parts { - expr = quote! { - #expr.join(#part) - }; - } - - expr.into() -} diff --git a/mingling_macros/src/func/program_comp_gen.rs b/mingling_macros/src/func/program_comp_gen.rs index 7f77d46..ed04379 100644 --- a/mingling_macros/src/func/program_comp_gen.rs +++ b/mingling_macros/src/func/program_comp_gen.rs @@ -42,13 +42,14 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { let internal_dispatcher_comp = quote! { use __internal_completion_mod::__internal_dispatcher_comp; + use __internal_completion_mod::__DispatcherComp; }; let comp_dispatcher = quote! { #[doc(hidden)] mod __internal_completion_mod { use ::mingling::Grouped; - ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); + ::mingling::macros::dispatcher!("__comp", CompletionContext); ::mingling::macros::pack!( CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) ); @@ -56,7 +57,6 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { #internal_dispatcher_comp use __internal_completion_mod::CompletionContext; use __internal_completion_mod::CompletionSuggest; - pub use __internal_completion_mod::CMDCompletion; #fn_exec_comp diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index ad84548..607ba9f 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -42,7 +42,7 @@ pub(crate) use func::group as group_impl; use func::pack_err; #[cfg(feature = "comp")] use func::suggest; -use func::{dispatcher, node, pack}; +use func::{dispatcher, pack}; use systems::res_injection; pub(crate) fn default_program_path() -> proc_macro2::TokenStream { quote::quote! { crate::ThisProgram } @@ -183,47 +183,6 @@ pub fn group_structural(input: TokenStream) -> TokenStream { func::group_structural::group_structural(input) } -/// Creates a `Node` from a dot-separated path string. -/// -/// Each segment is converted to kebab-case (unless it starts with `_`). -/// Segments are joined via `.join()` calls, building a path hierarchy for -/// command matching. -/// -/// # Syntax -/// -/// ```rust,ignore -/// node!("subcommand") -/// node!("sub.subsub") -/// node!("") // empty → Node::default() -/// ``` -/// -/// # Example -/// -/// ```rust,ignore -/// use mingling::macros::node; -/// -/// // Creates a single-level node for "hello" -/// let n = node!("hello"); -/// -/// // Creates a two-level node for "remote control" -/// let n = node!("remote.control"); -/// ``` -/// -/// # Internals -/// -/// The generated code is equivalent to: -/// ```rust,ignore -/// Node::default().join("hello") -/// Node::default().join("remote").join("control") -/// ``` -/// -/// This macro is typically used internally by `dispatcher!` and should rarely -/// need to be called directly. -#[proc_macro] -pub fn node(input: TokenStream) -> TokenStream { - node::node(input) -} - /// Creates a type-safe wrapper struct around an inner type, with automatic /// trait implementations for use in the Mingling chain/render pipeline. /// @@ -598,10 +557,8 @@ pub fn empty_result(input: TokenStream) -> TokenStream { /// /// 1. **Entry struct** — A `pack!`-style wrapper around `Vec` (the raw args). /// Registered in the program enum via `register_type!`. -/// 2. **Dispatcher struct** — A zero-sized struct implementing [`Dispatcher`]: -/// - `node()` returns the [`Node`] hierarchy for the command path. +/// 2. **Dispatcher struct** — A hidden zero-sized struct implementing [`Dispatcher`]: /// - `begin(args)` wraps `args` into the entry type and routes to chain. -/// - `clone_dispatcher()` returns a boxed clone. /// 3. **Registration** — Calls `register_dispatcher!` to collect the command /// at compile time (the `dispatch_tree` feature only selects the matching /// strategy generated later by `gen_program!`). @@ -612,12 +569,10 @@ pub fn empty_result(input: TokenStream) -> TokenStream { /// # See also /// /// - `dispatcher_clap!` — For clap-powered argument parsing. -/// - `node!` — For building custom [`Node`] paths. /// - [`#[chain]`](attr.chain.html) — For processing the dispatched entry. /// /// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html /// [`Dispatcher`]: https://docs.rs/mingling/latest/mingling/trait.Dispatcher.html -/// [`Node`]: https://docs.rs/mingling/latest/mingling/struct.Node.html #[proc_macro] pub fn dispatcher(input: TokenStream) -> TokenStream { dispatcher::dispatcher(input) -- cgit