diff options
Diffstat (limited to 'docs/_zh_CN/pages')
21 files changed, 2157 insertions, 0 deletions
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 @@ +<h1 align="center">帮助信息</h1> +<p align="center"> + 为命令添加 --help 支持 +</p> + +没有帮助信息的 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 <command>"); + r_println!("Commands:"); + r_println!(" greet Say hello"); +} +``` + +> [!TIP] +> `ErrorDispatcherNotFound` 是 `gen_program!()` 自动生成的类型,代表"没有匹配到任何命令"的情况。为它写 `#[help]` 就是给程序的根命令加帮助。 + +## 需要 Setup 配合 + +要让 `--help` 正常工作,需要在 `main` 里加上 `BasicProgramSetup`: + +```rust +@@@use mingling::macros::help; +@@@use mingling::setup::BasicProgramSetup; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` 内置了 `HelpFlagSetup`,它的作用仅仅是把 `program.user_context.help` 设为 `true`。 + +真正把请求路由到 `#[help]` 函数的是 `gen_program!()` 生成的代码 —— 它在调度时检查这个标记,如果为 `true` 就走帮助渲染路径,不经过 Chain。 + +不加 `BasicProgramSetup` 的话,`--help` 只是一个普通参数,会被当成 Entry 的输入传给 Chain。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">使用资源系统</h1> +<p align="center"> + 手把手带你使用资源 +</p> + +资源是 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)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">退出码控制</h1> +<p align="center"> + 如何使用资源系统管理程序退出码 +</p> + +程序退出时给 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<String>); +#[chain] +fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) { + // 检查失败的时候修改退出码资源 + ec.exit_code = 1; +} +``` + +> [!TIP] +> `ResExitCode` 就是一个 `struct ResExitCode { pub exit_code: i32 }`。`&mut ResExitCode` 注入后直接改字段即可。 + +## `Program` 的三种执行方式 + +`Program` 提供了三种执行方式(不包括 `repl` 特性下的 `exec_repl`): + +| 方式 | 行为 | +| ------------------------------- | -------------------------------------------------------------------------- | +| `program.exec_and_exit()` | 执行并直接以退出码终止进程 | +| `program.exec()` | 执行后返回 `i32` 退出码,由调用方决定怎么处理 | +| `program.exec_without_render()` | 返回 `Result<RenderResult, ProgramExecuteError>`,可读取内部的 `exit_code` | + +```rust +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); + + // 获取退出码自行处理 + let exit_code = program.exec(); + std::process::exit(exit_code); +} +@@@gen_program!(); +``` + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">钩子系统</h1> +<p align="center"> + 如何使用 ProgramHook 向程序内部插入行为 +</p> + +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 可以注册多个,按注册顺序执行。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">测试你的程序</h1> +<p align="center"> + 为 Chain 和 Renderer 编写单元测试 +</p> + +管线模型附带的一个好处就是 **可测试性**。 + +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<ChainProcess<ThisProgram>>`)。需要用框架提供的断言宏: + +```rust +@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process}; +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); +@@@#[chain] +@@@fn handle_hello(args: EntryHello) -> Next { +@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ if name.is_empty() { +@@@ ErrorNoName::default().to_render() +@@@ } else { +@@@ ResultName::new(name).to_render() +@@@ } +@@@} +#[test] +fn test_handle_hello_with_name() { + let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into(); + // 断言这是一个渲染结果(不是继续 chain) + assert_render_result!(chain_process); + // 断言 member_id 是 ResultName + assert_member_id!(chain_process, ResultName); + // 解包出内部值 + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +三个测试宏的作用: + +| 宏 | 功能 | +| ----------------------- | --------------------------------------------- | +| `assert_render_result!` | 断言 Chain 返回的是渲染路径(而非继续 chain) | +| `assert_member_id!` | 断言返回值的成员 ID 是某个类型 | +| `unpack_chain_process!` | 从 ChainProcess 中解包出原始类型 | + +## 用 entry! 宏构造数据 + +如果启用了 `extra_macros`,可以用 `entry!` 快速构造 Entry: + +```rust +// Features: ["extra_macros"] + +@@@use mingling::{assert_member_id, unpack_chain_process}; +@@@use mingling::macros::entry; +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_hello(args: EntryHello) -> Next { +@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ ResultName::new(name).to_render() +@@@} +#[test] +fn test_with_entry_macro() { + // entry! 从字符串字面量构造 Entry + let entry = entry!("--name", "Alice"); + let chain_process = handle_hello(entry).into(); + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +## 测试资源注入 + +如果 Chain 使用了资源,测试时需要提供资源实例: + +```rust +@@@use mingling::{assert_render_result, unpack_chain_process}; +@@@#[derive(Default, Clone)] +@@@struct ResPrefix(String); +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultGreeting = String); +@@@ +#[chain] +fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render() +} + +#[test] +fn test_handle_with_resource() { + // 资源需要在测试中手动传入 + let result = handle_hello( + EntryHello::new(vec!["World".to_string()]), + &ResPrefix("Hello".to_string()), + ); + let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram); + assert_eq!(greeting.inner, "Hello, World"); +} +``` + +管线模型让测试变得简单:每个 Chain 和 Renderer 都是相对独立的函数,构造输入、断言输出即可。 + +<p align="center" style="font-size: 0.85em; color: clear;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">声明一个分发器</h1> +<p align="center"> + 使用 dispatcher! 宏声明命令,并注册 +</p> + +Mingling 的管线从 Dispatcher 开始。 + +它的工作很简单:**匹配用户输入的命令,把参数包装成一个 Entry 类型**。 + +## `dispatcher!` 宏 + +`dispatcher!` 宏会同时生成两个类型: + +| 生成物 | 用途 | +| ----------- | ----------------------------------------------- | +| `CMDType` | 分发器本身,需要注册到 Program | +| `EntryType` | 入口类型,包裹 `Vec<String>`,作为 Chain 的输入 | + +写法是固定的三个部分: + +```rust +dispatcher!("命令路径", 分发器类型 => 入口类型); +``` + +看一个具体的例子: + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); +``` + +> [!NOTE] +> 命令名(`"greet"`)会自动转换为 kebab-case。即使你写 `"GreetUser"`,匹配时也会变成 `greet-user`。 + +## 注册到 Program + +有了分发器之后,需要告诉 Program 它的存在: + +```rust +@@@ dispatcher!("greet", CMDGreet => EntryGreet); +@@@ fn main() { +@@@ let mut program = ThisProgram::new(); +// 注册分发器 +program.with_dispatcher(CMDGreet); +@@@ } +@@@ gen_program!(); +``` + +> [!TIP] +> 如果命令多了,可以用 `with_dispatchers` 一次注册多个:`program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`。 + +## 多级命令 + +如果你的程序有层级结构——比如 `remote add`、`remote rm`——只需要在命令名里加点号分隔: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +用户在终端输入 `remote add` 时,Mingling 会依次匹配 `remote` 和 `add` 两个层级。 + +## 入口类型 `EntryGreet` + +你可能会好奇 `EntryGreet` 里面到底有什么。它本质上就是一个包装了 `Vec<String>` 的结构体: + +```rust +// 示意,dispatcher! 宏实际生成的代码 +pub struct EntryGreet { + pub inner: Vec<String>, +} +``` + +用户在命令行输入 `greet Alice Bob`,`EntryGreet.inner` 就是 `vec!["Alice", "Bob"]`。 + +> [!IMPORTANT] +> Entry 的 `inner` 只包含 **匹配后剩余的参数**。 +> +> 以 `remote add origin` 为例,`remote` 和 `add` 用于匹配命令路径,只有 `origin` 会进入 `EntryRemoteAdd.inner`。 + +## 进阶:隐式声明 + +以上是标准写法。如果你启用了 `extra_macros` 特性,还可以更简洁: + +```rust +// Features: ["extra_macros"] +// 省略 CMDType 和 EntryType,名字自动推导 + dispatcher!("greet"); +// dispatcher!("greet", CMDGreet => EntryGreet); +``` + +这种写法会自动生成 `CMDGreet` 和 `EntryGreet`,效果跟显式声明完全一样。 + +不过在教程阶段,我们继续用显式写法——更清晰,也不依赖额外特性。 + +详见[特性列表](pages/other/features)。 + +## 下一步 + +接下来我们写一个 Chain 来接收 Entry,处理真正的业务逻辑。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">声明一个链</h1> +<p align="center"> + 使用 chain 宏声明链,并承接 Entry 输入 +</p> + +上一节我们声明了 `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<ChainProcess<ThisProgram>>`。 + +> [!TIP] +> 想知道 `Into<ChainProcess<G>>` 是怎么工作的? +> +> 可以去 [任意输出机制](pages/concepts/3-any-output) 章节了解 `ChainProcess`。 + +## `pack!` 宏 + +你大概猜到了,`pack!(ResultName = String)` 定义了一个管线中传递的类型: + +```rust +// pack!(ResultName = String) 大概生成了这样的代码 + +#[derive(Groupped)] +pub struct ResultName { + pub inner: String, +} +``` + +你可以把它理解为一个 打了标签的 `String`。 + +调度器通过这个标签来精确路由,确保数据不会混淆 —— 比如发给 `RenderGreet` 的数据不会被误传给 `RenderError`。 + +> [!NOTE] +> 与简单的类型别名 (`type`) 不同,`pack!` 会生成一个全新的类型,拥有独立的 `TypeId`。 + +命名上推荐这样的习惯: + +| 角色 | 命名模式 | 示例 | +| -------- | ---------------- | -------------------- | +| 入口 | `Entry` + 命令名 | `EntryGreet` | +| 中间状态 | `State` + 描述 | `StateParsedArgs` | +| 最终结果 | `Result` + 描述 | `ResultGreetSomeone` | +| 错误 | `Error` + 描述 | `ErrorUserNotFound` | + +详见 [命名规范](pages/other/naming_rule),不过现在你只需要记住:**用 `pack!` 给你的数据取一个有意义的名字**。 + +## 从 Entry 中提取参数 + +`EntryGreet` 的 `inner` 是一个 `Vec<String>`,你可以在 Chain 里自由地处理它: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // 取第一个参数,没有就用默认值 + let name = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + + ResultName::new(name) +} +``` + +如果你启用了 `parser` 特性,还可以用 `Picker` 做更灵活的参数提取,不过那是后话了。 + +## 组合起来 + +现在把 Dispatcher 和 Chain 连在一起: + +```rust +// 1. 声明命令 +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. 声明管线中的数据类型 +pack!(ResultName = String); + +// 3. 处理逻辑 +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +不过这段代码还没写完 —— 我们只有 Dispatcher 和 Chain,还差最后一步:**把结果渲染出来**。这就是下一篇要讲的 Renderer。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">将结果渲染</h1> +<p align="center"> + 使用 renderer 宏声明渲染器,将结果输出 +</p> + +现在,我们创建了 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<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer: + +```rust +#[renderer] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { + if err.inner.is_empty() { + r_println!("Unknown command"); + } else { + r_println!("Command not found: \"{}\"", err.inner.join(" ")); + } +} +``` + +加上之后,再试试不存在的命令: + +```bash +cargo run -- great +``` + +```text +Command not found: "great" +``` + +## 恭喜 + +你完成了第一个完整的 Mingling 程序!来回顾一下学到的东西: + +| 概念 | 对应宏/函数 | 一句话 | +| -------- | ---------------- | -------------------------- | +| 声明命令 | `dispatcher!` | 告诉程序用户能输入什么 | +| 处理逻辑 | `#[chain]` | 收到参数后做什么 | +| 输出结果 | `#[renderer]` | 怎么把结果告诉用户 | +| 类型包装 | `pack!` | 给你的数据取个有意义的名字 | +| 程序入口 | `gen_program!()` | 自动生成管线的接线图 | + +在真实项目中你还会用到资源注入、hook、补全、REPL 等高级功能,不过核心骨架永远不变:**Dispatcher → Chain → Renderer**。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">多命令程序</h1> +<p align="center"> + 在一个程序里添加多个命令 +</p> + +真实世界的 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::<i32>().ok()).sum(); + ResultSum::new(sum) +} + +#[renderer] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); +} + +#[renderer] +fn render_sum(result: ResultSum) { + r_println!("Sum: {}", *result); +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatchers((CMDGreet, CMDAdd)); + program.exec_and_exit(); +} + +gen_program!(); +``` + +两个命令共享同一个管线模型,但各走各的: + +```text +> my-cli greet Alice +Hello, Alice! +> my-cli add 1 2 3 +Sum: 6 +``` + +## 注册多个分发器 + +注意到 `with_dispatchers` 了吗?当你需要注册多个分发器时,一次传一个元组就行: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@dispatcher!("add", CMDAdd => EntryAdd); +@@@pack!(ResultGreeting = String); +@@@pack!(ResultSum = i32); +@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) } +@@@#[renderer] fn render_greet(_greeting: ResultGreeting) { r_println!("hi"); } +@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) } +@@@#[renderer] fn render_sum(_sum: ResultSum) { r_println!("sum"); } +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatchers((CMDGreet, CMDAdd)); + program.exec_and_exit(); +} +``` + +等价于一个个注册,效果一样。 + +> [!TIP] +> 元组最多支持 7 个分发器。超过 7 个时链式调用 `with_dispatcher` 就行。 + +## 子命令 + +多层级的命令也是同理——每个点号分隔的层级都只是名字的一部分: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +每个子命令的 Entry、Chain、Renderer 完全独立,互不干扰。 + +## 数据类型的独立性 + +注意我们用了两个不同的 `pack!`: + +- `pack!(ResultGreeting = String)` +- `pack!(ResultSum = i32)` + +它们都是独立的类型,`gen_program!()` 会给它们分配不同的枚举变体。 + +调度器永远不会把 `ResultGreeting` 的数据送到 `render_sum` 去 —— **类型安全从命名那一刻就保证了**。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">使用 Picker 完成参数解析</h1> +<p align="center"> + 用 Picker 完成基本的参数解析 +</p> + +前面教程中我们都是手动从 `EntryGreet.inner`(`Vec<String>`)中提取参数。 + +```rust +@@@ fn main() { +@@@ let args : Vec<String> = vec![]; +let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); +@@@ } +``` + +但是,对于参数较多的场景,这个方案就不够用了:Mingling 提供了 `Picker` —— 通过链式调用来提取和转换参数。 + +要启用 `Picker`,你需要修改 `Cargo.toml` + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["parser"] +``` + +好了,让我们看看 `Picker` 的写法: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or((), "World").unpack(); + ResultName::new(name) +} +``` + +`AsPicker` 为所有可以转换为 `Vec<String>` 的类型实现了 `pick`、`pick_or`、`pick_or_route` 函数:它们可以语义化地从字符串列表中 **拾取 (Pick)** 参数,并转换为结构化数据。 + +对于上述示例中的代码: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) -> Next { +let name = prev.pick_or((), "World").unpack(); +@@@ResultName::new(name) +@@@} +``` + +它的语义为: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or((), "World").unpack(); +// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ 解包为 String +// | | | |__________ 默认值为 "World" +// | | |______________ 取出第一个位置参数(不指定标志) +// | |______________________ 拾取或使用默认 +// |___________________________ 从前一个输入中 +@@@} +``` + +## 解析标志参数 + +若你的程序需要解析标志参数(例如 `greet --name Alice`),可以使用如下方式 + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or(["--name", "-n"], "World").unpack(); + ResultName::new(name) +} +``` + +同理,它的语义为: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or(["--name", "-n"], "World").unpack(); +// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ 解包为 String +// | | | |__________ 默认值为 "World" +// | | |____________________________ 取出 "--name" 或 "-n" 后面的参数 +// | |____________________________________ 拾取或使用默认 +// |_________________________________________ 从前一个输入中 +@@@} +``` + +## 关于 `.unpack()` + +你可能注意到了,`Picker` 在命令解析的最后,会执行一个 `.unpack()` 函数,它的作用是将前面解析出来的结果,转换为结构化信息。 + +对于只拾取了一次的数据来说,`.unpack()` 会返回单个数据,而对于多次拾取,`Picker` 则会返回元组: + +```rust +// Features: ["parser"] +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultInfo = (String, u8, u32)); + +#[chain] +fn handle_test_entry(prev: EntryTest) -> Next { + let (name, age, id) = prev + .pick::<String>(["--name", "-n"]) + .pick::<u8>(["--age", "-a"]) + .pick::<u32>(["--id", "-I"]) + .unpack(); + + ResultInfo::new((name, age, id)) +} +``` + +> [!IMPORTANT] +> `Picker` 对解析顺序极其敏感,特别是位置参数:因为它是顺序解析的。若你需要解析位置参数,请确保解析前已拾取并消费所有 **标志参数**。 + +## 使用 `pick_or_route` 处理边界情况 + +就像那句老话:"永远不要相信你的用户"。为了应对必要参数缺失、输入类型不匹配等错误情况,`pick_or_route` 能将执行链路由到专门的错误处理类型上。 + +先来看一个简单示例 + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or_route(["--name", "-n"], ErrorNoName::default()) + .unpack(); + + // 使用 route! 宏展开 pick_result + let name = route!(pick_result); + ResultName::new(name).into() +} + +#[renderer] +fn render_no_name(_prev: ErrorNoName) { + r_println!("Error: No name provided."); +} + +#[renderer] +fn render_name(prev: ResultName) { + r_println!("Hello, {}!", *prev); +} +``` + +若使用 `pick_or_route`,写法会变得相对复杂:因为 `.unpack()` 不再直接返回参数,而是 `Result<Value, Route>`。 + +不过 **Mingling** 的 `extra_macros` 特性提供了简化展开的宏 `route!`,它不复杂,只是省略了一部分样板代码: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ use mingling::macros::route; +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = route!(pick_result); +@@@ mingling::macros::empty_result!() +@@@ } +``` + +它展开为: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = match pick_result { + Ok(r) => r, + Err(e) => return e.to_chain(), +}; +@@@ mingling::macros::empty_result!() +@@@ } +``` + +## 提取值的后处理 + +在您使用 `pick` 提取了用户输入后,可以使用 `after` 立刻处理该参数 + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev + .pick_or(["--name", "-n"], "World") + // 在提取出 --name 后,立刻格式化 + .after(|name: String| { + name.replace(['-', '_', '.'], " ") + .to_lowercase() + .trim() + .to_string() + }) + .unpack(); + + ResultName::new(name) +} +``` + +同样,你可以使用 `after_or_route` 来处理输入参数的格式错误 + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNameTooLong = usize); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or(["--name", "-n"], "World") + .after_or_route(|name: &String| { + if name.len() < 32 { + Ok(name.clone()) + } else { + Err(ErrorNameTooLong::new(name.len())) + } + }) + .unpack(); + let name = route!(pick_result); + + ResultName::new(name).into() +} + +#[renderer] +fn render_name_too_long(prev: ErrorNameTooLong) { + let len = *prev; + r_println!("Error: name too long (length: {} > 32)", len); +} + +#[renderer] +fn render_name(prev: ResultName) { + r_println!("Hello, {}!", *prev); +} +``` + +## 布尔值解析 + +`Picker` 当然也可以解析布尔类型,但是布尔类型分为显式和隐式模式: + +| 模式 | 格式 | +| ---- | ----------------------------------- | +| 隐式 | `--confirmed` | +| 显式 | `--confirm true` 或 `--confirm yes` | + +- 使用 `.pick::<bool>(flag)` 时,采用隐式解析:只要标志存在即为 `true` +- 使用 `.pick::<Yes>(flag)` 或 `.pick::<True>(flag)` 时,采用显式解析 + +一般来说使用隐式解析即可,但在处理重要的确认行为时,显式逻辑更符合语义。 + +```rust +// Features: ["parser"] +@@@use mingling::parser::Yes; +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); + +#[chain] +fn handle_entry(prev: EntryTest) -> Next { +@@@ let prev1 = prev.clone(); + let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes(); +@@@ let prev = prev1; + let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack(); + ResultDone::default().to_render() +} +``` + +## 特殊用法:`usize` 解析 + +**Mingling** 为 `usize` 提供了一个特殊的用法:解析类似 `25G`、`32mib` 等字样 + +```rust +// Features: ["parser"] + +#[test] +fn parse_size() { + let vec = vec!["--size".to_string(), "25mib".to_string()]; + let size: usize = vec.pick(["--size", "-S"]).unpack(); + assert_eq!(size, 25 * 1024 * 1024); +} +``` + +## 自定义可解析类型 + +你可以使用 `Pickable` trait 使你的类型支持被 `Picker` 解析,这也是 `Picker` 拓展性的来源 + +```rust +// Features: ["parser"] +@@@use mingling::parser::{Pickable, Argument}; +@@@use mingling::Flag; +#[derive(Default)] +pub struct Address { + ip: String, + port: u16, +} + +impl Pickable for Address { + type Output = Self; + fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> { + let raw = args.pick_argument(flag)?; + let parts: Vec<&str> = raw.split(':').collect(); + let ip = parts.first()?.to_string(); + let port: u16 = parts.get(1)?.parse().ok()?; + Some(Address { ip, port }) + } +} +@@@dispatcher!("connect", CMDConnect => EntryConnect); +@@@pack!(ResultConnected = Address); + +#[chain] +fn handle_connect_entry(prev: EntryConnect) -> Next { + let address: Address = prev.pick("--addr").unpack(); + ResultConnected::new(address) +} + +#[renderer] +fn render_connected(prev: ResultConnected) { + let addr = prev.inner; + r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); +} +``` + +执行效果如下: + +```text +~# my-cli connect --addr 127.0.0.1:8080 +Connected: IP: 127.0.0.1 PORT: 8080 +``` + +## 自动为枚举实现 Pickable + +要为枚举类型实现 `Pickable`,只需该枚举实现了 `EnumTag`,然后为其实现 `PickableEnum` 即可 + +```rust +// Features: ["parser"] +@@@use mingling::parser::PickableEnum; +@@@use mingling::EnumTag; +#[derive(Debug, Default, EnumTag)] +pub enum Fruits { + #[default] + Apple, + Banana, + Orange, +} + +impl PickableEnum for Fruits {} +@@@dispatcher!("eat", CMDEat => EntryEat); +@@@pack!(ResultFruit = Fruits); + +#[chain] +fn handle_eat_entry(prev: EntryEat) -> Next { + let fruit: Fruits = prev.pick("--fruit").unpack(); + ResultFruit::new(fruit) +} + +#[renderer] +fn render_fruit(prev: ResultFruit) { + r_println!("Picked fruit: {:?}", *prev); +} +``` + +以上便是 `Picker` 的所有用法。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">使用 Clap 完成参数解析</h1> +<p align="center"> + 用 clap 做更复杂的参数解析 +</p> + +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)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">程序装配</h1> +<p align="center"> + 用 Setup 初始化你的程序 +</p> + +当程序启动时需要做一些初始化工作——比如解析全局参数、注册资源——你可以用 `#[program_setup]` 来组织这些逻辑。 + +## 用 Setup 做初始化 + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // 从参数中提取全局标志 + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); +} +@@@ +@@@fn main() { +@@@ let mut program = ThisProgram::new(); +@@@ program.with_setup(MySetup); +@@@ program.exec_and_exit(); +@@@} +@@@gen_program!(); +``` + +`#[program_setup]` 标记的函数接收 `&mut Program<ThisProgram>`,你可以在里面做任何初始化操作。 + +在 `main` 里通过 `program.with_setup(...)` 注册即可使用。 + +> [!NOTE] +> `#[program_setup]` 需要 `extra_macros` 特性。没有此特性时,可以手动实现 `ProgramSetup` trait。 + +## 提取全局参数 + +Setup 里最常用的操作就是提取全局参数。Mingling 提供了几个辅助方法: + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // 布尔标志 + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); + + // 带值的参数 + program.global_argument("--name", |_program, value| { + // value 就是 "Alice" + let _ = value; + }); +} +``` + +> [!TIP] +> `global_flag` 和 `global_argument` 会自动从 `program.args` 中移除已匹配的参数,这些参数不会进入管线。 + +## 内置 Setup + +Mingling 提供了一些开箱即用的 Setup,覆盖了 CLI 程序最常见的需求: + +| Setup | 功能 | +| --------------------------- | --------------------------------------------------------------------- | +| `BasicProgramSetup` | 解析 `--help`/`-h`、`--quiet`/`-q`、`--confirm`/`-C` | +| `DirectoryEnvironmentSetup` | 注册目录资源:当前目录、可执行目录、Home 目录、临时目录 | +| `ExitCodeSetup` | 通过 `ResExitCode` 控制程序退出码 | +| `StructuralRendererSetup` | 启用 `--json`、`--yaml` 等结构化输出(需 `structural_renderer` 特性) | + +用法就是在 `main` 里加一行: + +```rust +@@@use mingling::setup::BasicProgramSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` 帮你处理了绝大多数 CLI 程序都需要的通用参数,省去自己手动解析的麻烦。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">错误处理</h1> +<p align="center"> + 将错误优雅地展示给用户 +</p> + +管线里不只有成功路径。当输入有误、资源找不到、操作失败时,你需要一个地方来处理这些"意外",而不是让程序 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)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">补全</h1> +<p align="center"> + 使用 comp 特性实现完全动态的补全系统 +</p> + +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)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">结构化渲染</h1> +<p align="center"> + 使用 structural_renderer 特性将结果渲染为序列化文本 +</p> + +启用 `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)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">基础管线</h1> +<p align="center"> + Mingling 的核心执行流程 +</p> + +Mingling 把命令的处理拆成三个独立的阶段:Dispatcher → Chain → Renderer。这篇讲实际的执行逻辑——从用户输入到最终输出,每一步发生了什么。 + +## 完整流程 + +```mermaid +graph TD + A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"] + B --> C["Dispatch<br/>匹配命令 → Entry"] + C --> D["Hook: post_dispatch"] + D --> E{"user_context.help?"} + E -->|"true"| F["render_help<br/>直接跳帮助渲染"] + E -->|"false"| G{"has_chain?"} + G -->|"有"| H["Hook: pre_chain"] + H --> I["do_chain<br/>执行业务逻辑"] + I --> J{"ChainProcess?"} + J -->|"Ok(any, Renderer)"| K["Hook: pre_render →<br/>render → post_render"] + J -->|"Ok(any, Chain)"| G + J -->|"Err"| L["finish"] + G -->|"无"| M{"has_renderer?"} + M -->|"有"| K + M -->|"无"| N["build_renderer_not_found"] + N --> G + K --> O["Hook: finish → 返回 RenderResult"] + L --> O + F --> O +``` + +## 阶段详解 + +### 1. 分发 + +`exec_with_args` 首先调用 `dispatch_args_dynamic` 或 `dispatch_args_trie`(取决于是否启用 `dispatch_tree` 特性),将用户输入的参数与注册的 Dispatcher 匹配。 + +匹配规则是按空格分割的**前缀匹配**——优先匹配最长的那个。例如注册了 `remote.add` 和 `remote`,输入 `remote add origin` 会匹配 `remote.add`。 + +```mermaid +graph LR + Input["用户输入"] --> M{匹配 Dispatcher} + M -->|"匹配到"| E["调用 dispatcher.begin(args)<br/>返回包装好的 Entry"] + M -->|"未匹配"| NF["build_dispatcher_not_found<br/>生成 ErrorDispatcherNotFound"] +``` + +匹配成功后调用 `dispatcher.begin(args)`,返回 `ChainProcess::Ok((AnyOutput, _))`,即包装好用户输入参数的 Entry 类型。 + +如果没有匹配到任何 Dispatcher,则生成 `ErrorDispatcherNotFound`(包裹完整的输入参数),后续可以被 Renderer 处理显示 "Command not found"。 + +### 2. Help 短路 + +在进入主循环前,检查 `program.user_context.help`。如果为 `true`(由 `BasicProgramSetup` 中的 `HelpFlagSetup` 在解析到 `--help` 时设置),直接调用 `render_help` 跳过整个管线。 + +### 3. Chain 主循环 + +这是核心调度逻辑。每次循环检查当前的 `AnyOutput`: + +1. **有 Chain 能处理** → 执行 `C::do_chain(current)` + - 返回 `(AnyOutput, Renderer)` → 退出循环,进入渲染 + - 返回 `(AnyOutput, Chain)` → 继续循环,把结果交给下一个 Chain + - 返回 `Err` → 终止程序 + +2. **没有 Chain,但有 Renderer** → 直接渲染 + +3. **两者都没有** → 生成 `renderer_not_found`,再循环一次(因为刚生成的这个类型可能有 Renderer) + +```mermaid +graph TD + Start["当前 AnyOutput"] --> C{"has_chain?"} + C -->|"是"| Chain["do_chain"] + Chain -->|"返回 (any, Chain)"| C + Chain -->|"返回 (any, Renderer)"| Render["render"] + Chain -->|"Err"| Exit["退出"] + C -->|"否"| R{"has_renderer?"} + R -->|"是"| Render + R -->|"否"| N["build_renderer_not_found<br/>再试一次"] + N --> C +``` + +### 4. Render + +渲染阶段调用 `C::render(any, &mut render_result)`,根据 `member_id` 找到对应的 `#[renderer]` 函数,将结果写入 `RenderResult`。如果启用了 `structural_renderer`,还会根据 `program.structural_renderer_name` 将结果序列化为 JSON/YAML 等格式。 + +### 5. 退出 + +设置 `exit_code`,触发 `finish` hook,返回 `RenderResult`。 + +> [!TIP] +> 这套运行时调度代码由 `gen_program!()` 生成的枚举和 `ProgramCollect` 实现来驱动。 +> +> 编译期只生成了类型到 Chain / Renderer / Help / Completion 的映射关系,实际的匹配和路由都是在运行时完成的。 + +## 这和直接函数调用的区别 + +这套管线可以避免你写出如下的代码: + +```rust +@@@ struct Config; +@@@ impl Config { fn read() -> Self { Config } } +@@@ fn main() { +@@@ let json = true; +// 读取配置 +let mut config = Config::read(); + +// 执行操作 +let Ok(result) = operation(&mut config) else { + panic!("错误处理"); +}; + +// 渲染结果 +if json { + print_json(); +} else { + println!("成功!"); +} +@@@ } +@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) } +@@@ fn print_json() {} +``` + +Mingling 的管线把 **命令匹配**、**业务逻辑**、**输出展示** 拆成三个独立的位置,每个位置只负责一件事。 + +更重要的是,管线经过 hook 和 `AnyOutput` 机制,允许横切关注点(日志、鉴权、退出码)无侵入地插入,不会污染你的业务代码。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">资源系统</h1> +<p align="center"> + Mingling 如何管理全局状态 +</p> + +命令行程序经常需要共享一些全局的东西——配置文件、数据库连接、计数器、当前工作目录。 + +在普通 Rust 里你可能会用 `OnceCell` 或 `lazy_static`,在 Mingling 里有一套统一的机制:**资源系统**。 + +## 什么是资源? + +资源就是在多个 Chain 和 Renderer 之间共享的数据。 + +你只需要定义一个类型、注册到 Program,然后在函数签名里声明你需要它——剩下的注入和生命周期管理都由框架完成。 + +## 核心机制:ResourceMarker + +任何同时实现了 `Default + Clone` 的类型都可以自动成为资源。框架会为它实现 `ResourceMarker` trait,使其具备: + +- **`res_clone()`** —— 当多个 Chain 同时访问时,框架可以通过 clone 来避免锁竞争 +- **`res_default()`** —— 资源未注册时提供兜底值 + +如果你需要更精细的生命周期控制,可以使用 `LazyRes<T>`。它允许资源在第一次被访问时才初始化,并且可以在析构时执行回调(比如退出前保存状态到磁盘)。 + +## 为什么不用全局变量? + +传统做法的静态变量是隐式依赖 —— 你看函数签名根本不知道它用了什么全局状态。而 Mingling 的资源注入让 **依赖显式化**: + +- 函数需要什么资源,参数列表就写什么 +- `&T` 表示只读访问,`&mut T` 表示可修改 +- 调用者一眼就能看出这个函数的副作用 + +例如: + +```rust +@@@ use mingling::res::ResExitCode; +@@@ pack!(ErrorFileNotFound = ()); +#[chain] +fn handle_error_file_not_found( + error: ErrorFileNotFound, + ec: &mut ResExitCode // 通过签名可以看出副作用! +) { + ec.exit_code = 2; // 这里修改了退出码 +} +``` + +## 资源与 Setup 的关系 + +资源通常通过两个途径注册到 Program: + +1. **直接注册** —— 在 `main` 中调用 `program.with_resource(...)` +2. **通过 Setup** —— 使用 `DirectoryEnvironmentSetup` 等内置 Setup 批量注册(如 `ResCurrentDir`、`ResHomeDir`) + +Setup 是比资源更高层的抽象,一个 Setup 可以注册多个资源并做其他初始化工作。 + +详见教程中的 [程序装配](./pages/8-setup-and-resources) 一章。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">任意输出机制</h1> +<p align="center"> + 关于 AnyOutput 和 ChainProcess 的运作模式 +</p> + +Dispatcher → Chain → Renderer 三阶段之间传递的数据是什么? + +Chain 的输出可能是一个成功结果、一个错误、或者还需要继续交给下一个 Chain——这些类型各不相同,管线如何在编译期不知道具体类型的情况下,把它们送到正确的地方? + +## AnyOutput:类型擦除 + 组标签 + +Mingling 的解法是把**所有类型擦除到同一个包装里**,然后用一个**枚举标签**来区分它们: + +``` +AnyOutput<G> +├── inner: Box<dyn Any + Send> ← 真正的数据,类型已被擦除 +├── type_id: TypeId ← 运行时类型 ID,用于安全 downcast +└── member_id: G ← 枚举标签,标记"这是谁" +``` + +这里的 `G` 就是 `gen_program!()` 生成的程序枚举(也就是你熟知的 `ThisProgram`)。 + +每个被 `pack!` 或 `#[derive(Groupped)]` 标记的类型都被分配到这个枚举的一个变体。 + +## ChainProcess:数据 + 路由 + +在 `AnyOutput` 的基础上,`ChainProcess<G>` 加了一个**路由信息**: + +``` +ChainProcess<G> +├── Ok(AnyOutput<G>, NextProcess) ← 携带数据,告诉调度器下一步去哪 +│ ├── NextProcess::Chain ← "还没完,继续交给下一个 Chain" +│ └── NextProcess::Renderer ← "出结果了,展示给用户" +└── Err(ChainProcessError) ← "出错了,终止程序" +``` + +这就是为什么 Chain 函数返回的不是裸数据,而是 `ChainProcess`——它把 **"下一步去哪"** 和 **"数据"** 打包在一起。 + +调度器根据 `NextProcess` 决定是继续循环还是退出渲染。 + +## Groupped:谁是谁 + +调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Groupped` trait: + +``` +trait Groupped<G> { + fn member_id() -> G; +} +``` + +当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Groupped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。 + +`to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。 + +## 调度的执行 + +在运行时,主循环的工作就是: + +1. 看当前 `AnyOutput` 的 `member_id` +2. 查这个变体有没有对应的 Chain → 有就执行,拿到新的 `AnyOutput` 和 `NextProcess` +3. 如果 `NextProcess` 是 `Chain` → 回到第 1 步 +4. 如果 `NextProcess` 是 `Renderer` → 退出循环,渲染 + +这套机制保证了**类型安全**:`gen_program!()` 生成的调度代码在做 `restore`(从 `Box<dyn Any>` 还原为具体类型)时,一定是在匹配的 `member_id` 分支内做的,不可能把 `ResultName` 的数据当作 `ErrorUserBlocked` 来解包。 + +> [!TIP] +> 日常开发中你不需要手动操作 `AnyOutput` 或 `ChainProcess`。 +> +> `pack!`、`#[chain]`、`#[renderer]` 这些宏帮你处理了所有的包装和解包。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">关于 ProgramCollect</h1> +<p align="center"> + 了解 gen_program!() 是如何生成程序的 +</p> + +每个 Mingling 程序最后都有一行 `gen_program!()`。它在背后做了三件事,把整个程序的骨架搭建出来。 + +## gen_program!() 的三件事 + +### 1. 生成枚举 + +扫描当前模块中所有 `pack!`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。 + +这个枚举就是 `AnyOutput<G>` 中 `G` 的类型 —— 调度器靠枚举变体来区分管线中传递的不同数据。 + +### 2. 生成 ProgramCollect 实现 + +`ProgramCollect` 是一个 trait,定义了 **"每个枚举变体对应什么类型、由谁处理"** 的映射关系: + +- **`do_chain`** —— 根据 `member_id` 调用对应的 `#[chain]` 函数,返回新的 `AnyOutput` 和 `NextProcess` +- **`render`** —— 根据 `member_id` 调用对应的 `#[renderer]` 函数,写入 `RenderResult` +- **`render_help`** —— 根据 `member_id` 调用对应的 `#[help]` 函数 +- **`has_chain` / `has_renderer`** —— 判断某个变体有没有对应的处理函数 +- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** —— 三个内置降级类型,处理边界情况 + +这套映射在运行时通过枚举匹配来完成——编译期只生成了枚举和匹配分支,实际的函数调用发生在运行时。 + +### 3. 生成 ThisProgram + +生成 `ThisProgram` 类型别名,指向 `Program<生成的枚举>`。这就是为什么在 `main` 中可以直接写 `ThisProgram::new()`——它就是你整个程序的完整类型。 + +--- + +## 关于 `pathf` 和 `dispatch_tree` 下的差异 + +以上是默认行为,但在启用特定 feature 时会有变化: + +### 1. `dispatch_tree` 特性 + +Dispatcher 的匹配不再使用 `Vec<Box<dyn Dispatcher>>` 做线性匹配,而是在编译期将子命令结构构建为前缀树(Trie)。 + +匹配复杂度从 `O(n)` 降到 `O(k)` —— `k` 是输入长度,与命令数量无关。 + +### 2. `pathf` 特性(Module Pathfinder) + +默认情况下所有宏标记的类型必须在同一模块才能被 `gen_program!()` 收集到 + +启用 `pathf` 后,编译期自动扫描所有子模块,找到所有用宏标记的类型并生成完整的模块路径引用 —— 类型定义在深层子模块也无需手动 `use`。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> |
