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/pages/concepts/.name | 1 + docs/pages/concepts/1-the-pipeline.md | 129 +++++++++++++++++++++++++++++++ docs/pages/concepts/2-resource.md | 60 ++++++++++++++ docs/pages/concepts/3-any-output.md | 73 +++++++++++++++++ docs/pages/concepts/4-program-collect.md | 52 +++++++++++++ 5 files changed, 315 insertions(+) create mode 100644 docs/pages/concepts/.name create mode 100644 docs/pages/concepts/1-the-pipeline.md create mode 100644 docs/pages/concepts/2-resource.md create mode 100644 docs/pages/concepts/3-any-output.md create mode 100644 docs/pages/concepts/4-program-collect.md (limited to 'docs/pages/concepts') diff --git a/docs/pages/concepts/.name b/docs/pages/concepts/.name new file mode 100644 index 0000000..efb7494 --- /dev/null +++ b/docs/pages/concepts/.name @@ -0,0 +1 @@ +Core Concepts diff --git a/docs/pages/concepts/1-the-pipeline.md b/docs/pages/concepts/1-the-pipeline.md new file mode 100644 index 0000000..e73379d --- /dev/null +++ b/docs/pages/concepts/1-the-pipeline.md @@ -0,0 +1,129 @@ +

The Pipeline

+

+ How Mingling executes commands, step by step +

+ +Mingling splits command handling into three independent phases: Dispatcher → Chain → Renderer. This doc covers the actual execution logic — what happens at each step from user input to final output. + +## Full Flow + +```mermaid +graph TD + A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"] + B --> C["Dispatch
match cmd → Entry"] + C --> D["Hook: post_dispatch"] + D --> E{"user_context.help?"} + E -->|"true"| F["render_help
skip to help rendering"] + E -->|"false"| G{"has_chain?"} + G -->|"yes"| H["Hook: pre_chain"] + H --> I["do_chain
run business logic"] + I --> J{"ChainProcess?"} + J -->|"Ok(any, Renderer)"| K["Hook: pre_render →
render → post_render"] + J -->|"Ok(any, Chain)"| G + J -->|"Err"| L["finish"] + G -->|"no"| M{"has_renderer?"} + M -->|"yes"| K + M -->|"no"| N["build_renderer_not_found"] + N --> G + K --> O["Hook: finish → return RenderResult"] + L --> O + F --> O +``` + +## Phase Breakdown + +### 1. Dispatch + +`exec_with_args` first calls `dispatch_args_dynamic` or `dispatch_args_trie` (depending on whether the `dispatch_tree` feature is enabled), matching user input against registered Dispatchers. + +The matching rule is **prefix matching** on space-separated tokens — the longest match wins. For example, if both `remote.add` and `remote` are registered, input `remote add origin` will match `remote.add`. + +```mermaid +graph LR + Input["user input"] --> M{"match Dispatcher"} + M -->|"matched"| E["call dispatcher.begin(args)
return wrapped Entry"] + M -->|"no match"| NF["build_dispatcher_not_found
generate ErrorDispatcherNotFound"] +``` + +On a match, `dispatcher.begin(args)` is called, returning `ChainProcess::Ok((AnyOutput, _))` — the Entry type wrapping the user's input params. + +If no Dispatcher matches, `ErrorDispatcherNotFound` is generated (wrapping the full input), which a Renderer can later handle to display "Command not found". + +### 2. Help Shortcut + +Before entering the main loop, `program.user_context.help` is checked. If `true` (set by `HelpFlagSetup` in `BasicProgramSetup` when `--help` is parsed), `render_help` is called directly, skipping the entire pipeline. + +### 3. Chain Main Loop + +This is the core scheduling logic. Each iteration checks the current `AnyOutput`: + +1. **Has a Chain** → execute `C::do_chain(current)` + - Returns `(AnyOutput, Renderer)` → exit loop, go to rendering + - Returns `(AnyOutput, Chain)` → continue loop, pass result to next Chain + - Returns `Err` → terminate + +2. **No Chain, but has a Renderer** → render directly + +3. **Neither** → generate `renderer_not_found`, then loop again (the newly generated type might have a Renderer) + +```mermaid +graph TD + Start["current AnyOutput"] --> C{"has_chain?"} + C -->|"yes"| Chain["do_chain"] + Chain -->|"returns (any, Chain)"| C + Chain -->|"returns (any, Renderer)"| Render["render"] + Chain -->|"Err"| Exit["exit"] + C -->|"no"| R{"has_renderer?"} + R -->|"yes"| Render + R -->|"no"| N["build_renderer_not_found
try again"] + N --> C +``` + +### 4. Render + +The rendering phase calls `C::render(any, &mut render_result)`, which finds the matching `#[renderer]` function via `member_id` and writes the result into `RenderResult`. If `structural_renderer` is enabled, the result is also serialized to JSON/YAML (etc.) based on `program.structural_renderer_name`. + +### 5. Exit + +Sets `exit_code`, triggers the `finish` hook, and returns `RenderResult`. + +> [!TIP] +> This runtime dispatch code is driven by the enums generated by `gen_program!()` and the `ProgramCollect` implementation. +> +> At compile time only the type-to-Chain / Renderer / Help / Completion mapping is generated; actual matching and routing happens at runtime. + +## How This Is Different from Direct Function Calls + +This pipeline helps avoid writing code like this: + +```rust +@@@ struct Config; +@@@ impl Config { fn read() -> Self { Config } } +@@@ fn main() { +@@@ let json = true; +// read config +let mut config = Config::read(); + +// run operation +let Ok(result) = operation(&mut config) else { + panic!("error handling"); +}; + +// render result +if json { + print_json(); +} else { + println!("success!"); +} +@@@ } +@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) } +@@@ fn print_json() {} +``` + +Mingling's pipeline separates **cmd matching**, **business logic**, and **output rendering** into three independent slots, each responsible for one thing. + +More importantly, through hooks and the `AnyOutput` mechanism, the pipeline lets cross-cutting concerns (logging, auth, exit codes) be inserted non-invasively — no pollution of your business code. + +

