aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md10
-rw-r--r--Cargo.toml1
-rw-r--r--docs/res/changlog_examples/feat_program_res.rs4
-rw-r--r--examples/example-general-renderer/src/main.rs6
-rw-r--r--examples/example-implicit-dispatcher/Cargo.lock76
-rw-r--r--examples/example-implicit-dispatcher/Cargo.toml8
-rw-r--r--examples/example-implicit-dispatcher/src/main.rs23
-rw-r--r--examples/example-repl-basic/src/main.rs27
-rw-r--r--mingling/src/example_docs.rs72
-rw-r--r--mingling_macros/src/dispatcher.rs83
-rw-r--r--mingling_macros/src/lib.rs22
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
diff --git a/Cargo.toml b/Cargo.toml
index 2548e8f..86b865e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 = &current_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 = &current_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>`: