diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-05-24 17:06:54 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-05-24 17:06:54 +0800 |
| commit | 60e70f5320b2abdb38a2349c18e5bffcfea37ca7 (patch) | |
| tree | 3402af0a2822255c1c3f9c77affe6da81c9d1279 | |
| parent | 11adad7db1b6202d5366527902c3f0a9fb90654f (diff) | |
Add implicit dispatcher macro with auto-derived names
| -rw-r--r-- | CHANGELOG.md | 10 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | docs/res/changlog_examples/feat_program_res.rs | 4 | ||||
| -rw-r--r-- | examples/example-general-renderer/src/main.rs | 6 | ||||
| -rw-r--r-- | examples/example-implicit-dispatcher/Cargo.lock | 76 | ||||
| -rw-r--r-- | examples/example-implicit-dispatcher/Cargo.toml | 8 | ||||
| -rw-r--r-- | examples/example-implicit-dispatcher/src/main.rs | 23 | ||||
| -rw-r--r-- | examples/example-repl-basic/src/main.rs | 27 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 72 | ||||
| -rw-r--r-- | mingling_macros/src/dispatcher.rs | 83 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 22 |
11 files changed, 293 insertions, 39 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a344e3..a6bda6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,16 @@ entry!(MyEntry, ["a", "b", "c"]) entry!["a", "b", "c"] ``` +7. **\[macros\]** Added `dispatcher!` macro with implicit entry/dispatcher name derivation + +```rust +// implicit +dispatcher!("remote.add" /*, CMDRemoteAdd => EntryRemoteAdd */); + +// explicit +dispatcher!("remote.remove", CMDRemoteRemove => EntryRemoteRemove); +``` + #### **BREAKING CHANGES** (API CHANGES): 1. **\[core\]** Panic Unwind will not be supported when the `async` feature is enabled @@ -14,6 +14,7 @@ exclude = [ "examples/example-general-renderer", "examples/example-help", "examples/example-hook", + "examples/example-implicit-dispatcher", "examples/example-panic-unwind", "examples/example-repl-basic", "examples/example-resources", diff --git a/docs/res/changlog_examples/feat_program_res.rs b/docs/res/changlog_examples/feat_program_res.rs index b3533f1..11a1471 100644 --- a/docs/res/changlog_examples/feat_program_res.rs +++ b/docs/res/changlog_examples/feat_program_res.rs @@ -20,12 +20,12 @@ fn main() { program.exec(); } -dispatcher!("modify", ResModifyCommand => ResModifyEntry); +dispatcher!("modify", CMDModify => EntryModify); pack!(DisplayGlobal = ()); #[chain] -fn modify(prev: ResModifyEntry) { +fn modify(prev: EntryModify) { let (name, age) = Picker::<()>::new(prev.inner) .pick::<String>("--name") .pick::<i32>("--age") diff --git a/examples/example-general-renderer/src/main.rs b/examples/example-general-renderer/src/main.rs index 5f74815..3ba4433 100644 --- a/examples/example-general-renderer/src/main.rs +++ b/examples/example-general-renderer/src/main.rs @@ -21,13 +21,13 @@ use mingling::prelude::*; use mingling::{Groupped, parser::Picker, setup::GeneralRendererSetup}; use serde::Serialize; -dispatcher!("render", RenderCommand => RenderCommandEntry); +dispatcher!("render", CMDRender => EntryRender); fn main() { let mut program = ThisProgram::new(); // Add `GeneralRendererSetup` to receive user input `--json` `--yaml` parameters program.with_setup(GeneralRendererSetup); - program.with_dispatcher(RenderCommand); + program.with_dispatcher(CMDRender); program.exec(); } @@ -53,7 +53,7 @@ struct Info { // --------- IMPORTANT --------- #[chain] -fn parse_render(prev: RenderCommandEntry) -> Next { +fn parse_render(prev: EntryRender) -> Next { let (name, age) = Picker::new(prev.inner) .pick::<String>(()) .pick::<i32>(()) diff --git a/examples/example-implicit-dispatcher/Cargo.lock b/examples/example-implicit-dispatcher/Cargo.lock new file mode 100644 index 0000000..7bd56a7 --- /dev/null +++ b/examples/example-implicit-dispatcher/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-implicit-dispatcher" +version = "0.1.0" +dependencies = [ + "mingling", +] + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "mingling" +version = "0.1.9" +dependencies = [ + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/examples/example-implicit-dispatcher/Cargo.toml b/examples/example-implicit-dispatcher/Cargo.toml new file mode 100644 index 0000000..db6fdab --- /dev/null +++ b/examples/example-implicit-dispatcher/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "example-implicit-dispatcher" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" +features = ["extra_macros"] diff --git a/examples/example-implicit-dispatcher/src/main.rs b/examples/example-implicit-dispatcher/src/main.rs new file mode 100644 index 0000000..3dc7f83 --- /dev/null +++ b/examples/example-implicit-dispatcher/src/main.rs @@ -0,0 +1,23 @@ +//! Example Implicit Dispatcher +//! +//! > This example demonstrates how to use the implicit `dispatcher!` definition syntax enabled by `extra_macros` + +use mingling::prelude::*; + +// When using implicit syntax, the entry and dispatcher names will be automatically derived +dispatcher!("remote.add" /*, CMDRemoteAdd => EntryRemoteAdd */); +dispatcher!("remote.remove", CMDRemoteRemove => EntryRemoteRemove); + +fn main() { + let mut program = ThisProgram::new(); + + // --------- IMPORTANT --------- + program.with_dispatcher(CMDRemoteAdd); + // ^^^^^^^^^^^^\_ CMDRemoteAdd is implicitly created + // --------- IMPORTANT --------- + + program.with_dispatcher(CMDRemoteRemove); + program.exec_and_exit(); +} + +gen_program!(); diff --git a/examples/example-repl-basic/src/main.rs b/examples/example-repl-basic/src/main.rs index f02c2f8..f2c871e 100644 --- a/examples/example-repl-basic/src/main.rs +++ b/examples/example-repl-basic/src/main.rs @@ -8,11 +8,10 @@ //! ``` use mingling::{ - REPL, hook::ProgramHook, prelude::*, setup::{BasicREPLOutputSetup, BasicREPLPromptSetup, BasicREPLReadlineSetup}, - this, + this, REPL, }; use std::{env::current_dir, path::PathBuf}; @@ -37,10 +36,10 @@ fn main() { program.with_resource(ResCurrentDir::default()); // Dispatchers - program.with_dispatcher(ChangeDirectoryCommand); - program.with_dispatcher(ListCommand); - program.with_dispatcher(ExitCommand); - program.with_dispatcher(ClearCommand); + program.with_dispatcher(CMDCd); + program.with_dispatcher(CMDLs); + program.with_dispatcher(CMDExit); + program.with_dispatcher(CMDClear); // Setups // Enable basic std::io::stdin().read_line(&mut input) @@ -78,10 +77,10 @@ fn main() { pack!(ErrorDirectoryNotExist = PathBuf); // Create commands: cd ls exit -dispatcher!("cd", ChangeDirectoryCommand => ChangeDirectoryEntry); -dispatcher!("ls", ListCommand => ListEntry); -dispatcher!("exit", ExitCommand => ExitEntry); -dispatcher!("clear", ClearCommand => ClearEntry); +dispatcher!("cd", CMDCd => EntryCd); +dispatcher!("ls", CMDLs => EntryLs); +dispatcher!("exit", CMDExit => EntryExit); +dispatcher!("clear", CMDClear => EntryClear); // Define data needed for the cd command's execution phase pack!(StateChangeDirectory = String); @@ -91,7 +90,7 @@ pack!(ResultList = Vec<String>); // Parse cd command arguments #[chain] -fn parse_cd_args(prev: ChangeDirectoryEntry) -> Next { +fn parse_cd_args(prev: EntryCd) -> Next { let join = prev.pick(()).unpack(); StateChangeDirectory::new(join) } @@ -115,7 +114,7 @@ fn handle_cd(prev: StateChangeDirectory, current_dir: &mut ResCurrentDir) -> Nex // Get directory contents via the CurrentDir resource #[chain] -fn handle_ls(_prev: ListEntry, current_dir: &ResCurrentDir) -> Next { +fn handle_ls(_prev: EntryLs, current_dir: &ResCurrentDir) -> Next { let dir = ¤t_dir.dir; let entries: Vec<String> = std::fs::read_dir(dir) .into_iter() @@ -145,7 +144,7 @@ fn render_list(list: ResultList) { // Handle exit command event #[chain] fn handle_exit( - _prev: ExitEntry, + _prev: EntryExit, repl: &mut REPL, // Import REPL resource, registered in `exec_repl`, usable directly ) { // Set the REPL exit flag; REPL will exit after this loop iteration @@ -154,7 +153,7 @@ fn handle_exit( // Handle clear command event #[chain] -fn handle_clear(_prev: ClearEntry) { +fn handle_clear(_prev: EntryClear) { // Clear the terminal screen print!("\x1B[2J\x1B[1;1H"); } diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 9a48596..df16cb1 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1024,13 +1024,13 @@ pub mod example_exitcode {} /// use mingling::{Groupped, parser::Picker, setup::GeneralRendererSetup}; /// use serde::Serialize; /// -/// dispatcher!("render", RenderCommand => RenderCommandEntry); +/// dispatcher!("render", CMDRender => EntryRender); /// /// fn main() { /// let mut program = ThisProgram::new(); /// // Add `GeneralRendererSetup` to receive user input `--json` `--yaml` parameters /// program.with_setup(GeneralRendererSetup); -/// program.with_dispatcher(RenderCommand); +/// program.with_dispatcher(CMDRender); /// program.exec(); /// } /// @@ -1056,7 +1056,7 @@ pub mod example_exitcode {} /// // --------- IMPORTANT --------- /// /// #[chain] -/// fn parse_render(prev: RenderCommandEntry) -> Next { +/// fn parse_render(prev: EntryRender) -> Next { /// let (name, age) = Picker::new(prev.inner) /// .pick::<String>(()) /// .pick::<i32>(()) @@ -1215,6 +1215,45 @@ pub mod example_help {} /// gen_program!(); /// ``` pub mod example_hook {} +/// Example Implicit Dispatcher +/// +/// > This example demonstrates how to use the implicit `dispatcher!` definition syntax enabled by `extra_macros` +/// +/// Source code (./Cargo.toml) +/// ```toml +/// [package] +/// name = "example-implicit-dispatcher" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// features = ["extra_macros"] +/// ``` +/// +/// Source code (./src/main.rs) +/// ```ignore +/// use mingling::prelude::*; +/// +/// // When using implicit syntax, the entry and dispatcher names will be automatically derived +/// dispatcher!("remote.add" /*, CMDRemoteAdd => EntryRemoteAdd */); +/// dispatcher!("remote.remove", CMDRemoteRemove => EntryRemoteRemove); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // --------- IMPORTANT --------- +/// program.with_dispatcher(CMDRemoteAdd); +/// // ^^^^^^^^^^^^\_ CMDRemoteAdd is implicitly created +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatcher(CMDRemoteRemove); +/// program.exec_and_exit(); +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_implicit_dispatcher {} /// Example Panic Unwind /// /// > This example introduces how to catch Panic in the Mingling program loop @@ -1322,11 +1361,10 @@ pub mod example_panic_unwind {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{ -/// REPL, /// hook::ProgramHook, /// prelude::*, /// setup::{BasicREPLOutputSetup, BasicREPLPromptSetup, BasicREPLReadlineSetup}, -/// this, +/// this, REPL, /// }; /// use std::{env::current_dir, path::PathBuf}; /// @@ -1351,10 +1389,10 @@ pub mod example_panic_unwind {} /// program.with_resource(ResCurrentDir::default()); /// /// // Dispatchers -/// program.with_dispatcher(ChangeDirectoryCommand); -/// program.with_dispatcher(ListCommand); -/// program.with_dispatcher(ExitCommand); -/// program.with_dispatcher(ClearCommand); +/// program.with_dispatcher(CMDCd); +/// program.with_dispatcher(CMDLs); +/// program.with_dispatcher(CMDExit); +/// program.with_dispatcher(CMDClear); /// /// // Setups /// // Enable basic std::io::stdin().read_line(&mut input) @@ -1392,10 +1430,10 @@ pub mod example_panic_unwind {} /// pack!(ErrorDirectoryNotExist = PathBuf); /// /// // Create commands: cd ls exit -/// dispatcher!("cd", ChangeDirectoryCommand => ChangeDirectoryEntry); -/// dispatcher!("ls", ListCommand => ListEntry); -/// dispatcher!("exit", ExitCommand => ExitEntry); -/// dispatcher!("clear", ClearCommand => ClearEntry); +/// dispatcher!("cd", CMDCd => EntryCd); +/// dispatcher!("ls", CMDLs => EntryLs); +/// dispatcher!("exit", CMDExit => EntryExit); +/// dispatcher!("clear", CMDClear => EntryClear); /// /// // Define data needed for the cd command's execution phase /// pack!(StateChangeDirectory = String); @@ -1405,7 +1443,7 @@ pub mod example_panic_unwind {} /// /// // Parse cd command arguments /// #[chain] -/// fn parse_cd_args(prev: ChangeDirectoryEntry) -> Next { +/// fn parse_cd_args(prev: EntryCd) -> Next { /// let join = prev.pick(()).unpack(); /// StateChangeDirectory::new(join) /// } @@ -1429,7 +1467,7 @@ pub mod example_panic_unwind {} /// /// // Get directory contents via the CurrentDir resource /// #[chain] -/// fn handle_ls(_prev: ListEntry, current_dir: &ResCurrentDir) -> Next { +/// fn handle_ls(_prev: EntryLs, current_dir: &ResCurrentDir) -> Next { /// let dir = ¤t_dir.dir; /// let entries: Vec<String> = std::fs::read_dir(dir) /// .into_iter() @@ -1459,7 +1497,7 @@ pub mod example_panic_unwind {} /// // Handle exit command event /// #[chain] /// fn handle_exit( -/// _prev: ExitEntry, +/// _prev: EntryExit, /// repl: &mut REPL, // Import REPL resource, registered in `exec_repl`, usable directly /// ) { /// // Set the REPL exit flag; REPL will exit after this loop iteration @@ -1468,7 +1506,7 @@ pub mod example_panic_unwind {} /// /// // Handle clear command event /// #[chain] -/// fn handle_clear(_prev: ClearEntry) { +/// fn handle_clear(_prev: EntryClear) { /// // Clear the terminal screen /// print!("\x1B[2J\x1B[1;1H"); /// } diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/dispatcher.rs index e327d6b..725597b 100644 --- a/mingling_macros/src/dispatcher.rs +++ b/mingling_macros/src/dispatcher.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{Ident, Result as SynResult, Token}; +use syn::{Ident, LitStr, Result as SynResult, Token}; #[cfg(feature = "dispatch_tree")] use crate::COMPILE_TIME_DISPATCHERS; @@ -19,6 +19,8 @@ enum DispatcherChainInput { command_struct: Ident, pack: Ident, }, + #[cfg(feature = "extra_macros")] + Auto { command_name: syn::LitStr }, } impl Parse for DispatcherChainInput { @@ -41,8 +43,25 @@ impl Parse for DispatcherChainInput { pack, }) } else 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 = "extra_macros")] + { + return Ok(DispatcherChainInput::Auto { command_name }); + } + #[cfg(not(feature = "extra_macros"))] + { + return Err(syn::Error::new( + command_name.span(), + "expected `, CommandStruct => EntryStruct` after command name", + )); + } + } + // Default format: "command_name", CommandStruct => ChainStruct - let command_name = input.parse()?; input.parse::<Token![,]>()?; let command_struct = input.parse()?; input.parse::<Token![=>]>()?; @@ -70,7 +89,34 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { // Parse the input let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); - // Determine if we're using default or explicit group + #[cfg(not(feature = "extra_macros"))] + let (command_name, command_struct, pack, _use_default, group_path) = match dispatcher_input { + DispatcherChainInput::Explicit { + group_name, + command_name, + command_struct, + pack, + } => ( + command_name, + command_struct, + pack, + false, + quote! { #group_name }, + ), + DispatcherChainInput::Default { + command_name, + command_struct, + pack, + } => ( + command_name, + command_struct, + pack, + true, + crate::default_program_path(), + ), + }; + + #[cfg(feature = "extra_macros")] let (command_name, command_struct, pack, _use_default, group_path) = match dispatcher_input { DispatcherChainInput::Explicit { group_name, @@ -95,6 +141,19 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { true, crate::default_program_path(), ), + DispatcherChainInput::Auto { command_name } => { + let command_name_str = command_name.value(); + let pascal = dotted_to_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, + true, + crate::default_program_path(), + ) + } }; let command_name_str = command_name.value(); @@ -229,3 +288,21 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { pub fn register_dispatcher(_input: TokenStream) -> TokenStream { quote! {}.into() } + +/// Converts a dotted command name (e.g. "remote.add") to PascalCase (e.g. "RemoteAdd"). +/// +/// Each segment is split by `.`, the first character of each segment is uppercased, +/// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax +/// (when `Command => Entry` is omitted) to auto-derive struct names. +#[cfg(feature = "extra_macros")] +fn dotted_to_pascal_case(s: &str) -> String { + s.split('.') + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().to_string() + chars.as_str(), + } + }) + .collect() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 57f37a1..73f2fa5 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -297,6 +297,25 @@ pub fn empty_result(_input: TokenStream) -> TokenStream { /// dispatcher!(MyProgram, "command.path", CommandStruct => EntryStruct); /// ``` /// +/// ## Abbreviated syntax (requires `extra_macros` feature) +/// +/// When the `extra_macros` feature is enabled, the `CommandStruct => EntryStruct` +/// portion can be omitted. The struct names are auto-derived from the command path +/// using PascalCase conversion: +/// +/// ```rust,ignore +/// // Auto-derives: "remote.add" → CMDRemoteAdd ⇒ EntryRemoteAdd +/// dispatcher!("remote.add"); +/// +/// // Auto-derives: "cmd.sub.leaf" → CMDCmdSubLeaf ⇒ EntryCmdSubLeaf +/// dispatcher!("cmd.sub.leaf"); +/// ``` +/// +/// The generated code is equivalent to writing: +/// ```rust,ignore +/// dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +/// ``` +/// /// # Example /// /// ```rust,ignore @@ -310,6 +329,9 @@ pub fn empty_result(_input: TokenStream) -> TokenStream { /// /// // With explicit program: /// dispatcher!(MyApp, "status", StatusCommand => StatusEntry); +/// +/// // Abbreviated form (requires extra_macros): +/// // dispatcher!("remote.add"); /// ``` /// /// The generated `HelloCommand` implements `Dispatcher<ThisProgram>`: |