+ Written by @Weicao-CatilGrass +

diff --git a/docs/pages/concepts/2-resource.md b/docs/pages/concepts/2-resource.md new file mode 100644 index 0000000..ad7ee16 --- /dev/null +++ b/docs/pages/concepts/2-resource.md @@ -0,0 +1,60 @@ +

Resource System

+

+ How Mingling Manages Global State +

+ +CLI programs often need to share global things—config files, database connections, counters, the current working directory. + +In vanilla Rust you might reach for `OnceCell` or `lazy_static`. In Mingling there's a unified mechanism: the **resource system**. + +## What is a Resource? + +A resource is data shared across multiple Chains and Renderers. + +You just define a type, register it with the Program, and declare it in your function signature—the framework handles injection and lifecycle management for you. + +## Core Mechanism: ResourceMarker + +Any type that implements both `Default + Clone` can automatically become a resource. The framework implements the `ResourceMarker` trait for it, giving it: + +- **`res_clone()`** — when multiple Chains access it concurrently, the framework can clone to avoid lock contention +- **`res_default()`** — provides a fallback value when the resource hasn't been registered + +If you need finer lifecycle control, you can use `LazyRes`. It lets the resource be initialized on first access and can run a callback on drop (e.g., saving state to disk before exit). + +## Why Not Global Variables? + +The traditional approach with statics creates implicit dependencies—you can't tell from the function signature what global state it uses. Mingling's resource injection makes **dependencies explicit**: + +- Whatever resources a function needs go in its parameter list +- `&T` means read-only access, `&mut T` means mutable +- Callers can see the function's side effects at a glance + +For example: + +```rust +@@@ use mingling::res::ResExitCode; +@@@ pack!(ErrorFileNotFound = ()); +#[chain] +fn handle_error_file_not_found( + error: ErrorFileNotFound, + ec: &mut ResExitCode // the signature reveals the side effect! +) { + ec.exit_code = 2; // modifying the exit code here +} +``` + +## Resources and Setup + +Resources are typically registered with the Program in two ways: + +1. **Direct registration** — calling `program.with_resource(...)` in `main` +2. **Via Setup** — using built-in Setups like `DirectoryEnvironmentSetup` to batch-register resources (e.g., `ResCurrentDir`, `ResHomeDir`) + +A Setup is a higher-level abstraction than a resource—one Setup can register multiple resources and do other initialization work. + +See the [Program Assembly](./pages/8-setup-and-resources) chapter in the tutorial for more details. + +

+ Written by @Weicao-CatilGrass +

diff --git a/docs/pages/concepts/3-any-output.md b/docs/pages/concepts/3-any-output.md new file mode 100644 index 0000000..f780377 --- /dev/null +++ b/docs/pages/concepts/3-any-output.md @@ -0,0 +1,73 @@ +

AnyOutput Mechanism

+

+ How AnyOutput and ChainProcess work +

