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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
|
<h1 align="center">Creating your first Program</h1>
<p align="center">
Learn <b>Mingling</b> and use it to create your first command-line program
</p>
## Intro
This chapter will guide you through **Mingling** step by step.
Before we start, let me explain what **Mingling** can do:
Without extra features, it is a sub-command dispatch system based on `proc-macro`: it matches user input, finds & creates the corresponding data, then pushes that data into a dispatcher that continually transforms its type. When the data can no longer be transformed, the program renders the final result to the terminal.
In other words, you need to understand a new dev paradigm: **a fully type-based dispatch system**. This may feel **frustrating** at first, but once you get the hang of it, you'll be able to write CLI apps that are super easy to modify and extend.
## Creating a Basic Program
Next I'll walk you through creating a basic program—I assume you already have an empty Rust project ready!
#### 1. Add Dependencies
Add the following deps to `Cargo.toml` ✏️
```toml
[dependencies]
mingling = "0.1.9"
# If you want the latest, try the version hosted on Github
mingling = { git = "https://github.com/catilgrass/mingling", branch = "main" }
```
> [!NOTE]
>
> This version matches the **Mingling** version used when writing this doc. Check [crates.io](https://crates.io/crates/mingling) for the latest release! 😄
>
> **Mingling** docs are actively updated to keep pace with the latest version.
#### 2. Create the Program
Now, create the program in `src/main.rs` ✏️
```rust
fn main() {
// Create ThisProgram and run it
ThisProgram::new().exec();
}
// The gen_program! macro collects *all preceding* components & types
// then generates the `ThisProgram` struct
mingling::macros::gen_program!();
```
> [!TIP]
>
> When `gen_program!()` expands, it gathers info from other components & types that were expanded before it. This means you must place `gen_program!()` at the very last expansion point in the crate.
>
> I recommend putting it at the end of `main.rs` or `lib.rs`.
#### 3. Create a Command
Of course, the program currently does nothing—it won't output anything at runtime. So let's create our first command `greet` and say hi to someone ✏️
```rust
fn main() {
// ...
}
// Create a dispatcher, binding GreetCommand to the "greet" sub-command
// When the user specifies this command, send GreetEntry to the dispatcher
dispatcher!("greet", GreetCommand => GreetEntry);
// ...
gen_program!();
```
Don't be scared by the sudden macro and two new types! Let me explain what this macro does:
##### About the `dispatcher!` macro 💡
1. It creates a `GreetCommand` struct and implements the `Dispatcher` trait
*This tells the framework: there's a new dispatcher that will handle a sub-command's behavior.*
2. It implements the `Dispatcher` trait's `node(&self) -> Node` function, setting the node to `"greet"`
*This tells the framework: this dispatcher handles the `"greet"` sub-command.*
3. It implements the `Dispatcher` trait's `begin` function, converting the user's full input into the first type `GreetEntry`
*This tells the framework: when this dispatcher is matched, it sends a `GreetEntry` type to the dispatcher for further processing.*
In short: **"When user types `greet`, I create a `GreetEntry` and throw it into the dispatcher for conversion."**
#### 4. Register the Command
After creating the `Dispatcher`, we have two types: `GreetCommand` and `GreetEntry`. First, register `GreetCommand` with `ThisProgram` ✏️
```rust
fn main() {
let mut program = ThisProgram::new();
// Register the dispatcher
program.with_dispatcher(GreetCommand);
program.exec();
}
```
Now `ThisProgram` recognizes the `"greet"` sub-command, but the framework still doesn't know what `"greet"` should do. That's where we implement the actual logic:
#### 5. Implement Rendering Behavior
We want `"greet"` to output `"Hello, World"`: since we're outputting to the screen, we can use another **Mingling** component, `Renderer`, which handles rendering data to the terminal ✏️
```rust
// ...
dispatcher!("greet", GreetCommand => GreetEntry);
// Declare a renderer `render_greet`, specifying the previous type as `GreetEntry`
#[renderer]
fn render_greet(_prev: GreetEntry) {
r_println!("Hello, World!");
}
// ...
gen_program!(); // The renderer will be registered with the program
```
For functions marked with `#[renderer]`, **Mingling** strictly enforces only one function signature:
```rust
#[renderer]
fn renderer_name (_prev: PreviousType) { }
```
The macro reads the type of the first param and tells `gen_program!` that this function renders that type.
##### About `r_println!()` 💡
You might notice that the print macro used inside `#[renderer]` is `r_println!` instead of `println!`. This is because the framework's rendering logic doesn't happen inside that function: after `#[renderer]` expands, it injects a `__renderer_inner_result: &mut RenderResult` into the function; `r_println!` appends the message to the `RenderResult`, and after the dispatcher closes, the final rendered data is handed to `Program::exec` for output.
#### 6. Add Execution Logic
I bet you're already itching to implement something like `greet Alice` to output `"Hello, Alice!"`—and this section is about to do just that!
**Mingling**'s core execution flow is `Dispatcher -> Chain -> Renderer`, and the key part is `Chain`: it converts the input data type into another type, then lets the dispatcher find the next `Chain` or `Renderer` based on the result type ✏️
```rust
dispatcher!("greet", GreetCommand => GreetEntry);
// Wrap the intermediate type `ResultGreetSomeone`
pack!(ResultGreetSomeone = String);
#[chain]
fn handle_greet_entry(prev: GreetEntry) -> Next {
let args = prev.inner;
let name = args
.first()
.cloned()
.unwrap_or_else(|| "World".to_string());
// Wrap into intermediate type
ResultGreetSomeone::new(name)
}
#[renderer]
fn render_greet_someone(prev: ResultGreetSomeone) {
// Deref prev to get the raw type
r_println!("Hello, {}!", *prev);
}
```
Just like `#[renderer]`, we created a `#[chain]` that processes type `GreetEntry` and outputs `ResultGreetSomeone`.
This inserts a `Chain` between the original `Dispatcher` and `Renderer`: it extracts the user's input params (or falls back to "World"), then passes them to the renderer to print to the terminal.
##### About `Next` 💡
`Next` is a placeholder generated by `gen_program!()`. After `#[chain]` expands, it's replaced by a type-erased type `ChainProcess<ThisProgram>` that the dispatcher can recognize, helping reduce boilerplate code.
> [!NOTE]
>
> `Next` is a temporary solution; the next update will wait until Rust's `Impl In Type Aliases` feature is stable.
>
> **But don't worry**: the next `Next` update won't introduce **breaking changes!**
##### About `pack!` 💡
`pack!` is an **extremely** frequently used macro in **Mingling** development: it wraps any type into another type and auto-derives the traits the framework needs.
Its syntax is as simple as you see:
```rust
pack!(PackedType = RawType);
```
Note: `pack!` doesn't support types with lifetimes, because types are always moved (not borrowed) between dispatchers.
#### 7. Compile & Run
Alright, we've completed a basic CLI app. Here's the full code—you can paste it and run it directly:
```rust
use mingling::macros::{chain, dispatcher, gen_program, pack, r_println, renderer};
fn main() {
let mut program = ThisProgram::new();
program.with_dispatcher(GreetCommand);
program.exec();
}
dispatcher!("greet", GreetCommand => GreetEntry);
pack!(ResultGreetSomeone = String);
#[chain]
fn handle_greet_entry(prev: GreetEntry) -> Next {
let args = prev.inner;
let name = args.first().cloned().unwrap_or_else(|| "World".to_string());
ResultGreetSomeone::new(name)
}
#[renderer]
fn render_greet_someone(prev: ResultGreetSomeone) {
r_println!("Hello, {}!", *prev);
}
gen_program!();
```
Output:
```bash
~> your-bin greet
Hello, World!
~> your-bin greet Alice
Hello, Alice!
```
At this point, you have successfully created a basic **Mingling** command-line program. The next chapter will explain how to implement a fallback mechanism for your command-line program to handle cases where a command or renderer does not exist.
<p align="center" style="font-size: 0.85em; color: gray;">
Written by @Weicao-CatilGrass
</p>
|