diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-17 05:49:19 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-17 05:49:19 +0800 |
| commit | 57c53affe3542cb6bd4e79ee4c18f20a1bd76b2d (patch) | |
| tree | 1cd4aef44cb7a45a8cd9d520b598f5f181e24c76 /docs/pages | |
| parent | ef23cd944402939605c78a4a853ef6e33af02c21 (diff) | |
refactor!: replace pack! macros with derive-based pipeline types
Remove the `pack!`, `pack_err!`, `pack_structural!`, and
`pack_err_structural!` macros, replacing all pipeline type definitions
with `#[derive(Grouped)]` and `#[derive(Grouped, Wrap)]` attributes.
This changes the generated struct shape from named-field structs with an
`inner` field to tuple structs accessed via `.0`, and removes the
auto-generated `name` and `info` fields from error types.
Diffstat (limited to 'docs/pages')
| -rw-r--r-- | docs/pages/11-resource-system.md | 11 | ||||
| -rw-r--r-- | docs/pages/12-exit-code.md | 3 | ||||
| -rw-r--r-- | docs/pages/13-hook.md | 5 | ||||
| -rw-r--r-- | docs/pages/14-testing.md | 39 | ||||
| -rw-r--r-- | docs/pages/2-define-a-dispatcher.md | 10 | ||||
| -rw-r--r-- | docs/pages/3-define-a-chain.md | 54 | ||||
| -rw-r--r-- | docs/pages/4-render-result.md | 35 | ||||
| -rw-r--r-- | docs/pages/5-multiple-commands.md | 20 | ||||
| -rw-r--r-- | docs/pages/6-argument-parse-picker.md | 64 | ||||
| -rw-r--r-- | docs/pages/9-error-handling.md | 40 | ||||
| -rw-r--r-- | docs/pages/advanced/2-structural-renderer.md | 20 | ||||
| -rw-r--r-- | docs/pages/concepts/2-resource.md | 3 | ||||
| -rw-r--r-- | docs/pages/concepts/3-any-output.md | 6 | ||||
| -rw-r--r-- | docs/pages/concepts/4-program-collect.md | 2 | ||||
| -rw-r--r-- | docs/pages/other/features.md | 59 | ||||
| -rw-r--r-- | docs/pages/other/naming_rule.md | 24 |
16 files changed, 226 insertions, 169 deletions
diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md index 3a4dc36..ef0d7b8 100644 --- a/docs/pages/11-resource-system.md +++ b/docs/pages/11-resource-system.md @@ -31,11 +31,12 @@ In a Chain or Renderer, simply declare the resource in the parameter list: @@@#[derive(Default, Clone)] @@@struct ResCurrentDir(String); @@@dispatcher!("pwd", EntryPrintWorkingDir); -@@@pack!(ResultPath = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultPath(String); // Inject read-only resource via &T #[chain] fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next { - ResultPath::new(cwd.0.clone()).to_render() + ResultPath(cwd.0.clone()).to_render() } #[renderer(buffer)] @@ -53,7 +54,8 @@ Use `&mut T` to inject a mutable resource: @@@#[derive(Default, Clone)] @@@struct ResVisitCount(u32); @@@dispatcher!("visit", EntryVisit); -@@@pack!(ResultDone = ()); +@@@#[derive(Grouped, Wrap, Default)] +@@@pub struct ResultDone(()); #[chain] fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { counter.0 += 1; @@ -74,7 +76,8 @@ A Chain can inject any number of resources at once — the framework matches the @@@#[derive(Default, Clone)] struct ResConfig(String); @@@#[derive(Default, Clone)] struct ResCounter(u32); @@@dispatcher!("test", EntryTest); -@@@pack!(ResultDone = ()); +@@@#[derive(Grouped, Wrap, Default)] +@@@pub struct ResultDone(()); // Inject both read-only and mutable resources #[chain] fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next { diff --git a/docs/pages/12-exit-code.md b/docs/pages/12-exit-code.md index 6828cde..8fa320c 100644 --- a/docs/pages/12-exit-code.md +++ b/docs/pages/12-exit-code.md @@ -29,7 +29,8 @@ 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>); +@@@#[derive(Grouped, Wrap)] +@@@pub struct EntryCheck(Vec<String>); #[chain] fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) { // Modify exit code when check fails diff --git a/docs/pages/13-hook.md b/docs/pages/13-hook.md index 90df379..6eefd12 100644 --- a/docs/pages/13-hook.md +++ b/docs/pages/13-hook.md @@ -55,10 +55,11 @@ Each hook callback receives a corresponding `Hook*Info` struct containing contex @@@use mingling::hook::ProgramHook; @@@ @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); @@@ @@@#[chain] fn handle_greet(args: EntryGreet) -> Next { -@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render() +@@@ ResultName(args.0.first().cloned().unwrap_or_default()).to_render() @@@} @@@#[renderer] fn render_name(r: ResultName) -> RenderResult { RenderResult::new() } fn main() { diff --git a/docs/pages/14-testing.md b/docs/pages/14-testing.md index 9f6b6ed..f0413a7 100644 --- a/docs/pages/14-testing.md +++ b/docs/pages/14-testing.md @@ -12,7 +12,8 @@ A Chain is just a function that takes input and returns output; a Renderer is ju Renderer is the easiest to test — call the function, assert the result: ```rust -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[renderer] fn render_greet(result: ResultName) -> RenderResult { let mut r = RenderResult::new(); @@ -22,7 +23,7 @@ fn render_greet(result: ResultName) -> RenderResult { #[test] fn test_render_name() { - let result = render_name(ResultName::new("Alice".to_string())); + let result = render_name(ResultName("Alice".to_string())); assert_eq!(result.to_string().as_str(), "Hello, Alice!\n"); } ``` @@ -36,27 +37,29 @@ Testing a Chain is slightly more complex because its return value is `Next` (act ```rust @@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process}; @@@dispatcher!("hello", EntryHello); -@@@pack!(ResultName = String); -@@@pack!(ErrorNoName = ()); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); +@@@#[derive(Grouped, Wrap, Default)] +@@@pub struct ErrorNoName(()); @@@#[chain] @@@fn handle_hello(args: EntryHello) -> Next { -@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ let name = args.0.first().cloned().unwrap_or_default(); @@@ if name.is_empty() { @@@ ErrorNoName::default().to_render() @@@ } else { -@@@ ResultName::new(name).to_render() +@@@ ResultName(name).to_render() @@@ } @@@} #[test] fn test_handle_hello_with_name() { - let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into(); + let chain_process = handle_hello(EntryGreet(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"); + assert_eq!(result_name.0, "Alice"); } ``` @@ -78,11 +81,12 @@ If `extras` is enabled, you can use `entry!` to quickly construct an Entry: @@@use mingling::{assert_member_id, unpack_chain_process}; @@@use mingling::macros::entry; @@@dispatcher!("hello", EntryHello); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); @@@#[chain] @@@fn handle_hello(args: EntryHello) -> Next { -@@@ let name = args.inner.first().cloned().unwrap_or_default(); -@@@ ResultName::new(name).to_render() +@@@ let name = args.0.first().cloned().unwrap_or_default(); +@@@ ResultName(name).to_render() @@@} #[test] fn test_with_entry_macro() { @@ -90,7 +94,7 @@ fn test_with_entry_macro() { 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"); + assert_eq!(result_name.0, "Alice"); } ``` @@ -103,23 +107,24 @@ If a Chain uses resources, you need to provide resource instances in the test: @@@#[derive(Default, Clone)] @@@struct ResPrefix(String); @@@dispatcher!("hello", EntryHello); -@@@pack!(ResultGreeting = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct 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() + let name = args.0.first().cloned().unwrap_or_default(); + ResultGreeting(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()]), + EntryHello(vec!["World".to_string()]), &ResPrefix("Hello".to_string()), ); let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram); - assert_eq!(greeting.inner, "Hello, World"); + assert_eq!(greeting.0, "Hello, World"); } ``` diff --git a/docs/pages/2-define-a-dispatcher.md b/docs/pages/2-define-a-dispatcher.md index efd744d..5aafb19 100644 --- a/docs/pages/2-define-a-dispatcher.md +++ b/docs/pages/2-define-a-dispatcher.md @@ -48,17 +48,15 @@ You might be curious about what's inside `EntryGreet`. It's essentially a struct ```rust // Illustration of code generated by the dispatcher! macro -pub struct EntryGreet { - pub inner: Vec<String>, -} +pub struct EntryGreet(pub Vec<String>); ``` -When the user types `greet Alice Bob` on the command line, `EntryGreet.inner` becomes `vec!["Alice", "Bob"]`. +When the user types `greet Alice Bob` on the command line, `EntryGreet`'s `.0` becomes `vec!["Alice", "Bob"]`. > [!IMPORTANT] -> Entry's `inner` only contains **the remaining args after matching**. +> Entry's `.0` 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`. +> Take `remote add origin` as an example: `remote` and `add` are used for matching the command path, only `origin` goes into `EntryRemoteAdd.0`. ## Advanced: Implicit Declaration diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md index 72ade94..ea39f6b 100644 --- a/docs/pages/3-define-a-chain.md +++ b/docs/pages/3-define-a-chain.md @@ -17,14 +17,15 @@ We need a Chain to process it. ```rust @@@dispatcher!("greet", EntryGreet); -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct 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()); + let name = args.0.first().cloned().unwrap_or_else(|| "World".to_string()); // Wrap the result into Next, telling the dispatcher where to go next - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -32,7 +33,7 @@ Notice anything? The Chain function signature declares what it needs — `args: EntryGreet`. -Then it returns a newtype via `ResultName::new(name)`. +Then it returns a newtype via `ResultName(name)`. This returned `Next` expands into `impl Into<ChainProcess<ThisProgram>>`. @@ -41,17 +42,30 @@ This returned `Next` expands into `impl Into<ChainProcess<ThisProgram>>`. > > Check out the [Any Output Mechanism](pages/concepts/3-any-output) chapter to learn about `ChainProcess`. -## The `pack!` Macro +## Declaring Types with `#[derive(Grouped, Wrap)]` -You've probably guessed it — `pack!(ResultName = String)` defines a type that flows through the pipeline: +You've probably guessed it — `#[derive(Grouped, Wrap)] pub struct ResultName(String);` defines a type that flows through the pipeline: ```rust -// pack!(ResultName = String) generates code roughly like this +// #[derive(Grouped, Wrap)] generates code roughly like this -#[derive(Grouped)] -pub struct ResultName { - pub inner: String, +pub struct ResultName(String); + +impl From<String> for ResultName { + fn from(inner: String) -> Self { + ResultName(inner) + } +} + +impl std::ops::Deref for ResultName { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } } + +// Grouped generates member_id() → ThisProgram::ResultName, +// giving the type its routing identity and Into<ChainProcess> conversion. ``` Think of it as a **tagged** `String`. @@ -59,7 +73,7 @@ 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`. +> Unlike a simple type alias (`type`), `#[derive(Grouped, Wrap)]` declares a completely new type with its own `TypeId`. Here's a recommended naming convention: @@ -70,25 +84,26 @@ Here's a recommended naming convention: | 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**. +See [Naming Convention](pages/other/naming_rule) for details, but for now just remember: **use `#[derive(Grouped)]` (optionally with `Wrap`) 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: +`EntryGreet`'s `.0` is a `Vec<String>`, which you can freely process inside a Chain: ```rust @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { // Take the first param, or use a default let name = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -103,16 +118,17 @@ Now let's connect the Dispatcher and Chain: dispatcher!("greet", EntryGreet); // 2. Declare the pipeline data type -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); // 3. Processing logic #[chain] fn handle_greet(args: EntryGreet) -> Next { - let name = args.inner + let name = args.0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name).into() + ResultName(name).into() } fn main() { diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md index fdf8b12..70ffc96 100644 --- a/docs/pages/4-render-result.md +++ b/docs/pages/4-render-result.md @@ -3,7 +3,7 @@ Declare a renderer using the <code>#[renderer]</code> macro to output results. </p> -Now we've created a Dispatcher and a Chain, and produced a Result type via `pack!`. The final step: **present the result to the user**. +Now we've created a Dispatcher and a Chain, and produced a Result type via `#[derive(Grouped, Wrap)]`. The final step: **present the result to the user**. ## The `#[renderer]` Macro @@ -11,7 +11,8 @@ Similar to `#[chain]`, `#[renderer]` marks a function that produces output: ```rust @@@use mingling::macros::buffer; -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[renderer(buffer)] fn render_name(name: ResultName) { r_println!("Hello, {}!", *name); @@ -27,7 +28,8 @@ If you find explicitly creating and returning a `RenderResult` too verbose, you ```rust use mingling::macros::buffer; -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[renderer(buffer)] fn render_name(name: ResultName) { r_println!("Hello, {}!", *name); @@ -54,17 +56,18 @@ use mingling::macros::buffer; // 1. Declare commands with a Dispatcher dispatcher!("greet", EntryGreet); -// 2. Declare result data with pack! -pack!(ResultName = String); +// 2. Declare result data with #[derive(Grouped, Wrap)] +#[derive(Grouped, Wrap)] +pub struct ResultName(String); // 3. Handle logic with a Chain #[chain] fn handle_greet(args: EntryGreet) -> Next { - let name = args.inner + let name = args.0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name).into() + ResultName(name).into() } // 4. Output results with a Renderer @@ -122,10 +125,10 @@ use mingling::macros::buffer; #[renderer(buffer)] fn render_entry_fallback(err: EntryFallback) { - if err.inner.is_empty() { + if err.0.is_empty() { r_println!("Unknown command"); } else { - r_println!("Command not found: \"{}\"", err.inner.join(" ")); + r_println!("Command not found: \"{}\"", err.0.join(" ")); } } ``` @@ -144,13 +147,13 @@ Command not found: "great" You've completed your first full Mingling program! Let's recap what you've learned: -| Concept | Macro / Function | One-liner | -| -------------- | ---------------- | --------------------------------------- | -| Declare cmds | `dispatcher!` | Tell the program what the user can type | -| Handle logic | `#[chain]` | What to do when args are received | -| Output results | `#[renderer]` | How to present results to the user | -| Type wrapping | `pack!` | Give your data a meaningful name | -| Program entry | `gen_program!()` | Auto-generate the pipeline wiring | +| Concept | Macro / Function | One-liner | +| -------------- | -------------------------- | --------------------------------------- | +| Declare cmds | `dispatcher!` | Tell the program what the user can type | +| Handle logic | `#[chain]` | What to do when args are received | +| Output results | `#[renderer]` | How to present results to the user | +| Type wrapping | `#[derive(Grouped, Wrap)]` | 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, completions, REPL, etc., but the core skeleton stays the same: **Dispatcher → Chain → Renderer**. diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md index a5c09b0..9979cd1 100644 --- a/docs/pages/5-multiple-commands.md +++ b/docs/pages/5-multiple-commands.md @@ -15,19 +15,21 @@ Work in the same project: dispatcher!("greet", EntryGreet); dispatcher!("add", EntryAdd); -pack!(ResultGreeting = String); -pack!(ResultSum = i32); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); +#[derive(Grouped, Wrap)] +pub struct ResultSum(i32); #[chain] fn handle_greet(args: EntryGreet) -> Next { - let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(name).into() + let name = args.0.first().cloned().unwrap_or_else(|| "World".to_string()); + ResultGreeting(name).into() } #[chain] fn handle_add(args: EntryAdd) -> Next { - let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum(); - ResultSum::new(sum).into() + let sum: i32 = args.0.iter().filter_map(|s| s.parse::<i32>().ok()).sum(); + ResultSum(sum).into() } #[renderer(buffer)] @@ -70,10 +72,10 @@ Each subcommand's Entry, Chain, and Renderer are completely independent and don' ## Type Independence -Notice we used two different `pack!` macros: +Notice we used two different `#[derive(Grouped, Wrap)]` structs: -- `pack!(ResultGreeting = String)` -- `pack!(ResultSum = i32)` +- `#[derive(Grouped, Wrap)] pub struct ResultGreeting(String);` +- `#[derive(Grouped, Wrap)] pub struct ResultSum(i32);` They are independent types, and `gen_program!()` assigns them different enum variants. diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md index da0fff1..9fe74eb 100644 --- a/docs/pages/6-argument-parse-picker.md +++ b/docs/pages/6-argument-parse-picker.md @@ -3,7 +3,7 @@ Use Picker to perform basic argument parsing </p> -In previous tutorials, we manually extracted parameters from `EntryGreet.inner` (`Vec<String>`). +In previous tutorials, we manually extracted parameters from `EntryGreet.0` (`Vec<String>`). ```rust @@@ fn main() { @@ -27,14 +27,15 @@ Now let's see how `Picker` is written: ```rust // Features: ["picker"] @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev .pick_or(&arg![String], || "World".to_string()) .unwrap(); - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -45,13 +46,14 @@ For the code above: ```rust // Features: ["picker"] @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); @@@#[chain] @@@fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev .pick_or(&arg![String], || "World".to_string()) .unwrap(); -@@@ResultName::new(name).into() +@@@ResultName(name).into() @@@} ``` @@ -60,7 +62,8 @@ Its semantics are: ```rust // Features: ["picker"] @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); @@@#[chain] @@@fn handle_greet_entry(prev: EntryGreet) { @@@let name: String = @@ -81,14 +84,15 @@ If your program needs to parse flag arguments (e.g. `greet --name Alice`), decla ```rust // Features: ["picker"] @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev .pick_or(&arg![name: String, 'n'], || "World".to_string()) .unwrap(); - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -99,7 +103,8 @@ Its semantics: ```rust // Features: ["picker"] @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); @@@#[chain] @@@fn handle_greet_entry(prev: EntryGreet) { @@@let name: String = @@ -122,7 +127,8 @@ For a single pick, `.unwrap()` returns the value directly; for multiple picks, i ```rust // Features: ["picker"] @@@dispatcher!("test", EntryTest); -@@@pack!(ResultInfo = (String, u8, u32)); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultInfo((String, u8, u32)); #[chain] fn handle_test_entry(prev: EntryTest) -> Next { @@ -132,7 +138,7 @@ fn handle_test_entry(prev: EntryTest) -> Next { .pick_or_default(&arg![id: u32, 'I']) .unwrap(); - ResultInfo::new((name, age, id)).into() + ResultInfo((name, age, id)).into() } ``` @@ -150,8 +156,10 @@ Here's a simple example: @@@use mingling::macros::buffer; @@@use mingling::macros::route; @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); -@@@pack!(ErrorNoName = ()); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); +@@@#[derive(Grouped, Wrap, Default)] +@@@pub struct ErrorNoName(()); #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { @@ -162,7 +170,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { }) .to_result() ); - ResultName::new(name).into() + ResultName(name).into() } #[renderer(buffer)] @@ -177,12 +185,13 @@ However, **Mingling**'s `extras` feature provides the `route!` macro for simplif ```rust // Features: ["picker", "extras"] -@@@ pack!(ErrorFail = ()); +@@@ #[derive(Grouped, Wrap)] +@@@ pub struct ErrorFail(()); @@@ use mingling::macros::route; @@@ use mingling::picker::IntoPicker; @@@ fn func() -> mingling::ChainProcess<ThisProgram> { @@@ let args: Vec<String> = vec![]; -let name = route!(args.pick_or_route(&arg![String], || ErrorFail::new(()).to_chain()).to_result()); +let name = route!(args.pick_or_route(&arg![String], || ErrorFail(()).to_chain()).to_result()); @@@ mingling::macros::empty_result!() @@@ } ``` @@ -191,11 +200,12 @@ It expands to: ```rust // Features: ["picker", "extras"] -@@@ pack!(ErrorFail = ()); +@@@ #[derive(Grouped, Wrap)] +@@@ pub struct ErrorFail(()); @@@ use mingling::picker::IntoPicker; @@@ fn func() -> mingling::ChainProcess<ThisProgram> { @@@ let args: Vec<String> = vec![]; -let name = match args.pick_or_route(&arg![String], || ErrorFail::new(()).to_chain()).to_result() { +let name = match args.pick_or_route(&arg![String], || ErrorFail(()).to_chain()).to_result() { Ok(r) => r, Err(e) => return e, }; @@ -210,7 +220,8 @@ After picking user input with `pick`, you can use `post` to process it immediate ```rust // Features: ["picker"] @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[chain] fn handle_greet_entry(prev: EntryGreet) -> Next { @@ -225,7 +236,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { }) .unwrap(); - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -237,7 +248,8 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { // Features: ["picker"] @@@use mingling::picker::value::Flag; @@@dispatcher!("test", EntryTest); -@@@pack!(ResultDone = ()); +@@@#[derive(Grouped, Wrap, Default)] +@@@pub struct ResultDone(()); #[chain] fn handle_entry(prev: EntryTest) -> Next { @@ -280,12 +292,13 @@ impl SinglePickable for Address { } } @@@dispatcher!("connect", EntryConnect); -@@@pack!(ResultConnected = Address); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultConnected(Address); #[chain] fn handle_connect_entry(prev: EntryConnect) -> Next { let address: Address = prev.pick_or_default(&arg![Address]).unwrap(); - ResultConnected::new(address).into() + ResultConnected(address).into() } #[renderer(buffer)] @@ -333,12 +346,13 @@ impl SinglePickable for Fruits { } } @@@dispatcher!("eat", EntryEat); -@@@pack!(ResultFruit = Fruits); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultFruit(Fruits); #[chain] fn handle_eat_entry(prev: EntryEat) -> Next { let fruit: Fruits = prev.pick_or_default(&arg![Fruits]).unwrap(); - ResultFruit::new(fruit).into() + ResultFruit(fruit).into() } #[renderer(buffer)] diff --git a/docs/pages/9-error-handling.md b/docs/pages/9-error-handling.md index eefc0f0..0cacf01 100644 --- a/docs/pages/9-error-handling.md +++ b/docs/pages/9-error-handling.md @@ -20,17 +20,19 @@ Error values can also take either path—you can render the error msg directly, ```rust @@@dispatcher!("greet", EntryGreet); -pack!(ResultGreeting = String); -pack!(ErrorNameEmpty = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); +#[derive(Grouped, Wrap)] +pub struct ErrorNameEmpty(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { - let name = args.inner.first().cloned().unwrap_or_default(); + let name = args.0.first().cloned().unwrap_or_default(); if name.is_empty() { - ErrorNameEmpty::new("name is required".to_string()).to_render() + ErrorNameEmpty("name is required".to_string()).to_render() } else { - ResultGreeting::new(name).to_render() + ResultGreeting(name).to_render() } } ``` @@ -40,9 +42,11 @@ Then write separate Renderers: ```rust @@@use mingling::macros::buffer; @@@dispatcher!("greet", 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() } +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultGreeting(String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ErrorNameEmpty(String); +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting(args.0.first().cloned().unwrap_or_default()).to_render() } #[renderer(buffer)] fn render_greet(result: ResultGreeting) { @@ -63,16 +67,18 @@ Each Renderer does its own job; what the user sees depends on what the Chain ret @@@use mingling::macros::buffer; dispatcher!("greet", EntryGreet); -pack!(ResultGreeting = String); -pack!(ErrorNameEmpty = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); +#[derive(Grouped, Wrap)] +pub struct ErrorNameEmpty(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { - let name = args.inner.first().cloned().unwrap_or_default(); + let name = args.0.first().cloned().unwrap_or_default(); if name.is_empty() { - ErrorNameEmpty::new("name is required".to_string()).to_render() + ErrorNameEmpty("name is required".to_string()).to_render() } else { - ResultGreeting::new(name).to_render() + ResultGreeting(name).to_render() } } @@ -104,14 +110,14 @@ Hello, Alice! Error: name is required ``` -## About `pack_err!` +## Declaring Error Types -If you've enabled `extras`, you can use `pack_err!` to quickly declare an error type with an auto-generated `name` field: +You can use `#[derive(Grouped, Default)]` to quickly declare an error type with no payload: ```rust // Features: ["extras"] -pack_err!(ErrorNotFound); -// Generates: struct ErrorNotFound { pub name: String } +#[derive(Grouped, Default)] +pub struct ErrorNotFound; ``` See [Feature List](pages/other/features) for details. diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md index e444ee6..23c7ec3 100644 --- a/docs/pages/advanced/2-structural-renderer.md +++ b/docs/pages/advanced/2-structural-renderer.md @@ -21,7 +21,7 @@ For more formats, enable `structural_renderer_full` (includes JSON, YAML, TOML, ## Basic Usage -After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pack!` to declare types that support structured output: +After enabling `StructuralRendererSetup`, use `#[derive(StructuralData, Grouped, Wrap)]` to declare types that support structured output: ```rust // Features: ["structural_renderer"] @@ -29,16 +29,18 @@ After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pac // serde = "1" @@@use mingling::macros::buffer; @@@use mingling::setup::StructuralRendererSetup; +@@@use mingling::StructuralData; @@@dispatcher!("render", EntryRender); -// pack_structural! is equivalent to pack! + StructuralData -pack_structural!(ResultInfo = (String, i32)); +// StructuralData + Grouped + Wrap gives the type structured output support +#[derive(serde::Serialize, StructuralData, Grouped, Wrap)] +pub struct 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)).into() + let name = args.0.first().cloned().unwrap_or_default(); + let age = args.0.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + ResultInfo((name, age)).into() } #[renderer(buffer)] @@ -61,7 +63,7 @@ When the user passes `--json`, the framework automatically serializes the render ## 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, Grouped)]`: +The default output from a tuple newtype (e.g. `#[derive(StructuralData, Grouped, Wrap)]`) wraps the value under an `inner` key. For full control over the output structure, define the type manually with `#[derive(StructuralData, Serialize, Grouped)]`: ```rust // Features: ["structural_renderer"] @@ -82,8 +84,8 @@ struct Info { #[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); + let name = args.0.first().cloned().unwrap_or_default(); + let age = args.0.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); Info { name, age }.to_render() } diff --git a/docs/pages/concepts/2-resource.md b/docs/pages/concepts/2-resource.md index ad7ee16..6f0ef99 100644 --- a/docs/pages/concepts/2-resource.md +++ b/docs/pages/concepts/2-resource.md @@ -34,7 +34,8 @@ For example: ```rust @@@ use mingling::res::ResExitCode; -@@@ pack!(ErrorFileNotFound = ()); +@@@ #[derive(Grouped, Wrap)] +@@@ pub struct ErrorFileNotFound(()); #[chain] fn handle_error_file_not_found( error: ErrorFileNotFound, diff --git a/docs/pages/concepts/3-any-output.md b/docs/pages/concepts/3-any-output.md index f02805f..2b07906 100644 --- a/docs/pages/concepts/3-any-output.md +++ b/docs/pages/concepts/3-any-output.md @@ -20,7 +20,7 @@ AnyOutput<G> Here `G` is the program enum generated by `gen_program!()` (i.e., `ThisProgram` as you know it). -Each type annotated with `pack!` or `#[derive(Grouped)]` is assigned to one variant of this enum. +Each type annotated with `#[derive(Grouped)]` (or `#[derive(Grouped, Wrap)]`) is assigned to one variant of this enum. ## ChainProcess: Data + Routing @@ -48,7 +48,7 @@ trait Grouped<G> { } ``` -When you use `pack!(ResultName = String)`, the macro automatically implements `Grouped` for `ResultName`, and `member_id()` returns the corresponding enum variant. The dispatcher looks at `member_id` and finds the matching Chain or Renderer. +When you write `#[derive(Grouped)]` on `ResultName`, the derive automatically implements `Grouped` 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. @@ -66,7 +66,7 @@ This mechanism ensures **type safety**: the dispatch code generated by `gen_prog > [!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. +> Macros and derives like `#[derive(Grouped, Wrap)]`, `#[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 diff --git a/docs/pages/concepts/4-program-collect.md b/docs/pages/concepts/4-program-collect.md index c5203c3..bc1fbc9 100644 --- a/docs/pages/concepts/4-program-collect.md +++ b/docs/pages/concepts/4-program-collect.md @@ -9,7 +9,7 @@ Every Mingling program ends with a `gen_program!()` call. Behind the scenes, it ### 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. +Scans the current module for all types marked with `#[derive(Grouped)]`, `#[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. diff --git a/docs/pages/other/features.md b/docs/pages/other/features.md index 3779390..b2c0ea5 100644 --- a/docs/pages/other/features.md +++ b/docs/pages/other/features.md @@ -80,11 +80,12 @@ Enables async runtime support, allowing `#[chain]` to bind `async` functions, e. ```rust // Features: ["async"] -pack!(StateFoo = ()); +#[derive(Grouped, Wrap)] +pub struct StateFoo(()); #[chain] async fn handle_state_foo(foo: StateFoo) -> Next { - StateFoo::new(()).into() + StateFoo(()).into() } ``` @@ -151,14 +152,13 @@ Enables an additional set of macros, providing more convenient syntactic sugar a For example, allows the shorthand form `dispatcher!("greet")`, which auto-generates `CMDGreet` / `EntryGreet`. -| Macro | Description | -| ------------------------------------------------------- | --------------------------------------------------------------- | -| `empty_result!()` | Shorthand for returning an empty result early in a chain | -| `entry!(Type, ["a", "b"])` | Construct test data for an entry type | -| `group!(Type)` | Register external types as group members without modifying them | -| `pack_err!(ErrorType)` / `pack_err!(ErrorType = Inner)` | Create error types with an automatic `name` field | -| `#[program_setup]` | Declare a program initialization function | -| `dispatcher!("cmd.path")` **shorthand** | Omit `EntryStruct`, the entry name is auto-derived | +| Macro | Description | +| --------------------------------------- | --------------------------------------------------------------- | +| `empty_result!()` | Shorthand for returning an empty result early in a chain | +| `entry!(Type, ["a", "b"])` | Construct test data for an entry type | +| `group!(Type)` | Register external types as group members without modifying them | +| `#[program_setup]` | Declare a program initialization function | +| `dispatcher!("cmd.path")` **shorthand** | Omit `EntryStruct`, the entry name is auto-derived | <details> <summary> Details </summary> @@ -168,10 +168,13 @@ For example, allows the shorthand form `dispatcher!("greet")`, which auto-genera ```rust // Features: ["extras"] -pack!(StatePrev1 = ()); -pack!(StatePrev2 = ()); +#[derive(Grouped, Wrap)] +pub struct StatePrev1(()); +#[derive(Grouped, Wrap)] +pub struct StatePrev2(()); -pack!(StateNext = ()); +#[derive(Grouped, Wrap)] +pub struct StateNext(()); #[chain] fn handle_state_prev2(_p: StatePrev2) { @@ -186,7 +189,7 @@ fn handle_state_prev1(_p: StatePrev1) -> Next { // When Next is needed but no return value is required, use this empty_result!() } else { - StateNext::new(()).into() + StateNext(()).into() } } ``` @@ -217,7 +220,8 @@ fn no_error_setup(program: &mut Program<ThisProgram>) { // Features: ["extras"] use mingling::macros::entry; -pack!(EntryHello = Vec<String>); +#[derive(Grouped, Wrap)] +pub struct EntryHello(Vec<String>); fn main() { let result: Next = handle_hello(entry!("--name", "Bob")).into(); @@ -231,7 +235,7 @@ fn handle_hello(args: EntryHello) {} ### `group!` Registers an external type as a member of the program group without modifying its definition. -The type's simple name is used as the enum variant, just like `pack!` or `#[derive(Grouped)]`. +The type's simple name is used as the enum variant, just like `#[derive(Grouped)]`. ```rust // Features: ["extras"] @@ -243,26 +247,23 @@ use std::num::ParseIntError; group!(std::num::ParseIntError); ``` -### `pack_err!` +### Declaring Error Types -Creates an error struct with an automatic `name: String` field set to the snake_case -of the struct name. Optionally wraps an inner type for additional context. +Error types are declared with derives — the old `pack_err!` macro was removed in 0.5.0. +Use `#[derive(Grouped, Default)]` for a unit error (no payload), or +`#[derive(Grouped, Wrap)]` to wrap an inner type for additional context. ```rust // Features: ["extras"] use std::path::PathBuf; -// Simple form — only a name field: -pack_err!(ErrorNotFound); -// Generates: -// struct ErrorNotFound { pub name: String } -// impl Default for ErrorNotFound { ... } +// Unit form — no payload: +#[derive(Grouped, Default)] +pub struct ErrorNotFound; -// Typed form — with additional info field: -pack_err!(ErrorNotDir = PathBuf); -// Generates: -// struct ErrorNotDir { pub name: String, pub info: PathBuf } -// impl ErrorNotDir { pub fn new(info: PathBuf) -> Self { ... } } +// Typed form — wraps an inner type: +#[derive(Grouped, Wrap)] +pub struct ErrorNotDir(PathBuf); ``` </details> diff --git a/docs/pages/other/naming_rule.md b/docs/pages/other/naming_rule.md index 770fd10..fb678aa 100644 --- a/docs/pages/other/naming_rule.md +++ b/docs/pages/other/naming_rule.md @@ -94,7 +94,7 @@ Result + Content | `ResultGreetSomeone` | Greeting result | | `ResultFruitList` | Fruit list result | -Result structs are expected to be consumed by the Renderer, and their internal structure should be designed for rendering aesthetics. Generally use `#[derive(Grouped)]` instead of `pack!()` wrapping for more flexible field control. +Result structs are expected to be consumed by the Renderer, and their internal structure should be designed for rendering aesthetics. Generally prefer a named-field struct with `#[derive(Grouped)]` over a single-field tuple wrapper (`#[derive(Grouped, Wrap)]`) for more flexible field control. ### Error @@ -146,7 +146,8 @@ Error + Description | Resource (mutable) | `counter`, `cache`, `session`, etc. | ```rust -@@@ pack!(EntryRemoteAdd = Vec<String>); +@@@ #[derive(Grouped, Wrap)] +@@@ pub struct EntryRemoteAdd(Vec<String>); @@@ #[derive(Default, Clone)] @@@ struct ResDatabase { } @@@ #[derive(Default, Clone)] @@ -168,9 +169,12 @@ fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResData @@@ #[derive(Default, Clone)] @@@ struct ResDatabase { } @@@ impl ResDatabase { fn has_remote(&self, remote: &String) -> bool { true } } -@@@ pack!(StateOperationRemotes = String); -@@@ pack!(ResultRemoteAdded = String); -@@@ pack!(ErrorRepositoryNotFound = String); +@@@ #[derive(Grouped, Wrap, Default)] +@@@ pub struct StateOperationRemotes(String); +@@@ #[derive(Grouped, Wrap)] +@@@ pub struct ResultRemoteAdded(String); +@@@ #[derive(Grouped, Wrap)] +@@@ pub struct ErrorRepositoryNotFound(String); // Dispatcher dispatcher!("remote.add", EntryRemoteAdd); @@ -183,10 +187,10 @@ fn handle_remote_add(args: EntryRemoteAdd) -> Next { // State → Error or Result #[chain] fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase) -> Next { - if db.has_remote(&state.inner) { - ErrorRepositoryNotFound::new(state.inner).to_render() + if db.has_remote(&state.0) { + ErrorRepositoryNotFound(state.0).to_render() } else { - ResultRemoteAdded::new(state.inner).to_render() + ResultRemoteAdded(state.0).to_render() } } @@ -194,13 +198,13 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase #[renderer(buffer)] fn render_remote_added(result: ResultRemoteAdded) { - r_println!("Remote added: {}", result.inner); + r_println!("Remote added: {}", result.0); } // Error rendering #[renderer(buffer)] fn render_error_repository_not_found(err: ErrorRepositoryNotFound) { - r_println!("Error: remote '{}' not found", err.inner); + r_println!("Error: remote '{}' not found", err.0); } ``` |
