diff options
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> |
