diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-30 18:05:05 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-30 18:05:05 +0800 |
| commit | 13408e79b940e9a33ca593ed30d1b20c54e01234 (patch) | |
| tree | 282549991a3f31791401ca2f3255b9318679d2e9 /docs/_zh_CN/pages/concepts | |
| parent | 29867ab5c0b40378a33318d989c809f90fc7d3aa (diff) | |
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
Diffstat (limited to 'docs/_zh_CN/pages/concepts')
| -rw-r--r-- | docs/_zh_CN/pages/concepts/.name | 1 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/concepts/1-the-pipeline.md | 129 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/concepts/2-resource.md | 60 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/concepts/3-any-output.md | 73 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/concepts/4-program-collect.md | 52 |
5 files changed, 315 insertions, 0 deletions
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> |
