diff options
Diffstat (limited to 'docs/pages')
25 files changed, 2189 insertions, 26 deletions
diff --git a/docs/pages/.gitignore b/docs/pages/.gitignore deleted file mode 100644 index dd33554..0000000 --- a/docs/pages/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.obsidian diff --git a/docs/pages/1-getting-started.md b/docs/pages/1-getting-started.md index 5e746d3..c3527db 100644 --- a/docs/pages/1-getting-started.md +++ b/docs/pages/1-getting-started.md @@ -19,9 +19,9 @@ features = [] ## Enable Features -**Mingling** has all features disabled by default and does not provide an all-in-one feature like `full`. +**Mingling** has all features disabled by default and does **not** provide an all-in-one feature like `full`. -Some features will **directly affect the behavior of the entire lifecycle**, so you need to enable them as needed, for example: +Some features **directly affect the entire lifecycle behavior**, so you need to enable them as needed, e.g.: ```toml [dependencies.mingling] @@ -33,11 +33,11 @@ features = [ ``` > [!NOTE] -> Visit [docs.rs](https://docs.rs/mingling/latest/mingling/feature/index.html) or [Features](pages/other/features) to learn about all available features. +> Visit [docs.rs](https://docs.rs/mingling/latest/mingling/feature/index.html) or [Features](pages/other/features) to learn about all features. ## Write the Basic Entry Point -Edit `src/main.rs` with the following code: +Write the following code in `src/main.rs`: ```rust use mingling::prelude::*; @@ -52,13 +52,13 @@ gen_program!(); ``` > [!IMPORTANT] -> Almost all Rust code blocks in the documentation have been compiled through the CI pipeline and are guaranteed to be usable. +> Almost all Rust code blocks in the docs have been compiled in CI and are guaranteed to work. > -> However, code blocks starting with `// NOT VERIFIED` have **not been verified**. +> However, code blocks starting with `// NOT VERIFIED` are **not verified**. > -> Want to know which `*.md` files have been compiled? Check [`verified-docs.toml`](https://github.com/mingling-rs/mingling/blob/main/verified-docs.toml) +> Want to know which `*.md` files are compiled? See [`verified-docs.toml`](https://github.com/mingling-rs/mingling/blob/main/verified-docs.toml). -## Verify Compilation +## Verify with Compilation ```plaintext ~# cargo check @@ -66,4 +66,4 @@ gen_program!(); --- -Once everything is good, start building! +Once everything is good, start writing something! diff --git a/docs/pages/10-help.md b/docs/pages/10-help.md new file mode 100644 index 0000000..2f3b74f --- /dev/null +++ b/docs/pages/10-help.md @@ -0,0 +1,69 @@ +<h1 align="center">Help Info</h1> +<p align="center"> + Adding --help support to commands +</p> + +A CLI without help info is not a good CLI. + +In Mingling, use the `#[help]` macro to add help text to commands. + +## Simplest Help + +Write a help function directly for an 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] +> Help functions also use `r_println!`, because `#[help]` follows the rendering pipeline — it's a short-circuit render triggered early by the `--help` flag, not logic outside the pipeline. + +## Global Help + +You can also write help for `ErrorDispatcherNotFound` as the "root help": + +```rust +@@@use mingling::macros::help; +// Triggered when user passes --help directly +#[help] +fn help_root(entry: ErrorDispatcherNotFound) { + r_println!("Usage: my-cli <command>"); + r_println!("Commands:"); + r_println!(" greet Say hello"); +} +``` + +> [!TIP] +> `ErrorDispatcherNotFound` is a type generated by `gen_program!()`, representing "no matching command found." Writing `#[help]` for it adds help to the program's root command. + +## Requires Setup + +For `--help` to work properly, add `BasicProgramSetup` in `main`: + +```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` includes `HelpFlagSetup`, which simply sets `program.user_context.help` to `true`. + +The actual routing to the `#[help]` function is handled by code generated via `gen_program!()` — it checks this flag during dispatch, and if `true`, goes through the help rendering path, bypassing the Chain. + +Without `BasicProgramSetup`, `--help` is treated as a normal argument and passed as input to the Entry's Chain. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md new file mode 100644 index 0000000..b54938a --- /dev/null +++ b/docs/pages/11-resource-system.md @@ -0,0 +1,89 @@ +<h1 align="center">Using the Resource System</h1> +<p align="center"> + A hands-on guide to resources +</p> + +Resources are Mingling's mechanism for managing global state. Any type that implements `Default + Clone` can be a resource. + +## Define a Resource + +```rust +// Any type implementing Default + Clone can be used as a resource +#[derive(Default, Clone)] +struct ResCurrentDir(String); + +// Register with the Program +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResCurrentDir(".".into())); + program.exec_and_exit(); +} +``` + +Since `ResCurrentDir` implements both `Default` and `Clone`, the framework automatically implements `ResourceMarker` for it — no manual impl needed. + +## Inject & Use + +In a Chain or Renderer, simply declare the resource in the parameter list: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResCurrentDir(String); +@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir); +@@@pack!(ResultPath = String); +// Inject read-only resource via &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); +} +``` + +## Modify a Resource + +Use `&mut T` to inject a mutable resource: + +```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); +} +``` + +## Use Multiple Resources + +A Chain can inject any number of resources at once — the framework matches them by type automatically: + +```rust +@@@#[derive(Default, Clone)] struct ResConfig(String); +@@@#[derive(Default, Clone)] struct ResCounter(u32); +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); +// Inject both read-only and mutable resources +#[chain] +fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next { + println!("config: {}", config.0); + counter.0 += 1; + ResultDone::default().to_render() +} +``` + +For deeper topics like `ResourceMarker`, lazy loading with `LazyRes`, etc., see [Core Concepts: Resource System](pages/concepts/2-resource). + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/12-exit-code.md b/docs/pages/12-exit-code.md new file mode 100644 index 0000000..6828cde --- /dev/null +++ b/docs/pages/12-exit-code.md @@ -0,0 +1,68 @@ +<h1 align="center">Exit Code Control</h1> +<p align="center"> + Managing program exit codes via the resource system +</p> + +Providing the shell with a correct exit code when a program terminates is a basic CLI convention. Mingling offers a ready-to-use `ExitCodeSetup` that, together with the `ResExitCode` resource, makes exit code control incredibly simple. + +## Enabling 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` does two things: + +1. Registers the `ResExitCode` resource (default value `0`) +2. Registers a `finish` hook that reads the value of `ResExitCode` as the final exit code before the program exits + +## Modifying the Exit Code + +In a Chain or Renderer, inject `ResExitCode` to modify the exit code: + +```rust +@@@use mingling::res::ResExitCode; +@@@use mingling::setup::ExitCodeSetup; +@@@pack!(EntryCheck = Vec<String>); +#[chain] +fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) { + // Modify exit code when check fails + ec.exit_code = 1; +} +``` + +> [!TIP] +> `ResExitCode` is simply `struct ResExitCode { pub exit_code: i32 }`. Inject `&mut ResExitCode` and modify the field directly. + +## Three Execution Modes of `Program` + +`Program` provides three execution modes (excluding `exec_repl` under the `repl` feature): + +| Mode | Behavior | +| ------------------------------- | ----------------------------------------------------------------------------------------- | +| `program.exec_and_exit()` | Executes and terminates the process directly with the exit code | +| `program.exec()` | Executes and returns an `i32` exit code, letting the caller decide handling | +| `program.exec_without_render()` | Returns `Result<RenderResult, ProgramExecuteError>`, with internal `exit_code` accessible | + +```rust +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); + + // Get exit code and handle it yourself + 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/pages/13-hook.md b/docs/pages/13-hook.md new file mode 100644 index 0000000..0c5e460 --- /dev/null +++ b/docs/pages/13-hook.md @@ -0,0 +1,107 @@ +<h1 align="center">Hook System</h1> +<p align="center"> + How to insert custom behavior into a program using ProgramHook +</p> + +Hooks let you insert custom logic at various lifecycle points in the pipeline — before dispatch, after chain, before/after render, on program exit … + +You can write cross-cutting concerns (logging, auth, metrics collection) in hooks instead of scattering them across business code. + +## Basic Usage + +`ProgramHook` uses a builder pattern: + +```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()` creates an empty hook, then chain-calls `.on_*()` methods to register the lifecycle nodes you care about. Unregistered nodes won't execute. + +## Lifecycle Nodes + +Hooks cover the full pipeline lifecycle: + +| Stage | Hook | Trigger Point | +| ------------ | ------------------ | ------------------- | +| **Dispatch** | `on_begin` | Execution start | +| | `on_pre_dispatch` | Before dispatch | +| | `on_post_dispatch` | After dispatch | +| **Chain** | `on_pre_chain` | Before chain exec | +| | `on_post_chain` | After chain exec | +| **Render** | `on_pre_render` | Before render exec | +| | `on_post_render` | After render exec | +| **Finish** | `on_finish` | Before program exit | + +Each hook callback receives a corresponding `Hook*Info` struct containing context info (input type, params, render results, etc.). + +## Real Example: Logging Operations + +```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(); + + // Log info before and after each chain execution + 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(); +} +``` + +Run output: + +```text +[hook] executing chain for: EntryGreet +[hook] chain output: ResultName +Hello, World! +``` + +## Controlling Behavior via Hooks + +Hooks aren't just for observation — you can use `ProgramControlUnit` to alter program behavior: + +| Variant | Effect | +| -------------------------- | ----------------------------------------- | +| `Continue` | Do nothing, continue execution | +| `OverrideExitCode(i32)` | Override the exit code | +| `RouteToChain(AnyOutput)` | Replace current data, re-enter Chain loop | +| `RouteToRender(AnyOutput)` | Skip subsequent Chain, render directly | + +> [!NOTE] +> Multiple hooks can be registered and execute in registration order. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/14-testing.md b/docs/pages/14-testing.md new file mode 100644 index 0000000..789da96 --- /dev/null +++ b/docs/pages/14-testing.md @@ -0,0 +1,129 @@ +<h1 align="center">Testing Your Program</h1> +<p align="center"> + Writing unit tests for Chain and Renderer +</p> + +A built-in benefit of the pipeline model is **testability**. + +A Chain is just a function that takes input and returns output; a Renderer is just a function that takes input and writes content — no global state magic, testing is straightforward. + +## Testing Renderer + +Renderer is the easiest to test — call the function, assert the result: + +```rust +@@@pack!(ResultName = String); +// Returns String instead of () +#[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"); +} +``` + +Note the Renderer's return type changed to `-> String` — `#[renderer]` auto-converts `RenderResult` to whatever return type you specify (default is `()`). By returning `String`, you can directly assert on the output content. + +## Testing Chain + +Testing a Chain is slightly more complex because its return value is `Next` (actually `impl Into<ChainProcess<ThisProgram>>`). You'll need the assertion macros provided by the framework: + +```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(); + // Asserts this is a render result (not continuing the chain) + assert_render_result!(chain_process); + // Asserts member_id is ResultName + assert_member_id!(chain_process, ResultName); + // Unpacks the inner value + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +What the three test macros do: + +| Macro | Function | +| ----------------------- | ----------------------------------------------------------------- | +| `assert_render_result!` | Asserts Chain returned the render path (not continuing the chain) | +| `assert_member_id!` | Asserts the return value's member ID is a certain type | +| `unpack_chain_process!` | Unpacks the original type from ChainProcess | + +## Constructing Data with the entry! Macro + +If `extra_macros` is enabled, you can use `entry!` to quickly construct an 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! constructs an Entry from string literals + 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"); +} +``` + +## Testing Resource Injection + +If a Chain uses resources, you need to provide resource instances in the test: + +```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() { + // Resources need to be passed manually in tests + 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"); +} +``` + +The pipeline model makes testing simple: each Chain and Renderer is a relatively independent function — construct input, assert output. + +<p align="center" style="font-size: 0.85em; color: clear;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/2-define-a-dispatcher.md b/docs/pages/2-define-a-dispatcher.md new file mode 100644 index 0000000..804ad1b --- /dev/null +++ b/docs/pages/2-define-a-dispatcher.md @@ -0,0 +1,103 @@ +<h1 align="center">Declare a Dispatcher</h1> +<p align="center"> + Use the <code>dispatcher!</code> macro to declare commands and register them +</p> + +Mingling's pipeline starts with a Dispatcher. + +Its job is simple: **match the user's input command, wrap the arguments into an Entry type**. + +## The `dispatcher!` Macro + +The `dispatcher!` macro generates two types at once: + +| Generated type | Purpose | +| -------------- | -------------------------------------------------------------- | +| `CMDType` | The dispatcher itself, needs to be registered to Program | +| `EntryType` | The entry type, wraps `Vec<String>`, serves as input for Chain | + +The syntax is a fixed three-part pattern: + +```rust +dispatcher!("command path", DispatcherType => EntryType); +``` + +Here's a concrete example: + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); +``` + +> [!NOTE] +> The command name (`"greet"`) is auto-converted to kebab-case. Even if you write `"GreetUser"`, matching will use `greet-user`. + +## Registering with Program + +Once you have a dispatcher, you need to tell Program about it: + +```rust +@@@ dispatcher!("greet", CMDGreet => EntryGreet); +@@@ fn main() { +@@@ let mut program = ThisProgram::new(); +// Register the dispatcher +program.with_dispatcher(CMDGreet); +@@@ } +@@@ gen_program!(); +``` + +> [!TIP] +> If you have many commands, use `with_dispatchers` to register multiple at once: `program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`. + +## Multi-level Commands + +If your program has a hierarchy — e.g., `remote add`, `remote rm` — just separate the command name with dots: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +When the user types `remote add` in the terminal, Mingling matches `remote` and `add` as two levels in sequence. + +## The Entry Type `EntryGreet` + +You might be curious about what's inside `EntryGreet`. It's essentially a struct wrapping `Vec<String>`: + +```rust +// Illustration of code generated by the dispatcher! macro +pub struct EntryGreet { + pub inner: Vec<String>, +} +``` + +When the user types `greet Alice Bob` on the command line, `EntryGreet.inner` becomes `vec!["Alice", "Bob"]`. + +> [!IMPORTANT] +> Entry's `inner` only contains **the remaining args after matching**. +> +> Take `remote add origin` as an example: `remote` and `add` are used for matching the command path, only `origin` goes into `EntryRemoteAdd.inner`. + +## Advanced: Implicit Declaration + +The above is the standard syntax. If you enable the `extra_macros` feature, you can be more concise: + +```rust +// Features: ["extra_macros"] +// Omit CMDType and EntryType, names are auto-derived + dispatcher!("greet"); +// dispatcher!("greet", CMDGreet => EntryGreet); +``` + +This syntax auto-generates `CMDGreet` and `EntryGreet`, with the same effect as the explicit declaration. + +But for the tutorial, we'll stick with explicit syntax — it's clearer and doesn't require extra features. + +See [Feature List](pages/other/features) for details. + +## Next Step + +Next we'll write a Chain to receive the Entry and handle the actual business logic. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md new file mode 100644 index 0000000..c6d3f8e --- /dev/null +++ b/docs/pages/3-define-a-chain.md @@ -0,0 +1,131 @@ +<h1 align="center">Declare a Chain</h1> +<p align="center"> + Use the <code>chain</code> macro to declare a chain and handle Entry input +</p> + +In the previous section, we declared `dispatcher!("greet", CMDGreet => EntryGreet)`. + +Now when a user types `greet`, it gets matched and wrapped into `EntryGreet`. + +But what happens after we get the Entry? + +We need a Chain to process it. + +## The `#[chain]` Macro + +`#[chain]` marks a handler function. The format is straightforward: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // args contains the remaining params after matching user input + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + // Wrap the result into Next, telling the dispatcher where to go next + ResultName::new(name) +} +``` + +Notice anything? + +The Chain function signature declares what it needs — `args: EntryGreet`. + +Then it returns a newtype via `ResultName::new(name)`. + +This returned `Next` expands into `impl Into<ChainProcess<ThisProgram>>`. + +> [!TIP] +> Wondering how `Into<ChainProcess<G>>` works? +> +> Check out the [Any Output Mechanism](pages/concepts/3-any-output) chapter to learn about `ChainProcess`. + +## The `pack!` Macro + +You've probably guessed it — `pack!(ResultName = String)` defines a type that flows through the pipeline: + +```rust +// pack!(ResultName = String) generates code roughly like this + +#[derive(Groupped)] +pub struct ResultName { + pub inner: String, +} +``` + +Think of it as a **tagged** `String`. + +The dispatcher uses this tag for precise routing, ensuring data doesn't get mixed up — e.g., data sent to `RenderGreet` won't be misdelivered to `RenderError`. + +> [!NOTE] +> Unlike a simple type alias (`type`), `pack!` generates a completely new type with its own `TypeId`. + +Here's a recommended naming convention: + +| Role | Naming Pattern | Example | +| ------------ | ---------------------- | -------------------- | +| Entry | `Entry` + command | `EntryGreet` | +| Intermediate | `State` + description | `StateParsedArgs` | +| Result | `Result` + description | `ResultGreetSomeone` | +| Error | `Error` + description | `ErrorUserNotFound` | + +See [Naming Convention](pages/other/naming_rule) for details, but for now just remember: **use `pack!` to give your data a meaningful name**. + +## Extracting Params from Entry + +`EntryGreet`'s `inner` is a `Vec<String>`, which you can freely process inside a Chain: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // Take the first param, or use a default + let name = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + + ResultName::new(name) +} +``` + +If you enable the `parser` feature, you can also use `Picker` for more flexible param extraction — but that's a topic for later. + +## Putting It Together + +Now let's connect the Dispatcher and Chain: + +```rust +// 1. Declare the command +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. Declare the pipeline data type +pack!(ResultName = String); + +// 3. Processing logic +#[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!(); +``` + +But this code isn't complete yet — we only have the Dispatcher and Chain. One last step remains: **rendering the result**. That's what the next chapter, Renderer, covers. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md new file mode 100644 index 0000000..5093a52 --- /dev/null +++ b/docs/pages/4-render-result.md @@ -0,0 +1,142 @@ +<h1 align="center">Rendering Results</h1> +<p align="center"> + Use the <code>#[renderer]</code> macro to declare a renderer and output results +</p> + +Now that we've created a Dispatcher and a Chain, and produced a Result type via `pack!`, there's one final step: **presenting the result to the user**. + +## The `#[renderer]` Macro + +Similar to `#[chain]`, `#[renderer]` marks an output function: + +```rust +pack!(ResultName = String); +#[renderer] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} +``` + +A Renderer takes the result produced by a Chain and outputs it using `r_println!`. What's the difference between `r_println!` and the usual `println!`? + +## The `r_println!` and `r_print!` Macros + +`r_println!` and `r_print!` are printing macros provided by Mingling. They write content into a `RenderResult` instead of printing directly to the terminal. This offers several benefits: + +1. **RenderResult holds an exit code** — you can make the program exit with a specific code +2. **Easier testing** — you can capture rendered output and make assertions +3. **Post-processing** — you can capture results and apply uniform text post-processing + +> [!TIP] +> For simple printing, you can think of it as a drop-in replacement for `println!`. Using `r_println!` instead of `println!` is a safe choice. + +## A Complete Runnable Program + +Putting the content of all three tutorials together, here's your first complete Mingling program: + +```rust +// 1. Declare a command with Dispatcher +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. Declare result data with pack! +pack!(ResultName = String); + +// 3. Handle logic with Chain +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +// 4. Output results with Renderer +#[renderer] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} + +// 5. Assemble and run the program in main +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +// 6. Generate the complete program with gen_program! +gen_program!(); +``` + +## Try It Out + +```bash +~# cargo run -- greet Alice +``` + +```text +Hello, Alice! +``` + +Try without arguments: + +```bash +~# cargo run -- greet +``` + +```text +Hello, World! +``` + +Try an unknown command: + +```bash +cargo run -- great +``` + +```text +# No output! +``` + +## Add a Fallback + +`gen_program!()` auto-generates an `ErrorDispatcherNotFound` type that wraps `Vec<String>` — it holds the user's unmatched input. You just need to write a Renderer for it: + +```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(" ")); + } +} +``` + +With that added, try the unknown command again: + +```bash +cargo run -- great +``` + +```text +Command not found: "great" +``` + +## Congratulations + +You've completed your first full Mingling program! Here's a recap of what you've learned: + +| Concept | Macro/Function | In a Nutshell | +| --------------- | ---------------- | ------------------------------------- | +| Declare command | `dispatcher!` | Tell the program what users can input | +| Handle logic | `#[chain]` | What to do with the arguments | +| Output results | `#[renderer]` | How to present results to users | +| Type wrapper | `pack!` | Give your data a meaningful name | +| Program entry | `gen_program!()` | Auto-generate the pipeline wiring | + +In real projects you'll also use advanced features like resource injection, hooks, autocompletion, REPL, etc., but the core skeleton stays the same: **Dispatcher → Chain → Renderer**. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md new file mode 100644 index 0000000..9ad7ef4 --- /dev/null +++ b/docs/pages/5-multiple-commands.md @@ -0,0 +1,109 @@ +<h1 align="center">Multi-Command Program</h1> +<p align="center"> + Adding multiple commands to a single program +</p> + +Real-world CLIs rarely have just one command. Let's extend our previous greet program by adding a second command, and see what a multi-command program looks like. + +## Adding a Second Command + +Work in the same project: + +```rust +// Declare two commands +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!(); +``` + +Both commands share the same pipeline model, but each has its own path: + +```text +> my-cli greet Alice +Hello, Alice! +> my-cli add 1 2 3 +Sum: 6 +``` + +## Registering Multiple Dispatchers + +Notice `with_dispatchers`? When you need to register multiple dispatchers, just pass them as a tuple: + +```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(); +} +``` + +This is equivalent to registering them one by one, same effect. + +> [!TIP] +> The tuple supports up to 7 dispatchers. For more than 7, chain `with_dispatcher` calls instead. + +## Subcommands + +Multi-level commands work the same way—each dot-separated level is just part of the name: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +Each subcommand's Entry, Chain, and Renderer are completely independent and don't interfere. + +## Type Independence + +Notice we used two different `pack!` macros: + +- `pack!(ResultGreeting = String)` +- `pack!(ResultSum = i32)` + +They are independent types, and `gen_program!()` assigns them different enum variants. + +The dispatcher will never route `ResultGreeting` data to `render_sum` — **type safety is guaranteed from the naming stage**. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md new file mode 100644 index 0000000..7e91af8 --- /dev/null +++ b/docs/pages/6-argument-parse-picker.md @@ -0,0 +1,393 @@ +<h1 align="center">Parsing Arguments with Picker</h1> +<p align="center"> + Use Picker to perform basic argument parsing +</p> + +In previous tutorials, we extracted args manually from `EntryGreet.inner` (`Vec<String>`). + +```rust +@@@ fn main() { +@@@ let args : Vec<String> = vec![]; +let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); +@@@ } +``` + +But this approach doesn't scale well for many params. Mingling provides `Picker` — a chaining API to extract and convert args. + +To enable `Picker`, update your `Cargo.toml`: + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["parser"] +``` + +Now let's look at `Picker` in action: + +```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` implements `pick`, `pick_or`, and `pick_or_route` for any type that can convert to `Vec<String>`: they semantically **pick** args from a string list and convert them to structured data. + +Breaking down the example above: + +```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) +@@@} +``` + +Its semantics are: + +```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(); +// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ unpack to String +// | | | |__________ default value "World" +// | | |______________ pick the first positional arg (no flag) +// | |______________________ pick or use default +// |___________________________ from previous input +@@@} +``` + +## Parsing Flag Args + +If your program needs to parse flag args (e.g., `greet --name Alice`), do this: + +```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) +} +``` + +Its semantics: + +```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(); +// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ unpack to String +// | | | |__________ default value "World" +// | | |____________________________ pick arg after "--name" or "-n" +// | |____________________________________ pick or use default +// |_________________________________________ from previous input +@@@} +``` + +## About `.unpack()` + +You may have noticed that `Picker` calls `.unpack()` at the end of parsing. It converts the accumulated parse results into structured info. + +For a single pick, `.unpack()` returns a single value; for multiple picks, it returns a tuple: + +```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` is very sensitive to parse order, esp. for positional args (they're parsed sequentially). If you need to parse positional args, make sure all **flag args** have been picked and consumed first. + +## Handling Edge Cases with `pick_or_route` + +As the old saying goes: "Never trust your users." To handle missing required args, type mismatches, etc., `pick_or_route` routes the execution chain to a dedicated error-handling type. + +A simple example: + +```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(); + + // Use route! macro to unpack 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); +} +``` + +With `pick_or_route`, the code gets a bit more complex: `.unpack()` no longer returns the value directly, but `Result<Value, Route>`. + +However, Mingling's `extra_macros` feature provides the `route!` macro to simplify unwrapping — it just omits some boilerplate: + +```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!() +@@@ } +``` + +It expands to: + +```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!() +@@@ } +``` + +## Post-processing Extracted Values + +After picking user input, you can use `after` to process the value immediately: + +```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") + // Format immediately after extracting --name + .after(|name: String| { + name.replace(['-', '_', '.'], " ") + .to_lowercase() + .trim() + .to_string() + }) + .unpack(); + + ResultName::new(name) +} +``` + +Similarly, use `after_or_route` to handle format errors in input args: + +```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); +} +``` + +## Boolean Parsing + +`Picker` can parse bools too, with two modes: implicit and explicit. + +| Mode | Format | +| -------- | ----------------------------------- | +| Implicit | `--confirmed` | +| Explicit | `--confirm true` or `--confirm yes` | + +- Using `.pick::<bool>(flag)` → implicit: flag present means `true` +- Using `.pick::<Yes>(flag)` or `.pick::<True>(flag)` → explicit + +Implicit is fine for most cases, but for important confirmations, explicit logic is more semantic. + +```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() +} +``` + +## Special Usage: `usize` Parsing + +Mingling provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc. + +```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); +} +``` + +## Custom Parseable Types + +Implement the `Pickable` trait to make your type parseable by `Picker` — this is where Picker's extensibility comes from. + +```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); +} +``` + +Output: + +```text +~# my-cli connect --addr 127.0.0.1:8080 +Connected: IP: 127.0.0.1 PORT: 8080 +``` + +## Auto-implementing Pickable for Enums + +To implement `Pickable` for an enum, just make it implement `EnumTag`, then implement `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); +} +``` + +That covers all the features of `Picker`. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/7-argument-parse-clap.md b/docs/pages/7-argument-parse-clap.md new file mode 100644 index 0000000..e912c38 --- /dev/null +++ b/docs/pages/7-argument-parse-clap.md @@ -0,0 +1,87 @@ +<h1 align="center">Parsing Arguments with Clap</h1> +<p align="center"> + Use clap for more complex argument parsing +</p> + +Picker is suitable for lightweight arg extraction, but when there are many args, complex validation rules, or you need auto-generated `--help`, you can use [clap](https://crates.io/crates/clap). + +## Enable clap feature + +```toml +[dependencies.mingling] +features = ["clap"] + +[dependencies.clap] +version = "4" +features = ["derive", "color"] +``` + +## dispatcher_clap + +Add `#[dispatcher_clap]` on a `clap::Parser` struct to auto-generate a 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); +} +``` + +## Working with BasicProgramSetup + +If you need `--help` support, register `BasicProgramSetup` in main and set the clap help output mode: + +```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(); +} +``` + +See [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding) for more details. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/8-setup-and-resources.md b/docs/pages/8-setup-and-resources.md new file mode 100644 index 0000000..3858e99 --- /dev/null +++ b/docs/pages/8-setup-and-resources.md @@ -0,0 +1,89 @@ +<h1 align="center">Program Setup</h1> +<p align="center"> + Initialize your program with Setup +</p> + +When a program needs to do some init work at startup—like parsing global args or registering resources—you can organize that logic with `#[program_setup]`. + +## Initialize with Setup + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // Extract global flag from args + 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!(); +``` + +A function annotated with `#[program_setup]` receives `&mut Program<ThisProgram>`, where you can do any init work. + +Register it in `main` via `program.with_setup(...)` to use it. + +> [!NOTE] +> `#[program_setup]` requires the `extra_macros` feature. Without it, you can manually implement the `ProgramSetup` trait. + +## Extract Global Args + +The most common use of Setup is extracting global args. Mingling provides a few helper methods: + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // Boolean flag + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); + + // Flag with a value + program.global_argument("--name", |_program, value| { + // value is "Alice" + let _ = value; + }); +} +``` + +> [!TIP] +> `global_flag` and `global_argument` automatically remove matched args from `program.args`, so they won't enter the pipeline. + +## Built-in Setup + +Mingling provides several ready-to-use Setups covering the most common CLI program needs: + +| Setup | Functionality | +| --------------------------- | ------------------------------------------------------------------------------------------ | +| `BasicProgramSetup` | Parses `--help`/`-h`, `--quiet`/`-q`, `--confirm`/`-C` | +| `DirectoryEnvironmentSetup` | Registers directory resources: current dir, executable dir, home dir, temp dir | +| `ExitCodeSetup` | Controls program exit code via `ResExitCode` | +| `StructuralRendererSetup` | Enables `--json`, `--yaml` etc. structured output (requires `structural_renderer` feature) | + +Usage is just one line in `main`: + +```rust +@@@use mingling::setup::BasicProgramSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` handles common params that most CLI programs need, saving you the trouble of manual parsing. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/9-error-handling.md b/docs/pages/9-error-handling.md new file mode 100644 index 0000000..ec1f422 --- /dev/null +++ b/docs/pages/9-error-handling.md @@ -0,0 +1,120 @@ +<h1 align="center">Error Handling</h1> +<p align="center"> + Gracefully present errors to the user +</p> + +A pipeline isn't just the happy path. When input is invalid, a resource isn't found, or an operation fails, you need a place to handle these "surprises" instead of letting the program panic. + +## Two Paths: Success vs. Error + +Recall the pipeline model: Chain's return value is `Next`, which has two destinations: + +| Route | Meaning | +| -------------- | ------------------------------------------- | +| `.to_render()` | Got a result, hand it to a Renderer to show | +| `.to_chain()` | Not done yet, hand it to the next Chain | + +Error values can also take either path—you can render the error msg directly, or pass it to the next Chain for potential recovery. + +## Distinguish Errors with Dedicated Types + +```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() + } +} +``` + +Then write separate Renderers: + +```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); +} +``` + +Each Renderer does its own job; what the user sees depends on what the Chain returned. + +## Complete Example + +```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!(); +``` + +Output: + +```text +~# my-cli greet Alice +Hello, Alice! + +~# my-cli greet +Error: name is required +``` + +## About `pack_err!` + +If you've enabled `extra_macros`, you can use `pack_err!` to quickly declare an error type with an auto-generated `name` field: + +```rust +// Features: ["extra_macros"] +pack_err!(ErrorNotFound); +// Generates: struct ErrorNotFound { pub name: String } +``` + +See [Feature List](pages/other/features) for details. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/advanced/.name b/docs/pages/advanced/.name new file mode 100644 index 0000000..a8a2c7e --- /dev/null +++ b/docs/pages/advanced/.name @@ -0,0 +1 @@ +Advanced diff --git a/docs/pages/advanced/1-completion.md b/docs/pages/advanced/1-completion.md new file mode 100644 index 0000000..a90c3ce --- /dev/null +++ b/docs/pages/advanced/1-completion.md @@ -0,0 +1,83 @@ +<h1 align="center">Completion</h1> +<p align="center"> + Fully dynamic completion system via the `comp` feature +</p> + +Mingling's completion is **fully dynamic** — no static completion files, suggestions are computed at runtime based on the user's current input. + +## Enable `comp` + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["comp"] + +[build-dependencies.mingling] +features = [ + "comp", + # Enable `builds` for build-time support + "builds" +] +``` + +## How it works + +When the user presses `TAB`, the completion script calls the program's hidden subcommand `__comp`, which dynamically queries the best suggestions based on the provided `ShellContext`. + +This hidden subcommand is auto-generated by `gen_program!()` when the `comp` feature is enabled. Its dispatcher is `CMDCompletion` — you need to add it to your program via `with_dispatcher`. + +Completion flow: + +1. Re-match the user's current input to a `Dispatcher` +2. Call the corresponding `#[completion]` function +3. The function returns a `Suggest` (file completion or a list of suggestions) +4. Notify the shell to display the suggestions + +## Define completions + +Use `#[completion(EntryType)]` to define completion logic for an 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 + } +} +``` + +The `suggest!` macro is a more concise way to write the same thing: + +```rust +// Features: ["comp"] +@@@use mingling::macros::suggest; +@@@fn example() { +suggest! { + "Alice": "Likes to receive messages", + "World" +}; +@@@} +``` + +`ShellContext` holds the user's current input state (`previous_word`, `current_word`, `all_words`, etc.). `Suggest` has two variants: `Suggest::Suggest(list)` returns a suggestion list, `Suggest::FileCompletion` delegates file completion to the shell. + +## Generate completion scripts + +Call `build_comp_scripts` in `build.rs` to generate completion scripts (requires `builds` + `comp` features). + +See [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/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md new file mode 100644 index 0000000..09c86d1 --- /dev/null +++ b/docs/pages/advanced/2-structural-renderer.md @@ -0,0 +1,120 @@ +<h1 align="center">Structural Rendering</h1> +<p align="center"> + Use the <code>structural_renderer</code> feature to render output as serialized text +</p> + +With `structural_renderer` enabled, your program can switch output to a structured format via `--json`, `--yaml`, etc., making it easy to integrate with other tools. + +## Enabling the Feature + +```toml +[dependencies.mingling] +features = ["structural_renderer"] +``` + +`structural_renderer` automatically enables `json_serde_fmt`. + +For more formats, enable `structural_renderer_full` (includes JSON, YAML, TOML, RON). + +> [!NOTE] +> To customize output types, see [Features](./pages/other/features) + +## Basic Usage + +After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pack!` to declare types that support structured output: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::setup::StructuralRendererSetup; +@@@dispatcher!("render", CMDRender => EntryRender); + +// pack_structural! is equivalent to 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); +} +``` + +Output: + +```text +~# my-cli render Bob 22 +("Bob", 22) + +~# my-cli render Bob 22 --json +{"inner":["Bob",22]} +``` + +When the user passes `--json`, the framework automatically serializes the render result as JSON — no business logic changes needed. + +## Customizing Output Structure + +The default output from `pack_structural!` includes an `inner` field. For full control over the output structure, define the type manually with `#[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!(); +``` + +Now `--json` outputs: + +```json +{ "name": "Bob", "age": 22 } +``` + +## Notes + +- Supported formats: JSON, YAML, TOML, RON (depends on enabled features) +- `StructuralRendererSetup` registers global params like `--json`, `--yaml`, `--toml`, `--ron` + +> [!NOTE] +> Each type still needs an **empty Renderer**, otherwise that type **is not considered renderable** + +See [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/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 @@ +<h1 align="center">The Pipeline</h1> +<p align="center"> + How Mingling executes commands, step by step +</p> + +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<br/>match cmd → Entry"] + C --> D["Hook: post_dispatch"] + D --> E{"user_context.help?"} + E -->|"true"| F["render_help<br/>skip to help rendering"] + E -->|"false"| G{"has_chain?"} + G -->|"yes"| H["Hook: pre_chain"] + H --> I["do_chain<br/>run business logic"] + 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 -->|"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)<br/>return wrapped Entry"] + M -->|"no match"| NF["build_dispatcher_not_found<br/>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<br/>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. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">Resource System</h1> +<p align="center"> + How Mingling Manages Global State +</p> + +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<T>`. 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. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">AnyOutput Mechanism</h1> +<p align="center"> + How AnyOutput and ChainProcess work +</p> + +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<G> +├── inner: Box<dyn Any + Send> ← 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<G>` adds **routing info**: + +``` +ChainProcess<G> +├── Ok(AnyOutput<G>, 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<G> { + 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<dyn Any>` 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. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> 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 @@ +<h1 align="center">About ProgramCollect</h1> +<p align="center"> + Understand how gen_program!() builds a program +</p> + +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<G>` — 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<GeneratedEnum>`. 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<Box<dyn Dispatcher>>` 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`. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/other/features.md b/docs/pages/other/features.md index 891083d..813ccdd 100644 --- a/docs/pages/other/features.md +++ b/docs/pages/other/features.md @@ -39,13 +39,12 @@ Enables scripts needed for use in `build.rs`, currently including: 1. Completion script generation under the `comp` feature: ```rust +// BUILD TIME // Features: ["builds", "comp"] use mingling::build::build_comp_scripts; -fn main() { - // Generate completion scripts for `myprogram` - build_comp_scripts("myprogram").unwrap(); -} +// Generate completion scripts for `myprogram` +build_comp_scripts("myprogram").unwrap(); ``` ## Feature `clap` diff --git a/docs/pages/other/naming_rule.md b/docs/pages/other/naming_rule.md index 4f4efe8..21d7947 100644 --- a/docs/pages/other/naming_rule.md +++ b/docs/pages/other/naming_rule.md @@ -33,9 +33,9 @@ Setups are initialization steps executed at program startup, registered via `wit Name + Setup ``` -| Example | Description | -| ---------------------- | ---------------------------------------------------------- | -| `BasicSetup` | Basic initialization (`--quiet`, `--help`, `--confirm`) | +| Example | Description | +| ------------------------- | ------------------------------------------------------------- | +| `BasicSetup` | Basic initialization (`--quiet`, `--help`, `--confirm`) | | `StructuralRendererSetup` | structural renderer initialization (`--json`, `--yaml`, etc.) | ### Dispatcher @@ -148,9 +148,13 @@ Error + Description | Resource (mutable) | `counter`, `cache`, `session`, etc. | ```rust -// NOT VERIFIED +@@@ pack!(EntryRemoteAdd = Vec<String>); +@@@ #[derive(Default, Clone)] +@@@ struct ResDatabase { } +@@@ #[derive(Default, Clone)] +@@@ struct ResCurrentDir { } #[chain] -fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResDatabase) -> Next { +fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResDatabase) { // args: entry data // cwd: injected immutable resource // db: injected mutable resource @@ -162,37 +166,43 @@ fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResData ## Complete Example ```rust -// NOT VERIFIED +@@@ #[derive(Default, Clone)] +@@@ struct ResDatabase { } +@@@ impl ResDatabase { fn has_remote(&self, remote: &String) -> bool { true } } +@@@ pack!(StateOperationRemotes = String); +@@@ pack!(ResultRemoteAdded = String); +@@@ pack!(ErrorRepositoryNotFound = String); // Dispatcher dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); // Entry → State #[chain] fn handle_remote_add(args: EntryRemoteAdd) -> Next { - StateOperationRemotes::new(...).to_chain() + StateOperationRemotes::default().to_chain() } // State → Error or Result #[chain] fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase) -> Next { - if db.has_remote(&state.name) { - ErrorRepositoryNotFound::new(...).to_render() + if db.has_remote(&state.inner) { + ErrorRepositoryNotFound::new(state.inner).to_render() } else { - ResultRemoteAdded::new(...).to_render() + ResultRemoteAdded::new(state.inner).to_render() } } // Result rendering #[renderer] fn render_remote_added(result: ResultRemoteAdded) { - r_println!("Remote added: {}", result.name); + r_println!("Remote added: {}", result.inner); } // Error rendering #[renderer] fn render_error_repository_not_found(err: ErrorRepositoryNotFound) { - r_println!("Error: remote '{}' not found", err.name); + r_println!("Error: remote '{}' not found", err.inner); } + ``` <p align="center" style="font-size: 0.85em; color: gray;"> |