+ +What data is passed between the Dispatcher → Chain → Renderer stages? + +A Chain's output could be a successful result, an error, or something that still needs to go to the next Chain—these types are all different. How does the pipeline route them to the right place without knowing the concrete types at compile time? + +## AnyOutput: Type Erasure + Group Tag + +Mingling's solution is to **erase all types into the same wrapper**, then use an **enum tag** to distinguish them: + +``` +AnyOutput +├── inner: Box ← the real data, type erased +├── type_id: TypeId ← runtime type ID, for safe downcast +└── member_id: G ← enum tag, marks "who this is" +``` + +Here `G` is the program enum generated by `gen_program!()` (i.e., `ThisProgram` as you know it). + +Each type annotated with `pack!` or `#[derive(Groupped)]` is assigned to one variant of this enum. + +## ChainProcess: Data + Routing + +On top of `AnyOutput`, `ChainProcess` adds **routing info**: + +``` +ChainProcess +├── Ok(AnyOutput, NextProcess) ← carries data, tells the dispatcher where to go next +│ ├── NextProcess::Chain ← "not done yet, pass to the next Chain" +│ └── NextProcess::Renderer ← "got a result, show it to the user" +└── Err(ChainProcessError) ← "something went wrong, abort" +``` + +This is why a Chain function returns `ChainProcess` instead of raw data—it bundles **"where to go next"** and **"the data"** together. + +The dispatcher reads `NextProcess` to decide whether to continue the loop or exit to rendering. + +## Groupped: Who Is Who + +How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Groupped` trait: + +``` +trait Groupped { + fn member_id() -> G; +} +``` + +When you use `pack!(ResultName = String)`, the macro automatically implements `Groupped` for `ResultName`, and `member_id()` returns the corresponding enum variant. The dispatcher looks at `member_id` and finds the matching Chain or Renderer. + +`to_chain()` and `to_render()` are essentially convenience methods on `AnyOutput` that construct `ChainProcess::Ok(any, Chain)` and `ChainProcess::Ok(any, Renderer)` respectively. + +## How Dispatching Works + +At runtime, the main loop does this: + +1. Check the current `AnyOutput`'s `member_id` +2. Look up whether this variant has a Chain → if yes, execute it, get a new `AnyOutput` and `NextProcess` +3. If `NextProcess` is `Chain` → go back to step 1 +4. If `NextProcess` is `Renderer` → exit the loop, render + +This mechanism ensures **type safety**: the dispatch code generated by `gen_program!()` only does `restore` (converting from `Box` back to a concrete type) inside the matching `member_id` branch, so it's impossible to unwrap a `ResultName`'s data as if it were `ErrorUserBlocked`. + +> [!TIP] +> In day-to-day dev, you don't need to manually touch `AnyOutput` or `ChainProcess`. +> +> Macros like `pack!`, `#[chain]`, and `#[renderer]` handle all the wrapping and unwrapping for you. + +

+ Written by @Weicao-CatilGrass +

diff --git a/docs/pages/concepts/4-program-collect.md b/docs/pages/concepts/4-program-collect.md new file mode 100644 index 0000000..a24f115 --- /dev/null +++ b/docs/pages/concepts/4-program-collect.md @@ -0,0 +1,52 @@ +

About ProgramCollect

+

+ Understand how gen_program!() builds a program +

+ +Every Mingling program ends with a `gen_program!()` call. Behind the scenes, it does three things to scaffold the entire program. + +## The three tasks of gen_program!() + +### 1. Generate an enum + +Scans the current module for all types marked with `pack!`, `#[chain]`, `#[renderer]` and similar macros, then generates an enum variant for each type. + +This enum is the type of `G` in `AnyOutput` — the scheduler uses enum variants to distinguish different data flowing through the pipeline. + +### 2. Generate a ProgramCollect impl + +`ProgramCollect` is a trait that defines the mapping of **"which type each enum variant corresponds to and who handles it"**: + +- **`do_chain`** — calls the corresponding `#[chain]` function by `member_id`, returns a new `AnyOutput` and `NextProcess` +- **`render`** — calls the corresponding `#[renderer]` function by `member_id`, writes to `RenderResult` +- **`render_help`** — calls the corresponding `#[help]` function by `member_id` +- **`has_chain` / `has_renderer`** — checks whether a variant has a corresponding handler +- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** — three built-in fallback types for edge cases + +This mapping is resolved at runtime via enum matching — only the enum and match branches are generated at compile time; actual function calls happen at runtime. + +### 3. Generate ThisProgram + +Generates the `ThisProgram` type alias, pointing to `Program`. That's why you can write `ThisProgram::new()` directly in `main` — it's the complete type of your whole program. + +--- + +## Differences under `pathf` and `dispatch_tree` + +The above describes the default behavior, which changes when specific features are enabled: + +### 1. `dispatch_tree` feature + +The Dispatcher no longer uses `Vec>` for linear matching. Instead, the subcommand structure is built as a prefix tree (Trie) at compile time. + +Matching complexity drops from `O(n)` to `O(k)` — where `k` is input length, independent of the number of commands. + +### 2. `pathf` feature (Module Pathfinder) + +By default, all macro-marked types must be in the same module for `gen_program!()` to collect them. + +With `pathf` enabled, the compiler automatically scans all sub-modules at compile time, finds all macro-marked types, and generates full module path references — types defined in deep sub-modules don't need a manual `use`. + +

+ Written by @Weicao-CatilGrass +

-- cgit