diff options
Diffstat (limited to 'docs')
39 files changed, 492 insertions, 395 deletions
diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md index 40d8cd9..b0fbe31 100644 --- a/docs/_zh_CN/pages/11-resource-system.md +++ b/docs/_zh_CN/pages/11-resource-system.md @@ -31,11 +31,12 @@ fn main() { @@@#[derive(Default, Clone)] @@@struct ResCurrentDir(String); @@@dispatcher!("pwd", EntryPrintWorkingDir); -@@@pack!(ResultPath = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultPath(String); // 通过 &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 @@ fn render_path(result: ResultPath) { @@@#[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 @@ Chain 可以同时注入任意多个资源,框架按类型自动匹配: @@@#[derive(Default, Clone)] struct ResConfig(String); @@@#[derive(Default, Clone)] struct ResCounter(u32); @@@dispatcher!("test", EntryTest); -@@@pack!(ResultDone = ()); +@@@#[derive(Grouped, Wrap, Default)] +@@@pub struct ResultDone(()); // 同时注入只读 + 可修改 #[chain] fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next { diff --git a/docs/_zh_CN/pages/12-exit-code.md b/docs/_zh_CN/pages/12-exit-code.md index 7c55b60..9270b84 100644 --- a/docs/_zh_CN/pages/12-exit-code.md +++ b/docs/_zh_CN/pages/12-exit-code.md @@ -3,9 +3,7 @@ 如何使用资源系统管理程序退出码 </p> -程序退出时给 shell 一个正确的退出码是 CLI 的基本素养 - -。Mingling 提供了开箱即用的 `ExitCodeSetup`,配合 `ResExitCode` 资源,让退出码控制变得极其简单。 +程序退出时给 shell 一个正确的退出码是 CLI 的基本素养。Mingling 提供了开箱即用的 `ExitCodeSetup`,配合 `ResExitCode` 资源,让退出码控制变得极其简单。 ## 启用 ExitCodeSetup @@ -31,7 +29,8 @@ fn main() { ```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) { // 检查失败的时候修改退出码资源 diff --git a/docs/_zh_CN/pages/13-hook.md b/docs/_zh_CN/pages/13-hook.md index 811957c..860bd2b 100644 --- a/docs/_zh_CN/pages/13-hook.md +++ b/docs/_zh_CN/pages/13-hook.md @@ -55,10 +55,11 @@ Hook 覆盖了管线的完整生命周期: @@@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/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md index 30f3fc4..13585ee 100644 --- a/docs/_zh_CN/pages/14-testing.md +++ b/docs/_zh_CN/pages/14-testing.md @@ -12,7 +12,8 @@ Chain 只是一个接收输入、返回输出的函数,Renderer 也只是接 Renderer 是最容易测试的——调用函数,断言返回结果: ```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 @@ fn test_render_name() { ```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(EntryHello(vec!["Alice".to_string()])).into(); // 断言这是一个渲染结果(不是继续 chain) assert_render_result!(chain_process); // 断言 member_id 是 ResultName assert_member_id!(chain_process, ResultName); // 解包出内部值 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 @@ fn test_handle_hello_with_name() { @@@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 @@ fn test_with_entry_macro() { @@@#[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() { // 资源需要在测试中手动传入 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/_zh_CN/pages/2-define-a-dispatcher.md b/docs/_zh_CN/pages/2-define-a-dispatcher.md index 7ae26f1..77a1513 100644 --- a/docs/_zh_CN/pages/2-define-a-dispatcher.md +++ b/docs/_zh_CN/pages/2-define-a-dispatcher.md @@ -48,17 +48,15 @@ dispatcher!("remote.rm", EntryRemoteRm); ```rust // 示意,dispatcher! 宏实际生成的代码 -pub struct EntryGreet { - pub inner: Vec<String>, -} +pub struct EntryGreet(pub Vec<String>); ``` -用户在命令行输入 `greet Alice Bob`,`EntryGreet.inner` 就是 `vec!["Alice", "Bob"]`。 +用户在命令行输入 `greet Alice Bob`,`EntryGreet` 包裹的 `Vec<String>`(即字段 `0`)就是 `vec!["Alice", "Bob"]`。 > [!IMPORTANT] -> Entry 的 `inner` 只包含 **匹配后剩余的参数**。 +> Entry 包裹的参数只包含 **匹配后剩余的参数**。 > -> 以 `remote add origin` 为例,`remote` 和 `add` 用于匹配命令路径,只有 `origin` 会进入 `EntryRemoteAdd.inner`。 +> 以 `remote add origin` 为例,`remote` 和 `add` 用于匹配命令路径,只有 `origin` 会进入 `EntryRemoteAdd`(即字段 `0`)。 ## 进阶:隐式声明 diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md index c8d8a24..3356b74 100644 --- a/docs/_zh_CN/pages/3-define-a-chain.md +++ b/docs/_zh_CN/pages/3-define-a-chain.md @@ -17,14 +17,15 @@ ```rust @@@dispatcher!("greet", EntryGreet); -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { // args 就是用户输入经过匹配后剩下的参数 - let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + let name = args.0.first().cloned().unwrap_or_else(|| "World".to_string()); // 把结果包装成 Next,告诉调度器下一步去哪 - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -32,7 +33,7 @@ fn handle_greet(args: EntryGreet) -> Next { Chain 函数签名里写着它需要什么——`args: EntryGreet` -然后用 `ResultName::new(name)` 返回一个新类型。 +然后用 `ResultName(name)` 返回一个新类型。 这个返回的 `Next` 会展开成 `impl Into<ChainProcess<ThisProgram>>`。 @@ -41,17 +42,30 @@ Chain 函数签名里写着它需要什么——`args: EntryGreet` > > 可以去 [任意输出机制](pages/concepts/3-any-output) 章节了解 `ChainProcess`。 -## `pack!` 宏 +## 用 `#[derive]` 定义管线类型 -你大概猜到了,`pack!(ResultName = String)` 定义了一个管线中传递的类型: +你大概猜到了,`#[derive(Grouped, Wrap)] pub struct ResultName(String);` 定义了一个管线中传递的类型: ```rust -// pack!(ResultName = String) 大概生成了这样的代码 +// 实际写代码时只需一行 #[derive(Grouped, Wrap)],它大概展开成下面这些实现: -#[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 生成 member_id() → ThisProgram::ResultName, +// 赋予类型路由身份和 Into<ChainProcess> 转换。 ``` 你可以把它理解为一个 打了标签的 `String`。 @@ -59,7 +73,7 @@ pub struct ResultName { 调度器通过这个标签来精确路由,确保数据不会混淆 —— 比如发给 `RenderGreet` 的数据不会被误传给 `RenderError`。 > [!NOTE] -> 与简单的类型别名 (`type`) 不同,`pack!` 会生成一个全新的类型,拥有独立的 `TypeId`。 +> 与简单的类型别名 (`type`) 不同,`#[derive(Grouped, Wrap)]` 会定义一个全新的类型,拥有独立的 `TypeId`。 命名上推荐这样的习惯: @@ -70,25 +84,26 @@ pub struct ResultName { | 最终结果 | `Result` + 描述 | `ResultGreetSomeone` | | 错误 | `Error` + 描述 | `ErrorUserNotFound` | -详见 [命名规范](pages/other/naming_rule),不过现在你只需要记住:**用 `pack!` 给你的数据取一个有意义的名字**。 +详见 [命名规范](pages/other/naming_rule),不过现在你只需要记住:**用 `#[derive(Grouped)]`(可搭配 `Wrap`)给你的数据取一个有意义的名字**。 ## 从 Entry 中提取参数 -`EntryGreet` 的 `inner` 是一个 `Vec<String>`,你可以在 Chain 里自由地处理它: +`EntryGreet` 包裹的 `Vec<String>`(即字段 `0`)你可以在 Chain 里自由地处理它: ```rust @@@dispatcher!("greet", EntryGreet); -@@@pack!(ResultName = String); +@@@#[derive(Grouped, Wrap)] +@@@pub struct ResultName(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { // 取第一个参数,没有就用默认值 let name = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultName::new(name).into() + ResultName(name).into() } ``` @@ -103,16 +118,17 @@ fn handle_greet(args: EntryGreet) -> Next { dispatcher!("greet", EntryGreet); // 2. 声明管线中的数据类型 -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); // 3. 处理逻辑 #[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/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md index e3e49f5..ad915ba 100644 --- a/docs/_zh_CN/pages/4-render-result.md +++ b/docs/_zh_CN/pages/4-render-result.md @@ -3,7 +3,7 @@ 使用 renderer 宏声明渲染器,将结果输出 </p> -现在,我们创建了 Dispatcher 和 Chain,也通过 `pack!` 产出了一个 Result 类型。最后一步:**把结果展示给用户**。 +现在,我们创建了 Dispatcher 和 Chain,也通过 `#[derive(Grouped, Wrap)]` 产出了一个 Result 类型。最后一步:**把结果展示给用户**。 ## `#[renderer]` 宏 @@ -11,7 +11,8 @@ ```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 @@ Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在 ```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. 用 Dispatcher 声明命令 dispatcher!("greet", EntryGreet); -// 2. 用 pack! 声明结果数据 -pack!(ResultName = String); +// 2. 用 #[derive(Grouped, Wrap)] 声明结果数据 +#[derive(Grouped, Wrap)] +pub struct ResultName(String); // 3. 用 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. 用 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" 你完成了第一个完整的 Mingling 程序!来回顾一下学到的东西: -| 概念 | 对应宏/函数 | 一句话 | -| -------- | ---------------- | -------------------------- | -| 声明命令 | `dispatcher!` | 告诉程序用户能输入什么 | -| 处理逻辑 | `#[chain]` | 收到参数后做什么 | -| 输出结果 | `#[renderer]` | 怎么把结果告诉用户 | -| 类型包装 | `pack!` | 给你的数据取个有意义的名字 | -| 程序入口 | `gen_program!()` | 自动生成管线的接线图 | +| 概念 | 对应宏/函数 | 一句话 | +| -------- | -------------------------- | -------------------------- | +| 声明命令 | `dispatcher!` | 告诉程序用户能输入什么 | +| 处理逻辑 | `#[chain]` | 收到参数后做什么 | +| 输出结果 | `#[renderer]` | 怎么把结果告诉用户 | +| 类型包装 | `#[derive(Grouped, Wrap)]` | 给你的数据取个有意义的名字 | +| 程序入口 | `gen_program!()` | 自动生成管线的接线图 | 在真实项目中你还会用到资源注入、hook、补全、REPL 等高级功能,不过核心骨架永远不变:**Dispatcher → Chain → Renderer**。 diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md index e0a58e7..9d9d37e 100644 --- a/docs/_zh_CN/pages/5-multiple-commands.md +++ b/docs/_zh_CN/pages/5-multiple-commands.md @@ -15,19 +15,21 @@ 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 @@ dispatcher!("remote.rm", EntryRemoteRm); ## 数据类型的独立性 -注意我们用了两个不同的 `pack!`: +注意我们用了两个不同的类型: -- `pack!(ResultGreeting = String)` -- `pack!(ResultSum = i32)` +- `#[derive(Grouped, Wrap)] pub struct ResultGreeting(String);` +- `#[derive(Grouped, Wrap)] pub struct ResultSum(i32);` 它们都是独立的类型,`gen_program!()` 会给它们分配不同的枚举变体。 diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md index 7944d0a..6c88423 100644 --- a/docs/_zh_CN/pages/6-argument-parse-picker.md +++ b/docs/_zh_CN/pages/6-argument-parse-picker.md @@ -3,7 +3,7 @@ 用 Picker 完成基本的参数解析 </p> -前面教程中我们都是手动从 `EntryGreet.inner`(`Vec<String>`)中提取参数。 +前面教程中我们都是手动从 `EntryGreet.0`(`Vec<String>`)中提取参数。 ```rust @@@ fn main() { @@ -27,14 +27,15 @@ features = ["picker"] ```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 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ```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 @@ let name = prev ```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 @@ let name = prev ```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 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ```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 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ```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 @@ fn handle_test_entry(prev: EntryTest) -> Next { @@@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 @@ fn render_greet(result: ResultName) { ```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 @@ let name = route!(args.pick_or_route(&arg![String], || ErrorFail::new(()).to_cha ```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 @@ let name = match args.pick_or_route(&arg![String], || ErrorFail::new(()).to_chai ```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/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md index dce48d1..463b829 100644 --- a/docs/_zh_CN/pages/9-error-handling.md +++ b/docs/_zh_CN/pages/9-error-handling.md @@ -20,17 +20,19 @@ ```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 @@ fn handle_greet(args: EntryGreet) -> Next { ```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 @@ fn render_error_name_empty(err: ErrorNameEmpty) { @@@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 ``` -## 关于 `pack_err!` +## 关于错误类型 -如果你启用了 `extras`,还可以用 `pack_err!` 快速声明带有自动 `name` 字段的错误类型: +不需要额外上下文、只起“标记”作用的错误类型,可以直接用 `#[derive(Grouped, Default)]` 声明: ```rust // Features: ["extras"] -pack_err!(ErrorNotFound); -// 生成: struct ErrorNotFound { pub name: String } +#[derive(Grouped, Default)] +pub struct ErrorNotFound; ``` 详见 [特性列表](pages/other/features)。 diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md index f2e1717..f678e2f 100644 --- a/docs/_zh_CN/pages/advanced/2-structural-renderer.md +++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md @@ -21,7 +21,7 @@ features = ["structural_renderer"] ## 基本用法 -启用 `StructuralRendererSetup` 后,用 `pack_structural!` 替代 `pack!` 来声明支持结构化输出的类型: +启用 `StructuralRendererSetup` 后,用 `#[derive(StructuralData)]`(搭配 `serde::Serialize`、`Grouped` 和 `Wrap`)来声明支持结构化输出的类型: ```rust // Features: ["structural_renderer"] @@ -29,16 +29,18 @@ features = ["structural_renderer"] // serde = "1" @@@use mingling::macros::buffer; @@@use mingling::setup::StructuralRendererSetup; +@@@use mingling::StructuralData; @@@dispatcher!("render", EntryRender); -// pack_structural! 等价于 pack! + StructuralData -pack_structural!(ResultInfo = (String, i32)); +// #[derive(Grouped, Wrap)] + StructuralData + serde::Serialize 等价于旧版的 pack_structural! +#[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 @@ fn render_info(r: ResultInfo) { ## 自定义输出结构 -`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Grouped)]` 手动定义类型: +用 `#[derive(Grouped, Wrap)]` 包装的元组结构体默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[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/_zh_CN/pages/concepts/2-resource.md b/docs/_zh_CN/pages/concepts/2-resource.md index 1052254..154fa7d 100644 --- a/docs/_zh_CN/pages/concepts/2-resource.md +++ b/docs/_zh_CN/pages/concepts/2-resource.md @@ -34,7 +34,8 @@ ```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/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md index 118e856..1a2164c 100644 --- a/docs/_zh_CN/pages/concepts/3-any-output.md +++ b/docs/_zh_CN/pages/concepts/3-any-output.md @@ -20,7 +20,7 @@ AnyOutput<G> 这里的 `G` 就是 `gen_program!()` 生成的程序枚举(也就是你熟知的 `ThisProgram`)。 -每个被 `pack!` 或 `#[derive(Grouped)]` 标记的类型都被分配到这个枚举的一个变体。 +每个被 `#[derive(Grouped)]`(可搭配 `Wrap`)标记的类型都被分配到这个枚举的一个变体。 ## ChainProcess:数据 + 路由 @@ -48,7 +48,7 @@ trait Grouped<G> { } ``` -当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Grouped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。 +当你用 `#[derive(Grouped, Wrap)] pub struct ResultName(String);` 时,派生宏自动为 `ResultName` 实现 `Grouped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。 `to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。 @@ -66,7 +66,7 @@ trait Grouped<G> { > [!TIP] > 日常开发中你不需要手动操作 `AnyOutput` 或 `ChainProcess`。 > -> `pack!`、`#[chain]`、`#[renderer]` 这些宏帮你处理了所有的包装和解包。 +> `#[derive(Grouped)]`、`#[chain]`、`#[renderer]` 这些宏帮你处理了所有的包装和解包。 <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass diff --git a/docs/_zh_CN/pages/concepts/4-program-collect.md b/docs/_zh_CN/pages/concepts/4-program-collect.md index a0236f6..616f645 100644 --- a/docs/_zh_CN/pages/concepts/4-program-collect.md +++ b/docs/_zh_CN/pages/concepts/4-program-collect.md @@ -9,7 +9,7 @@ ### 1. 生成枚举 -扫描当前模块中所有 `pack!`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。 +扫描当前模块中所有 `#[derive(Grouped)]`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。 这个枚举就是 `AnyOutput<G>` 中 `G` 的类型 —— 调度器靠枚举变体来区分管线中传递的不同数据。 diff --git a/docs/_zh_CN/pages/other/features.md b/docs/_zh_CN/pages/other/features.md index 30231ce..5fa7c86 100644 --- a/docs/_zh_CN/pages/other/features.md +++ b/docs/_zh_CN/pages/other/features.md @@ -80,11 +80,12 @@ features = ["build_full"] ```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 @@ build_comp_scripts("myprogram").unwrap(); 例如,允许 `dispatcher!("greet")` 的缩写形式,自动生成 `CMDGreet` / `EntryGreet`。 -| 宏 | 说明 | -| ------------------------------------------------------- | -------------------------------------- | -| `empty_result!()` | 链中提前返回空结果的简写 | -| `entry!(Type, ["a", "b"])` | 构造入口类型的测试数据 | -| `group!(Type)` | 将外部类型注册为组成员,无需修改其定义 | -| `pack_err!(ErrorType)` / `pack_err!(ErrorType = Inner)` | 创建带自动 `name` 字段的错误类型 | -| `#[program_setup]` | 声明程序初始化函数 | -| `dispatcher!("cmd.path")` **缩写形式** | 省略 `EntryStruct`,入口类型名自动推导 | +| 宏 | 说明 | +| -------------------------------------- | -------------------------------------- | +| `empty_result!()` | 链中提前返回空结果的简写 | +| `entry!(Type, ["a", "b"])` | 构造入口类型的测试数据 | +| `group!(Type)` | 将外部类型注册为组成员,无需修改其定义 | +| `#[program_setup]` | 声明程序初始化函数 | +| `dispatcher!("cmd.path")` **缩写形式** | 省略 `EntryStruct`,入口类型名自动推导 | <details> <summary> Details </summary> @@ -168,10 +168,13 @@ build_comp_scripts("myprogram").unwrap(); ```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 { // 当需要 Next 且不需要返回值,便可以使用它 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!` 将外部类型注册为程序组成员,无需修改原始类型的定义。 -类型名会直接作为枚举变体,与 `pack!` 或 `#[derive(Grouped)]` 一致。 +类型名会直接作为枚举变体,与 `#[derive(Grouped)]` 一致。 ```rust // Features: ["extras"] @@ -243,26 +247,23 @@ use std::num::ParseIntError; group!(std::num::ParseIntError); ``` -### `pack_err!` +### 定义错误类型 -创建带自动 `name: String` 字段的错误结构体,字段值自动设为结构体名的蛇形命名。 -可选择包裹一个内部类型以携带额外上下文。 +0.5.0 起 `pack_err!` 已移除,错误类型直接用 derive 声明: +不携带额外上下文时用 `#[derive(Grouped, Default)]`(仅作标记),或 +用 `#[derive(Grouped, Wrap)]` 包裹一个内部类型以携带上下文。 ```rust // Features: ["extras"] use std::path::PathBuf; -// 简单形式——仅包含 name 字段: -pack_err!(ErrorNotFound); -// 生成: -// struct ErrorNotFound { pub name: String } -// impl Default for ErrorNotFound { ... } +// 简单形式——只作为标记使用: +#[derive(Grouped, Default)] +pub struct ErrorNotFound; -// 带类型的形式——包含额外的 info 字段: -pack_err!(ErrorNotDir = PathBuf); -// 生成: -// struct ErrorNotDir { pub name: String, pub info: PathBuf } -// impl ErrorNotDir { pub fn new(info: PathBuf) -> Self { ... } } +// 带类型的形式——包裹一个内部类型以携带上下文: +#[derive(Grouped, Wrap)] +pub struct ErrorNotDir(PathBuf); ``` </details> diff --git a/docs/_zh_CN/pages/other/naming_rule.md b/docs/_zh_CN/pages/other/naming_rule.md index 1bca8f6..7694f56 100644 --- a/docs/_zh_CN/pages/other/naming_rule.md +++ b/docs/_zh_CN/pages/other/naming_rule.md @@ -94,7 +94,7 @@ Result + 内容 | `ResultGreetSomeone` | 问候结果 | | `ResultFruitList` | 水果列表结果 | -结果结构体期望被 Renderer 消费,内部结构应该为了渲染美观而设计。一般用 `#[derive(Grouped)]` 代替 `pack!()` 包装,以获得更灵活的字段控制。 +结果结构体期望被 Renderer 消费,内部结构应该为了渲染美观而设计。一般用 `#[derive(Grouped)]` 标注结构体,以获得更灵活的字段控制。 ### 错误 @@ -146,7 +146,8 @@ Error + 描述 | 资源(可变) | `counter`、`cache`、`session` 等 | ```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!("remote.add", EntryRemoteAdd); @@ -183,23 +187,24 @@ fn handle_remote_add(args: EntryRemoteAdd) -> Next { // 状态 → 错误或结果 #[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() } } // 结果渲染 + #[renderer(buffer)] fn render_remote_added(result: ResultRemoteAdded) { - r_println!("Remote added: {}", result.inner); + r_println!("Remote added: {}", result.0); } // 错误渲染 #[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); } ``` diff --git a/docs/dev/_sidebar.md b/docs/dev/_sidebar.md index cfb482a..e0a77d3 100644 --- a/docs/dev/_sidebar.md +++ b/docs/dev/_sidebar.md @@ -1,15 +1,15 @@ - [Welcome!](README) * ❓ Issues * [[Solved] The Picker2 Arguments Parser](pages/issues/_add-picker2) + * [[T1] Modify the dispatcher! Syntax](pages/issues/_modify-dispatcher-syntax) + * [[T0] Remove the pack! Family of Macros](pages/issues/_remove-pack-macros) + * [[T0] Remove the parser Feature](pages/issues/_remove-parser-feature) * [[Solved] Remove r_print! and r_println! Macros](pages/issues/_remove-r-print-macro) + * [[T0] Remove with_dispatcher and with_dispatchers](pages/issues/_remove-with-dispatcher) * [[Solved] The Command Macro](pages/issues/_the-command-macro) * [[Solved] The Mod Pathfinder](pages/issues/_the-mod-pathfinder) * [[T0] Generalize the REPL System](pages/issues/t0_generalize-repl-system) - * [[T0] Remove the pack! Family of Macros](pages/issues/t0_remove-pack-macros) - * [[T0] Remove the parser Feature](pages/issues/t0_remove-parser-feature) - * [[T0] Remove with_dispatcher and with_dispatchers](pages/issues/t0_remove-with-dispatcher) * [[T1] Higher-Level Abstractions for the Completion System](pages/issues/t1_completion-higher-level-abstractions) - * [[T1] Modify the dispatcher! Syntax](pages/issues/t1_modify-dispatcher-syntax) * [[T1] Move structural_renderer from mingling_core to mingling](pages/issues/t1_move-structural-renderer) * [[T1] The pathf_export Attribute Macro](pages/issues/t1_pathf-export-macro) * [[T2] Automated dispatcher_tree Optimization Decisions](pages/issues/t2_automated-dispatch-tree-optimization) diff --git a/docs/dev/pages/issues/t1_modify-dispatcher-syntax.md b/docs/dev/pages/issues/_modify-dispatcher-syntax.md index f1a9346..ed409ef 100644 --- a/docs/dev/pages/issues/t1_modify-dispatcher-syntax.md +++ b/docs/dev/pages/issues/_modify-dispatcher-syntax.md @@ -27,17 +27,17 @@ dispatcher!("command", EntryCommand); ## Tasks -- [ ] Update `dispatcher!` to accept the simplified explicit form (`"name", EntryType`) -- [ ] Decide whether the old `CMDType => EntryType` form should error with a helpful message or be removed outright -- [ ] Remove the generated `CMD*` struct machinery -- [ ] Update `#[command]` macro internals that depend on `CMD*` -- [ ] Migrate examples, tests, and docs -- [ ] Keep the implicit mode (`dispatcher!("name")`) working unchanged +- [x] Update `dispatcher!` to accept the simplified explicit form (`"name", EntryType`) +- [x] Decide whether the old `CMDType => EntryType` form should error with a helpful message or be removed outright +- [x] Remove the generated `CMD*` struct machinery +- [x] Update `#[command]` macro internals that depend on `CMD*` +- [x] Migrate examples, tests, and docs +- [x] Keep the implicit mode (`dispatcher!("name")`) working unchanged ## 🕘 Progress -- [ ] In Progress -- [ ] Complete +- [x] In Progress +- [x] Complete <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass diff --git a/docs/dev/pages/issues/t0_remove-pack-macros.md b/docs/dev/pages/issues/_remove-pack-macros.md index 517646a..74c21aa 100644 --- a/docs/dev/pages/issues/t0_remove-pack-macros.md +++ b/docs/dev/pages/issues/_remove-pack-macros.md @@ -32,17 +32,17 @@ pub struct ResultNames { ## Tasks -- [ ] Identify all usages of `pack!` / `pack_structural!` / `pack_err_structural!` across the codebase, examples, and docs -- [ ] Migrate internal usages to `#[derive(Grouped)]` -- [ ] Remove the macro definitions and their re-exports -- [ ] Update the docs / helpdoc examples that reference `pack!` -- [ ] Update downstream feature docs (`structural_renderer` etc.) where `pack_structural!` was involved -- [ ] Verify all tests pass +- [x] Identify all usages of `pack!` / `pack_structural!` / `pack_err_structural!` across the codebase, examples, and docs +- [x] Migrate internal usages to `#[derive(Grouped)]` +- [x] Remove the macro definitions and their re-exports +- [x] Update the docs / helpdoc examples that reference `pack!` +- [x] Update downstream feature docs (`structural_renderer` etc.) where `pack_structural!` was involved +- [x] Verify all tests pass ## 🕘 Progress -- [ ] In Progress -- [ ] Complete +- [x] In Progress +- [x] Complete <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass diff --git a/docs/dev/pages/issues/t0_remove-parser-feature.md b/docs/dev/pages/issues/_remove-parser-feature.md index 5c1cffd..c2a11c0 100644 --- a/docs/dev/pages/issues/t0_remove-parser-feature.md +++ b/docs/dev/pages/issues/_remove-parser-feature.md @@ -19,17 +19,17 @@ Completely remove the `parser` feature in 0.5.0. This will directly affect downs ## Tasks -- [ ] Identify all usages of the `parser` feature across the codebase, examples, and docs -- [ ] Migrate internal usages (tests, examples, dev-dependencies) to `picker` -- [ ] Remove the `parser` feature from `mingling` and its dependency (`size`) -- [ ] Remove parser-related modules and public API -- [ ] Update docs and helpdoc examples -- [ ] Note the downstream migration path in the changelog +- [x] Identify all usages of the `parser` feature across the codebase, examples, and docs +- [x] Migrate internal usages (tests, examples, dev-dependencies) to `picker` +- [x] Remove the `parser` feature from `mingling` and its dependency (`size`) +- [x] Remove parser-related modules and public API +- [x] Update docs and helpdoc examples +- [x] Note the downstream migration path in the changelog ## 🕘 Progress -- [ ] In Progress -- [ ] Complete +- [x] In Progress +- [x] Complete <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass diff --git a/docs/dev/pages/issues/t0_remove-with-dispatcher.md b/docs/dev/pages/issues/_remove-with-dispatcher.md index 8779d74..b0d972f 100644 --- a/docs/dev/pages/issues/t0_remove-with-dispatcher.md +++ b/docs/dev/pages/issues/_remove-with-dispatcher.md @@ -19,16 +19,16 @@ Make `Dispatcher` registration also compile-time collected in non-`dispatcher_tr ## Tasks -- [ ] Design how dispatchers are collected at compile time when `dispatcher_tree` is disabled (consistent with how `chain` / `renderer` / `completion` / `metadata` are collected) -- [ ] Remove `with_dispatcher` and `with_dispatchers` from the `Program` API -- [ ] Update `gen_program!` and the macros so registration happens automatically -- [ ] Migrate examples, tests, and docs that call `with_dispatcher` / `with_dispatchers` -- [ ] Verify both `dispatcher_tree`-enabled and disabled modes behave identically +- [x] Design how dispatchers are collected at compile time when `dispatcher_tree` is disabled (consistent with how `chain` / `renderer` / `completion` / `metadata` are collected) +- [x] Remove `with_dispatcher` and `with_dispatchers` from the `Program` API +- [x] Update `gen_program!` and the macros so registration happens automatically +- [x] Migrate examples, tests, and docs that call `with_dispatcher` / `with_dispatchers` +- [x] Verify both `dispatcher_tree`-enabled and disabled modes behave identically ## 🕘 Progress -- [ ] In Progress -- [ ] Complete +- [x] In Progress +- [x] Complete <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json index 8d6eaa6..38f73cd 100644 --- a/docs/example-pages/examples.json +++ b/docs/example-pages/examples.json @@ -273,23 +273,6 @@ ] }, { - "id": "example-pack-err", - "name": "Pack an Error", - "icon": "🛑", - "category": "macros", - "desc": "Demonstrates how to use the `pack_err!` macro to define error types with automatic `name` field (snake_case at compile time) and optional `info` field. Also shows `--json` serialization when `structural_renderer` is enabled.\n", - "tags": [ - "pack_err!", - "extras", - "structural_renderer", - "--json" - ], - "files": [ - "src/main.rs", - "Cargo.toml" - ] - }, - { "id": "example-panic-unwind", "name": "Panic Unwind", "icon": "💥", 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); } ``` diff --git a/docs/res/changlog_examples/feat_program_res.rs b/docs/res/changlog_examples/feat_program_res.rs index 5133000..e9cc4e2 100644 --- a/docs/res/changlog_examples/feat_program_res.rs +++ b/docs/res/changlog_examples/feat_program_res.rs @@ -22,11 +22,12 @@ fn main() { dispatcher!("modify", EntryModify); -pack!(DisplayGlobal = ()); +#[derive(Grouped, Wrap)] +pub struct DisplayGlobal(()); #[chain] fn modify(prev: EntryModify) { - let (name, age) = Picker::<()>::new(prev.inner) + let (name, age) = Picker::<()>::new(prev.0) .pick::<String>("--name") .pick::<i32>("--age") .unpack_directly(); |
