1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
<h1 align="center">The Command Macro</h1>
<p align="center">
A macro for quickly building commands
</p>
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<T>` (equivalent to `T: From<EntryGreet>`)
- **Return type**: Any type `R` such that `R: Into<Next>` (`Next` is `ChainProcess<ThisProgram>`, generated by `gen_program!()`)
The `pack!(EntryGreet = Vec<String>)` generated by `dispatcher!("greet")` automatically provides `From<EntryGreet> for Vec<String>`, so the most common pattern is to use `Vec<String>` as the first argument:
```rust
use mingling::prelude::*;
#[command]
fn greet(args: Vec<String>) -> 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<String>, EntryGreet: Into<Vec<String>>
#[chain]
fn __command_handle_greet(args: EntryGreet) -> Next {
greet(args.into()).into()
}
// The original function remains unchanged
fn greet(args: Vec<String>) -> 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<EntryGreet>`, the macro-generated chain performs the conversion via `args.into()` |
| Return value | Any `R: Into<Next>`, the macro-generated chain converts it via `.into()` to `ChainProcess<ThisProgram>` |
| 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<String>, 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<String>, 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<String>) -> 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.
<p align="center" style="font-size: 0.85em; color: gray;">
Written by @Weicao-CatilGrass · Revised for clarity
</p>
|