From 13408e79b940e9a33ca593ed30d1b20c54e01234 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 30 Jun 2026 18:05:05 +0800 Subject: feat(docs): add Chinese and English documentation for Mingling tutorials Add comprehensive documentation covering Declare a Dispatcher, Declare a Chain, Rendering Results, Multi-Command Program, Argument Parsing with Picker and Clap, Program Setup, Error Handling, Help Info, Resource System, Exit Code Control, Hook System, Testing, Completion, Structural Rendering, and Core Concepts --- docs/_zh_CN/_sidebar.md | 21 ++ docs/_zh_CN/pages/10-help.md | 69 ++++ docs/_zh_CN/pages/11-resource-system.md | 89 +++++ docs/_zh_CN/pages/12-exit-code.md | 70 ++++ docs/_zh_CN/pages/13-hook.md | 107 ++++++ docs/_zh_CN/pages/14-testing.md | 129 +++++++ docs/_zh_CN/pages/2-define-a-dispatcher.md | 103 ++++++ docs/_zh_CN/pages/3-define-a-chain.md | 131 +++++++ docs/_zh_CN/pages/4-render-result.md | 142 ++++++++ docs/_zh_CN/pages/5-multiple-commands.md | 109 ++++++ docs/_zh_CN/pages/6-argument-parse-picker.md | 393 +++++++++++++++++++++ docs/_zh_CN/pages/7-argument-parse-clap.md | 87 +++++ docs/_zh_CN/pages/8-setup-and-resources.md | 89 +++++ docs/_zh_CN/pages/9-error-handling.md | 120 +++++++ docs/_zh_CN/pages/advanced/.name | 1 + docs/_zh_CN/pages/advanced/1-completion.md | 83 +++++ .../_zh_CN/pages/advanced/2-structural-renderer.md | 120 +++++++ docs/_zh_CN/pages/concepts/.name | 1 + docs/_zh_CN/pages/concepts/1-the-pipeline.md | 129 +++++++ docs/_zh_CN/pages/concepts/2-resource.md | 60 ++++ docs/_zh_CN/pages/concepts/3-any-output.md | 73 ++++ docs/_zh_CN/pages/concepts/4-program-collect.md | 52 +++ 22 files changed, 2178 insertions(+) create mode 100644 docs/_zh_CN/pages/10-help.md create mode 100644 docs/_zh_CN/pages/11-resource-system.md create mode 100644 docs/_zh_CN/pages/12-exit-code.md create mode 100644 docs/_zh_CN/pages/13-hook.md create mode 100644 docs/_zh_CN/pages/14-testing.md create mode 100644 docs/_zh_CN/pages/2-define-a-dispatcher.md create mode 100644 docs/_zh_CN/pages/3-define-a-chain.md create mode 100644 docs/_zh_CN/pages/4-render-result.md create mode 100644 docs/_zh_CN/pages/5-multiple-commands.md create mode 100644 docs/_zh_CN/pages/6-argument-parse-picker.md create mode 100644 docs/_zh_CN/pages/7-argument-parse-clap.md create mode 100644 docs/_zh_CN/pages/8-setup-and-resources.md create mode 100644 docs/_zh_CN/pages/9-error-handling.md create mode 100644 docs/_zh_CN/pages/advanced/.name create mode 100644 docs/_zh_CN/pages/advanced/1-completion.md create mode 100644 docs/_zh_CN/pages/advanced/2-structural-renderer.md create mode 100644 docs/_zh_CN/pages/concepts/.name create mode 100644 docs/_zh_CN/pages/concepts/1-the-pipeline.md create mode 100644 docs/_zh_CN/pages/concepts/2-resource.md create mode 100644 docs/_zh_CN/pages/concepts/3-any-output.md create mode 100644 docs/_zh_CN/pages/concepts/4-program-collect.md (limited to 'docs/_zh_CN') diff --git a/docs/_zh_CN/_sidebar.md b/docs/_zh_CN/_sidebar.md index 620b11c..80645c0 100644 --- a/docs/_zh_CN/_sidebar.md +++ b/docs/_zh_CN/_sidebar.md @@ -1,5 +1,26 @@ - [Welcome!](README) * [起步](pages/1-getting-started) +* [声明一个分发器](pages/2-define-a-dispatcher) +* [声明一个链](pages/3-define-a-chain) +* [将结果渲染](pages/4-render-result) +* [多命令程序](pages/5-multiple-commands) +* [使用 Picker 完成参数解析](pages/6-argument-parse-picker) +* [使用 Clap 完成参数解析](pages/7-argument-parse-clap) +* [程序装配](pages/8-setup-and-resources) +* [错误处理](pages/9-error-handling) +* [帮助信息](pages/10-help) +* [使用资源系统](pages/11-resource-system) +* [退出码控制](pages/12-exit-code) +* [钩子系统](pages/13-hook) +* [测试你的程序](pages/14-testing) * 其他 * [特性](pages/other/features) * [命名规范](pages/other/naming_rule) +* 核心概念 + * [基础管线](pages/concepts/1-the-pipeline) + * [资源系统](pages/concepts/2-resource) + * [任意输出机制](pages/concepts/3-any-output) + * [关于 ProgramCollect](pages/concepts/4-program-collect) +* 进阶 + * [补全](pages/advanced/1-completion) + * [结构化渲染](pages/advanced/2-structural-renderer) diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md new file mode 100644 index 0000000..7b56557 --- /dev/null +++ b/docs/_zh_CN/pages/10-help.md @@ -0,0 +1,69 @@ +
+ 为命令添加 --help 支持 +
+ +没有帮助信息的 CLI 不是好 CLI。 + +Mingling 里用 `#[help]` 宏给命令添加帮助文本。 + +## 最简单的帮助 + +直接给 Entry 写一个帮助函数: + +```rust +@@@use mingling::macros::help; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +#[help] +fn help_greet(_entry: EntryGreet) { + r_println!("Usage: greet [name]"); + r_println!("Say hello to someone."); +} +``` + +> [!NOTE] +> 帮助函数里也用 `r_println!`,因为 `#[help]` 走的也是渲染流程 —— 它是被 `--help` 参数提前触发的短路渲染,不是独立于管线之外的逻辑。 + +## 全局帮助 + +你也可以为 `ErrorDispatcherNotFound` 写帮助,作为"根帮助": + +```rust +@@@use mingling::macros::help; +// 用户直接输入 --help 时触发 +#[help] +fn help_root(entry: ErrorDispatcherNotFound) { + r_println!("Usage: my-cli+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md new file mode 100644 index 0000000..5c3cf90 --- /dev/null +++ b/docs/_zh_CN/pages/11-resource-system.md @@ -0,0 +1,89 @@ ++ 手把手带你使用资源 +
+ +资源是 Mingling 中管理全局状态的机制。任何实现了 `Default + Clone` 的类型都可以成为资源。 + +## 定义一个资源 + +```rust +// 只要实现 Default + Clone,就可以作为资源使用 +#[derive(Default, Clone)] +struct ResCurrentDir(String); + +// 注册到 Program +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResCurrentDir(".".into())); + program.exec_and_exit(); +} +``` + +因为 `ResCurrentDir` 同时实现了 `Default` 和 `Clone`,框架会自动为它实现 `ResourceMarker` trait,无需手动 impl。 + +## 注入并使用 + +在 Chain 或 Renderer 中,只需在参数列表里声明你要的资源: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResCurrentDir(String); +@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir); +@@@pack!(ResultPath = String); +// 通过 &T 注入只读资源 +#[chain] +fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next { + ResultPath::new(cwd.0.clone()).to_render() +} + +#[renderer] +fn render_path(result: ResultPath) { + r_println!("{}", *result); +} +``` + +## 修改资源 + +用 `&mut T` 注入可修改资源: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResVisitCount(u32); +@@@dispatcher!("visit", CMDVisit => EntryVisit); +@@@pack!(ResultDone = ()); +#[chain] +fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { + counter.0 += 1; + ResultDone::default() +} + +#[renderer] +fn render_done(_done: ResultDone, counter: &ResVisitCount) { + r_println!("visit count is : {}", counter.0); +} +``` + +## 多个资源同用 + +Chain 可以同时注入任意多个资源,框架按类型自动匹配: + +```rust +@@@#[derive(Default, Clone)] struct ResConfig(String); +@@@#[derive(Default, Clone)] struct ResCounter(u32); +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); +// 同时注入只读 + 可修改 +#[chain] +fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next { + println!("config: {}", config.0); + counter.0 += 1; + ResultDone::default().to_render() +} +``` + +如果要深入了解 `ResourceMarker`、`LazyRes` 惰性加载等进阶内容,可以查看 [核心概念:资源系统](pages/concepts/2-resource)。 + ++ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/12-exit-code.md b/docs/_zh_CN/pages/12-exit-code.md new file mode 100644 index 0000000..7c55b60 --- /dev/null +++ b/docs/_zh_CN/pages/12-exit-code.md @@ -0,0 +1,70 @@ ++ 如何使用资源系统管理程序退出码 +
+ +程序退出时给 shell 一个正确的退出码是 CLI 的基本素养 + +。Mingling 提供了开箱即用的 `ExitCodeSetup`,配合 `ResExitCode` 资源,让退出码控制变得极其简单。 + +## 启用 ExitCodeSetup + +```rust +@@@use mingling::prelude::*; +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); +@@@ program.exec_and_exit(); +} +``` + +`ExitCodeSetup` 做了两件事: + +1. 注册 `ResExitCode` 资源(默认值为 `0`) +2. 注册一个 `finish` hook,在程序退出前读取 `ResExitCode` 的值作为最终退出码 + +## 修改退出码 + +在 Chain 或 Renderer 中通过 `ResExitCode` 注入来修改退出码: + +```rust +@@@use mingling::res::ResExitCode; +@@@use mingling::setup::ExitCodeSetup; +@@@pack!(EntryCheck = Vec+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/13-hook.md b/docs/_zh_CN/pages/13-hook.md new file mode 100644 index 0000000..543ee55 --- /dev/null +++ b/docs/_zh_CN/pages/13-hook.md @@ -0,0 +1,107 @@ ++ 如何使用 ProgramHook 向程序内部插入行为 +
+ +Hook 让你在管线的各个生命周期节点插入自定义逻辑 —— 在 dispatch 之前、chain 之后、render 前后、程序退出时 …… + +你可以把横切关注点(日志、鉴权、指标收集)写在 hook 里,而不是散落在各处的业务代码中。 + +## 基本用法 + +`ProgramHook` 采用 builder 模式构造: + +```rust +@@@use mingling::hook::ProgramHook; +fn main() { + let mut program = ThisProgram::new(); + program.with_hook( + ProgramHook::empty() + .on_pre_chain(|info| { + println!("before chain: {}", info.input); + }) + .on_post_render(|info| { + println!("after render: {}", info.result); + }), + ); + program.exec_and_exit(); +} +``` + +> [!TIP] +> `ProgramHook::empty()` 创建一个空 hook,然后链式调用 `.on_*()` 方法注册你关心的生命周期节点。没有注册的节点不会执行。 + +## 生命周期节点 + +Hook 覆盖了管线的完整生命周期: + +| 阶段 | Hook | 触发时机 | +| ------------ | ------------------ | ------------- | +| **Dispatch** | `on_begin` | 执行开始 | +| | `on_pre_dispatch` | Dispatch 之前 | +| | `on_post_dispatch` | Dispatch 之后 | +| **Chain** | `on_pre_chain` | Chain 执行前 | +| | `on_post_chain` | Chain 执行后 | +| **Render** | `on_pre_render` | Render 执行前 | +| | `on_post_render` | Render 执行后 | +| **Finish** | `on_finish` | 程序退出前 | + +每个 hook 回调接收对应的 `Hook*Info` 结构体,里面包含当前上下文的信息(输入类型、参数、渲染结果等)。 + +## 实际例子:记录操作日志 + +```rust +@@@use mingling::prelude::*; +@@@use mingling::hook::ProgramHook; +@@@ +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@ +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { +@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render() +@@@} +@@@#[renderer] fn render_name(r: ResultName) { r_println!("Hello, {}!", *r); } +fn main() { + let mut program = ThisProgram::new(); + + // 记录每次 chain 执行前后的信息 + program.with_hook( + ProgramHook::empty() + .on_pre_chain(|info| { + eprintln!("[hook] executing chain for: {}", info.input); + }) + .on_post_chain(|info| { + eprintln!("[hook] chain output: {}", info.output.member_id); + }), + ); + + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +运行效果: + +```text +[hook] executing chain for: EntryGreet +[hook] chain output: ResultName +Hello, World! +``` + +## 通过 Hook 控制行为 + +Hook 不仅用来 "看",还可以用 `ProgramControlUnit` 改变程序行为: + +| 变体 | 效果 | +| -------------------------- | ----------------------------------------- | +| `Continue` | 什么都不做,继续执行 | +| `OverrideExitCode(i32)` | 覆盖退出码 | +| `RouteToChain(AnyOutput)` | 将当前数据替换为新的,重新进入 Chain 循环 | +| `RouteToRender(AnyOutput)` | 跳过后续 Chain,直接渲染 | + +> [!NOTE] +> Hook 可以注册多个,按注册顺序执行。 + ++ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md new file mode 100644 index 0000000..af9d077 --- /dev/null +++ b/docs/_zh_CN/pages/14-testing.md @@ -0,0 +1,129 @@ ++ 为 Chain 和 Renderer 编写单元测试 +
+ +管线模型附带的一个好处就是 **可测试性**。 + +Chain 只是一个接收输入、返回输出的函数,Renderer 也只是接收输入、写入内容的函数 —— 没有全局状态的黑魔法,测试起来很直接。 + +## 测试 Renderer + +Renderer 是最容易测试的——调用函数,断言返回结果: + +```rust +@@@pack!(ResultName = String); +// 返回 String 而不是 () +#[renderer] +fn render_name(name: ResultName) -> String { + r_println!("Hello, {}!", *name); +} + +#[test] +fn test_render_name() { + let result = render_name(ResultName::new("Alice".to_string())); + assert_eq!(result, "Hello, Alice!\n"); +} +``` + +注意到 Renderer 的返回值改成了 `-> String`——`#[renderer]` 会把 `RenderResult` 自动转换成你指定的返回类型(默认是 `()`)。返回 `String` 后你就可以直接断言输出内容了。 + +## 测试 Chain + +测试 Chain 稍微复杂一点,因为它的返回值是 `Next`(实际是 `impl Into+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/2-define-a-dispatcher.md b/docs/_zh_CN/pages/2-define-a-dispatcher.md new file mode 100644 index 0000000..0afd911 --- /dev/null +++ b/docs/_zh_CN/pages/2-define-a-dispatcher.md @@ -0,0 +1,103 @@ ++ 使用 dispatcher! 宏声明命令,并注册 +
+ +Mingling 的管线从 Dispatcher 开始。 + +它的工作很简单:**匹配用户输入的命令,把参数包装成一个 Entry 类型**。 + +## `dispatcher!` 宏 + +`dispatcher!` 宏会同时生成两个类型: + +| 生成物 | 用途 | +| ----------- | ----------------------------------------------- | +| `CMDType` | 分发器本身,需要注册到 Program | +| `EntryType` | 入口类型,包裹 `Vec+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md new file mode 100644 index 0000000..7ac5c60 --- /dev/null +++ b/docs/_zh_CN/pages/3-define-a-chain.md @@ -0,0 +1,131 @@ ++ 使用 chain 宏声明链,并承接 Entry 输入 +
+ +上一节我们声明了 `dispatcher!("greet", CMDGreet => EntryGreet)` + +现在用户输入 `greet` 时会被匹配并包装成 `EntryGreet`。 + +但拿到 Entry 之后呢? + +我们需要一个 Chain 来处理它。 + +## `#[chain]` 宏 + +`#[chain]` 用来标记一个处理函数,格式非常直接: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // args 就是用户输入经过匹配后剩下的参数 + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + // 把结果包装成 Next,告诉调度器下一步去哪 + ResultName::new(name) +} +``` + +注意到了吗? + +Chain 函数签名里写着它需要什么——`args: EntryGreet` + +然后用 `ResultName::new(name)` 返回一个新类型。 + +这个返回的 `Next` 会展开成 `impl Into+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md new file mode 100644 index 0000000..9fac8a4 --- /dev/null +++ b/docs/_zh_CN/pages/4-render-result.md @@ -0,0 +1,142 @@ ++ 使用 renderer 宏声明渲染器,将结果输出 +
+ +现在,我们创建了 Dispatcher 和 Chain,也通过 `pack!` 产出了一个 Result 类型。最后一步:**把结果展示给用户**。 + +## `#[renderer]` 宏 + +跟 `#[chain]` 类似,`#[renderer]` 用于标记一个输出函数: + +```rust +@@@pack!(ResultName = String); +#[renderer] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} +``` + +Renderer 接收 Chain 产出的结果,然后用 `r_println!` 输出。这个 `r_println!` 跟我们平常用的 `println!` 有什么区别? + +## `r_println!` 和 `r_print!` 宏 + +`r_println!` 和 `r_print!` 是 Mingling 提供的打印宏,它们把内容写入 `RenderResult` 而不是直接输出到终端。这样做的好处是: + +1. **RenderResult 持有退出码**——你可以设置程序以特定退出码结束 +2. **方便测试**——可以捕获渲染结果做断言 +3. **便于后处理**——你可以将结果捕获,统一进行文本后处理 + +> [!TIP] +> 如果只是简单打印,你可以先把它理解为 `println!` 的平替。用 `r_println!` 替换 `println!` 不会错。 + +## 完整的可运行程序 + +把三篇教程的内容合在一起,你的第一个 Mingling 程序就完整了: + +```rust +// 1. 用 Dispatcher 声明命令 +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. 用 pack! 声明结果数据 +pack!(ResultName = String); + +// 3. 用 Chain 处理逻辑 +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +// 4. 用 Renderer 输出结果 +#[renderer] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} + +// 5. 在 main 函数内装配程序并运行 +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +// 6. 使用 gen_program! 生成完整程序 +gen_program!(); +``` + +## 跑起来试试 + +```bash +~# cargo run -- greet Alice +``` + +```text +Hello, Alice! +``` + +试试不给参数: + +```bash +~# cargo run -- greet +``` + +```text +Hello, World! +``` + +试试不存在的命令: + +```bash +cargo run -- great +``` + +```text +# 什么也没输出! +``` + +## 补上 Fallback + +`gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md new file mode 100644 index 0000000..4a9c72d --- /dev/null +++ b/docs/_zh_CN/pages/5-multiple-commands.md @@ -0,0 +1,109 @@ ++ 在一个程序里添加多个命令 +
+ +真实世界的 CLI 很少只有一个命令。这篇我们来扩展之前的 greet 程序,加上第二个命令,看看多命令的程序长什么样。 + +## 添加第二个命令 + +继续在同一个项目里操作: + +```rust +// 声明两个命令 +dispatcher!("greet", CMDGreet => EntryGreet); +dispatcher!("add", CMDAdd => EntryAdd); + +pack!(ResultGreeting = String); +pack!(ResultSum = i32); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(name) +} + +#[chain] +fn handle_add(args: EntryAdd) -> Next { + let sum: i32 = args.inner.iter().filter_map(|s| s.parse::+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md new file mode 100644 index 0000000..9c7e028 --- /dev/null +++ b/docs/_zh_CN/pages/6-argument-parse-picker.md @@ -0,0 +1,393 @@ ++ 用 Picker 完成基本的参数解析 +
+ +前面教程中我们都是手动从 `EntryGreet.inner`(`Vec+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/7-argument-parse-clap.md b/docs/_zh_CN/pages/7-argument-parse-clap.md new file mode 100644 index 0000000..e15bebb --- /dev/null +++ b/docs/_zh_CN/pages/7-argument-parse-clap.md @@ -0,0 +1,87 @@ ++ 用 clap 做更复杂的参数解析 +
+ +Picker 适合轻量级的参数提取,但当参数数量多、有复杂的校验规则、或者需要自动生成 `--help` 时,可以接入 [clap](https://crates.io/crates/clap)。 + +## 开启 clap 特性 + +```toml +[dependencies.mingling] +features = ["clap"] + +[dependencies.clap] +version = "4" +features = ["derive", "color"] +``` + +## dispatcher_clap + +`#[dispatcher_clap]` 加在 `clap::Parser` 结构体上,自动生成 Dispatcher: + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" +@@@ use mingling::macros::dispatcher_clap; +#[derive(Default, clap::Parser, Groupped)] +#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] +pub struct EntryGreet { + #[clap(default_value = "World")] + name: String, + #[arg(short, long, default_value_t = 1)] + repeat: i32, +} + +#[renderer] +fn render_greet(greet: EntryGreet) { + let count = greet.repeat.max(0) as usize; + r_print!("Hello, "); + for _ in 0..count { + r_print!("{} ", greet.name); + } + r_println!("!"); +} + +#[renderer] +fn render_greet_parse_failed(err: ErrorGreetParsed) { + r_println!("{}", *err); +} +``` + +## 与 BasicProgramSetup 配合 + +如果需要 `--help` 支持,在 main 中注册 `BasicProgramSetup` 并设置 clap 帮助的输出模式: + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" +@@@use mingling::setup::BasicProgramSetup; +@@@use mingling::macros::dispatcher_clap; +@@@#[derive(Default, clap::Parser, Groupped)] +@@@#[dispatcher_clap("greet", CMDGreet)] +@@@pub struct EntryGreet { +@@@ name: String, +@@@} +@@@#[renderer] +@@@fn render_greet(greet: EntryGreet) { +@@@ r_println!("Hello, {}!", greet.name); +@@@} +@@@ +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.stdout_setting.clap_help_print_behaviour = + mingling::ClapHelpPrintBehaviour::WriteToRenderResult; + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +详见 [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding)。 + ++ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/8-setup-and-resources.md b/docs/_zh_CN/pages/8-setup-and-resources.md new file mode 100644 index 0000000..d38e69d --- /dev/null +++ b/docs/_zh_CN/pages/8-setup-and-resources.md @@ -0,0 +1,89 @@ ++ 用 Setup 初始化你的程序 +
+ +当程序启动时需要做一些初始化工作——比如解析全局参数、注册资源——你可以用 `#[program_setup]` 来组织这些逻辑。 + +## 用 Setup 做初始化 + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md new file mode 100644 index 0000000..a4d5b0a --- /dev/null +++ b/docs/_zh_CN/pages/9-error-handling.md @@ -0,0 +1,120 @@ ++ 将错误优雅地展示给用户 +
+ +管线里不只有成功路径。当输入有误、资源找不到、操作失败时,你需要一个地方来处理这些"意外",而不是让程序 panic。 + +## 两个路径:成功 vs 错误 + +回顾管线模型,Chain 的返回值是 `Next`,它有两个去向: + +| 路由 | 含义 | +| -------------- | ---------------------------- | +| `.to_render()` | 出结果了,交给 Renderer 展示 | +| `.to_chain()` | 还没处理完,交给下一个 Chain | + +错误类型的值也可以走任意一条路——你可以选择直接渲染错误信息,也可以交给下一个 Chain 尝试恢复。 + +## 用独立类型区分错误 + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); +pack!(ErrorNameEmpty = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + + if name.is_empty() { + ErrorNameEmpty::new("name is required".to_string()).to_render() + } else { + ResultGreeting::new(name).to_render() + } +} +``` + +然后各自写 Renderer: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultGreeting = String); +@@@pack!(ErrorNameEmpty = String); +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting::new(args.inner.first().cloned().unwrap_or_default()).to_render() } + +#[renderer] +fn render_greeting(result: ResultGreeting) { + r_println!("Hello, {}!", *result); +} + +#[renderer] +fn render_error_name_empty(err: ErrorNameEmpty) { + r_println!("Error: {}", *err); +} +``` + +两个 Renderer 各司其职,用户看到什么取决于 Chain 返回了什么。 + +## 完整的例子 + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +pack!(ResultGreeting = String); +pack!(ErrorNameEmpty = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + if name.is_empty() { + ErrorNameEmpty::new("name is required".to_string()).to_render() + } else { + ResultGreeting::new(name).to_render() + } +} + +#[renderer] +fn render_greeting(result: ResultGreeting) { + r_println!("Hello, {}!", *result); +} + +#[renderer] +fn render_error_name_empty(err: ErrorNameEmpty) { + r_println!("Error: {}", *err); +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +运行效果: + +```text +~# my-cli greet Alice +Hello, Alice! + +~# my-cli greet +Error: name is required +``` + +## 关于 `pack_err!` + +如果你启用了 `extra_macros`,还可以用 `pack_err!` 快速声明带有自动 `name` 字段的错误类型: + +```rust +// Features: ["extra_macros"] +pack_err!(ErrorNotFound); +// 生成: struct ErrorNotFound { pub name: String } +``` + +详见 [特性列表](pages/other/features)。 + ++ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/advanced/.name b/docs/_zh_CN/pages/advanced/.name new file mode 100644 index 0000000..eb92a2d --- /dev/null +++ b/docs/_zh_CN/pages/advanced/.name @@ -0,0 +1 @@ +进阶 diff --git a/docs/_zh_CN/pages/advanced/1-completion.md b/docs/_zh_CN/pages/advanced/1-completion.md new file mode 100644 index 0000000..3941404 --- /dev/null +++ b/docs/_zh_CN/pages/advanced/1-completion.md @@ -0,0 +1,83 @@ ++ 使用 comp 特性实现完全动态的补全系统 +
+ +Mingling 的补全是**完全动态**的——没有静态的补全文件,而是在运行时根据用户当前输入实时计算补全建议。 + +## 开启 comp + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["comp"] + +[build-dependencies.mingling] +features = [ + "comp", + # 启用 `builds` 特性以提供构建期支持 + "builds" +] +``` + +## 工作原理 + +当用户按下 `TAB` 时,补全脚本会调用程序的隐藏子命令 `__comp`,它会根据输入的 `ShellContext` 动态地查询最合适的建议。 + +这个隐藏子命令由 `gen_program!()` 在启用 `comp` 特性时自动生成,对应的分发器是 `CMDCompletion`,你需要使用 `with_dispatcher` 添加到程序中。 + +补全流程: + +1. 二次匹配用户当前输入的 `Dispatcher` +2. 调用对应的 `#[completion]` 函数 +3. 函数返回 `Suggest`(文件补全或建议列表) +4. 通知 Shell 将建议呈现出来 + +## 定义补全 + +用 `#[completion(EntryType)]` 为 Entry 定义补全逻辑: + +```rust +// Features: ["comp"] +@@@use mingling::prelude::*; +@@@use mingling::{ShellContext, Suggest, SuggestItem}; +@@@use std::collections::BTreeSet; +@@@dispatcher!("greet", CMDGreet => EntryGreet); + +#[completion(EntryGreet)] +fn complete_greet(ctx: &ShellContext) -> Suggest { + if ctx.previous_word == "greet" { + let mut items = BTreeSet::new(); + items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into())); + items.insert(SuggestItem::new("World".into())); + Suggest::Suggest(items) + } else { + Suggest::FileCompletion + } +} +``` + +`suggest!` 宏是更简洁的写法,效果相同: + +```rust +// Features: ["comp"] +@@@use mingling::macros::suggest; +@@@fn example() { +suggest! { + "Alice": "Likes to receive messages", + "World" +}; +@@@} +``` + +`ShellContext` 包含用户当前的输入状态(`previous_word`、`current_word`、`all_words` 等),`Suggest` 有两种变体:`Suggest::Suggest(list)` 返回建议列表,`Suggest::FileCompletion` 交给 shell 自己做文件补全。 + +## 生成补全脚本 + +在 `build.rs` 中调用 `build_comp_scripts` 生成补全脚本(需要 `builds` + `comp` 特性)。 + +详见 [example-completion](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-completion)。 + ++ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md new file mode 100644 index 0000000..14164da --- /dev/null +++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md @@ -0,0 +1,120 @@ ++ 使用 structural_renderer 特性将结果渲染为序列化文本 +
+ +启用 `structural_renderer` 后,你的程序可以通过 `--json`、`--yaml` 等参数将输出切换为结构化格式,方便与其他工具集成。 + +## 开启特性 + +```toml +[dependencies.mingling] +features = ["structural_renderer"] +``` + +`structural_renderer` 会自动启用 `json_serde_fmt`。 + +如果需要更多格式,可以启用 `structural_renderer_full`(包含 JSON、YAML、TOML、RON)。 + +> [!NOTE] +> 若需要定制输出类型,可以查看 [特性](./pages/other/features) + +## 基本用法 + +启用 `StructuralRendererSetup` 后,用 `pack_structural!` 替代 `pack!` 来声明支持结构化输出的类型: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::setup::StructuralRendererSetup; +@@@dispatcher!("render", CMDRender => EntryRender); + +// pack_structural! 等价于 pack! + StructuralData +pack_structural!(ResultInfo = (String, i32)); + +#[chain] +fn handle_render(args: EntryRender) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + ResultInfo::new((name, age)) +} + +#[renderer] +fn render_info(r: ResultInfo) { + r_println!("{:?}", *r); +} +``` + +运行效果: + +```text +~# my-cli render Bob 22 +("Bob", 22) + +~# my-cli render Bob 22 --json +{"inner":["Bob",22]} +``` + +用户传入 `--json` 时,框架自动将渲染结果序列化为 JSON,无需修改业务代码。 + +## 自定义输出结构 + +`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Groupped)]` 手动定义类型: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::prelude::*; +@@@use mingling::setup::StructuralRendererSetup; +@@@use mingling::StructuralData; +@@@use serde::Serialize; +@@@dispatcher!("render", CMDRender => EntryRender); + +#[derive(Serialize, StructuralData, Groupped)] +struct Info { + name: String, + age: i32, +} + +#[chain] +fn handle_render(args: EntryRender) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + Info { name, age }.to_render() +} + +#[renderer] +fn render_info(info: Info) { + r_println!("{} is {} years old", info.name, info.age); +} +@@@ +@@@fn main() { +@@@ let mut program = ThisProgram::new(); +@@@ program.with_setup(StructuralRendererSetup); +@@@ program.with_dispatcher(CMDRender); +@@@ program.exec(); +@@@} +@@@gen_program!(); +``` + +这时 `--json` 输出: + +```json +{ "name": "Bob", "age": 22 } +``` + +## 注意事项 + +- 支持格式:JSON、YAML、TOML、RON(取决于启用的特性) +- `StructuralRendererSetup` 注册 `--json`、`--yaml`、`--toml`、`--ron` 等全局参数 + +> [!NOTE] +> 每个类型仍需一个**空的 Renderer**,否则该类型 **不被视为可渲染** + +详见 [example-structural-renderer](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-structural-renderer)。 + ++ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/concepts/.name b/docs/_zh_CN/pages/concepts/.name new file mode 100644 index 0000000..dfd4543 --- /dev/null +++ b/docs/_zh_CN/pages/concepts/.name @@ -0,0 +1 @@ +核心概念 diff --git a/docs/_zh_CN/pages/concepts/1-the-pipeline.md b/docs/_zh_CN/pages/concepts/1-the-pipeline.md new file mode 100644 index 0000000..b0bb19d --- /dev/null +++ b/docs/_zh_CN/pages/concepts/1-the-pipeline.md @@ -0,0 +1,129 @@ ++ Mingling 的核心执行流程 +
+ +Mingling 把命令的处理拆成三个独立的阶段:Dispatcher → Chain → Renderer。这篇讲实际的执行逻辑——从用户输入到最终输出,每一步发生了什么。 + +## 完整流程 + +```mermaid +graph TD + A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"] + B --> C["Dispatch+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/concepts/2-resource.md b/docs/_zh_CN/pages/concepts/2-resource.md new file mode 100644 index 0000000..1052254 --- /dev/null +++ b/docs/_zh_CN/pages/concepts/2-resource.md @@ -0,0 +1,60 @@ ++ Mingling 如何管理全局状态 +
+ +命令行程序经常需要共享一些全局的东西——配置文件、数据库连接、计数器、当前工作目录。 + +在普通 Rust 里你可能会用 `OnceCell` 或 `lazy_static`,在 Mingling 里有一套统一的机制:**资源系统**。 + +## 什么是资源? + +资源就是在多个 Chain 和 Renderer 之间共享的数据。 + +你只需要定义一个类型、注册到 Program,然后在函数签名里声明你需要它——剩下的注入和生命周期管理都由框架完成。 + +## 核心机制:ResourceMarker + +任何同时实现了 `Default + Clone` 的类型都可以自动成为资源。框架会为它实现 `ResourceMarker` trait,使其具备: + +- **`res_clone()`** —— 当多个 Chain 同时访问时,框架可以通过 clone 来避免锁竞争 +- **`res_default()`** —— 资源未注册时提供兜底值 + +如果你需要更精细的生命周期控制,可以使用 `LazyRes+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md new file mode 100644 index 0000000..9b820da --- /dev/null +++ b/docs/_zh_CN/pages/concepts/3-any-output.md @@ -0,0 +1,73 @@ ++ 关于 AnyOutput 和 ChainProcess 的运作模式 +
+ +Dispatcher → Chain → Renderer 三阶段之间传递的数据是什么? + +Chain 的输出可能是一个成功结果、一个错误、或者还需要继续交给下一个 Chain——这些类型各不相同,管线如何在编译期不知道具体类型的情况下,把它们送到正确的地方? + +## AnyOutput:类型擦除 + 组标签 + +Mingling 的解法是把**所有类型擦除到同一个包装里**,然后用一个**枚举标签**来区分它们: + +``` +AnyOutput+ Written by @Weicao-CatilGrass +
diff --git a/docs/_zh_CN/pages/concepts/4-program-collect.md b/docs/_zh_CN/pages/concepts/4-program-collect.md new file mode 100644 index 0000000..f5e6b8f --- /dev/null +++ b/docs/_zh_CN/pages/concepts/4-program-collect.md @@ -0,0 +1,52 @@ ++ 了解 gen_program!() 是如何生成程序的 +
+ +每个 Mingling 程序最后都有一行 `gen_program!()`。它在背后做了三件事,把整个程序的骨架搭建出来。 + +## gen_program!() 的三件事 + +### 1. 生成枚举 + +扫描当前模块中所有 `pack!`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。 + +这个枚举就是 `AnyOutput+ Written by @Weicao-CatilGrass +
-- cgit