The Command Macro
A macro for quickly building commands
Mingling's macro syntax has been largely stabilized since 0.3.0, and significant changes are not expected going forward — now is the perfect time to introduce some syntactic sugar.
## The Problem
For a long time, creating a command in Mingling required the following steps:
**Step 1: Declare the entry point with `dispatcher!`**
```rust
// Full form: declare dispatcher and entry types
dispatcher!("greet", CMDGreet => EntryGreet);
```
Or use the implicit syntax provided by `extra_macros` (automatically deriving `CMDGreet` and `EntryGreet`):
```rust
dispatcher!("greet");
```
**Step 2: Register the dispatcher in `main`**
```rust
fn main() {
let mut program = ThisProgram::new();
program.with_dispatcher(CMDGreet);
program.exec_and_exit();
}
```
**Step 3: Write the handling logic with `#[chain]`**
```rust
#[chain]
fn handle_greet(args: EntryGreet) -> Next {
// ...
}
```
Three steps, three concepts (`dispatcher!`, `CMD*`, `#[chain]`), repeated for every new command. It made me wonder: do we really need to write this much boilerplate so often?
## The Design: `#[command]`
To provide a more concise approach, I plan to add the `#[command]` attribute macro, merging `dispatcher!` and `#[chain]` into a single step.
### Basic Usage
`#[command]` is applied to a function whose signature must satisfy two constraints:
- **First argument**: Any type `T` such that `EntryGreet: Into` (equivalent to `T: From`)
- **Return type**: Any type `R` such that `R: Into` (`Next` is `ChainProcess`, generated by `gen_program!()`)
The `pack!(EntryGreet = Vec)` generated by `dispatcher!("greet")` automatically provides `From for Vec`, so the most common pattern is to use `Vec` as the first argument:
```rust
use mingling::prelude::*;
#[command]
fn greet(args: Vec) -> Next {
let name = args.first().cloned().unwrap_or_else(|| "World".into());
ResultName::new(name).into()
}
```
This expands to:
```rust
// Automatically generates the dispatcher
dispatcher!("greet");
// Automatically generates the chain handler, bridging EntryGreet → T
// Here T = Vec, EntryGreet: Into>
#[chain]
fn __command_handle_greet(args: EntryGreet) -> Next {
greet(args.into()).into()
}
// The original function remains unchanged
fn greet(args: Vec) -> Next {
let name = args.first().cloned().unwrap_or_else(|| "World".into());
ResultName::new(name).into()
}
```
| Mapping | Rule |
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
| Command name | The function name is used directly as the command name, e.g. `fn greet(...)` → `"greet"` |
| First argument | Any `T: From`, the macro-generated chain performs the conversion via `args.into()` |
| Return value | Any `R: Into`, the macro-generated chain converts it via `.into()` to `ChainProcess` |
| Dispatcher registration | You must manually call `program.with_dispatcher(CMDGreet)` in `main` |
### Resource Injection
`#[command]` also supports resource injection, following the same rules as `#[chain]` — from the second argument onward, use `&T` or `&mut T` to declare resource references:
```rust
#[command]
fn greet(args: Vec, ec: &mut ResExitCode) -> Next {
ec.exit_code = 1;
let name = args.first().cloned().unwrap_or_else(|| "World".into());
ResultName::new(name).into()
}
```
This expands to:
```rust
dispatcher!("greet");
#[chain]
fn __command_handle_greet(args: EntryGreet, ec: &mut ResExitCode) -> Next {
greet(args.into(), ec).into()
}
fn greet(args: Vec, ec: &mut ResExitCode) -> Next {
ec.exit_code = 1;
let name = args.first().cloned().unwrap_or_else(|| "World".into());
ResultName::new(name).into()
}
```
### Implicit vs Explicit
`#[command]` is syntactic sugar over `dispatcher!("name")` + `#[chain]`. It makes the generation of the `dispatcher` and `chain` more implicit, but dramatically reduces boilerplate:
```rust
// Before
dispatcher!("greet", CMDGreet => EntryGreet);
#[chain]
fn handle_greet(args: EntryGreet) -> Next {
// ...
}
// After
#[command]
fn greet(args: Vec) -> Next {
// ...
}
```
Whenever you need fine-grained control over the behavior of the Dispatcher or Chain (e.g., customizing the `node` path, adding extra derive attributes to the Entry type), you can always fall back to the original explicit form. The two approaches do not conflict.
## On Naming
In Mingling, there is no concept called `Command` — all behavior is the result of dispatchers producing Entry types that are routed through the Program pipeline. The combination of `Dispatcher` + `Chain` is the standard way to build a Command, so using `#[command]` to generate `dispatcher!` and `#[chain]` makes semantic sense.
## Enabling
This macro requires the `extra_macros` feature. Enable it in your `Cargo.toml`:
```toml
[dependencies]
mingling = { version = "...", features = ["extra_macros"] }
```
## Caveats
- The `CMDGreet` generated by `#[command]` is a regular struct and still needs to be manually registered with the Program via `program.with_dispatcher(CMDGreet)`.
- If the function name contains non-alphabetic characters (such as underscores), the command name will be used as-is. For example, `fn add_remote(...)` corresponds to the command name `"add_remote"`.
- Nested commands (e.g., `"remote.add"`) cannot be expressed directly with `#[command]`; use the traditional `dispatcher!("remote.add")` form instead.
Written by @Weicao-CatilGrass · Revised for clarity