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 | |
| 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.
101 files changed, 1229 insertions, 2428 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ea71a92..62b1f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -301,6 +301,54 @@ None _Behavioral note:_ the `picked` values, flag parsing semantics, and the "longest registered prefix wins" matching rule are all preserved by the `picker` feature's `arg-picker` API. The removal is purely a dead-code cleanup: the legacy built-in parser has been fully superseded by `picker`, and downstream code must migrate to the new API names described above. +5. **[`macros`]** **[BREAKING REMOVAL]** Removed the `pack!`, `pack_err!`, `pack_structural!`, and `pack_err_structural!` macros and replaced all pipeline type definitions with `#[derive(Grouped)]` (and, where applicable, `#[derive(Grouped, Wrap)]`) on user-defined structs. + + ### What changed + + The `pack!` family of macros — which generated a wrapper struct with a fixed `inner` field and a suite of trait impls (`From`/`Into`, `AsRef`/`AsMut`, `Deref`/`DerefMut`, conditional `Default`, `Grouped`, and `Into<AnyOutput>`/`Into<ChainProcess>`) — has been removed. Pipeline types are now defined directly as ordinary Rust structs annotated with the `Grouped` derive macro, optionally combined with `Wrap` for the ergonomic trait impls previously provided by `pack!`. + + **Removed API:** + + - **`mingling::macros::pack!`** — Removed entirely (both the `pack`/`pack` and `pack`/`pack_structural` re-exports and the underlying macro implementations). + - **`mingling::macros::pack_err!`** — Removed (extras feature). + - **`mingling::macros::pack_structural!`** — Removed (structural_renderer feature). + - **`mingling::macros::pack_err_structural!`** — Removed (structural_renderer + extras features). + - _*All `pack`* re-exports_* from `mingling::prelude` and `mingling::macros` — removed. + - **`mingling_macros` implementation modules** — Deleted `func/pack.rs`, `func/pack_err.rs`, `func/pack_structural.rs`, `func/pack_err_structural.rs`, and `systems/structural_data.rs`. + - **`mingling_pathf::patterns::PackPattern`** — Removed from `pattern_analyzer::init()` and the `patterns` module (both the `patterns/pack.rs` file and its `pub use pack::*;` re-export). + - **Pathf test asset** — Deleted `mingling_pathf/test/src/test_files/test_pack.rs` and the `test_pack_analyze` test in `mingling_pathf/test/src/lib.rs`. + - **`mingling/src/docs/lib.md` / `mingling/src/example_docs.rs`** — The `example_pack_err` doc module (and the corresponding `examples/example-pack-err/` example) were removed (Cargo.toml, Cargo.lock, src/main.rs, page.toml, test.toml, and the `examples.json` entry). + - **`docs/pages/other/features.md`** — The `pack_err!` row and detailed section were removed in favor of a "Declaring Error Types" section documenting `#[derive(Grouped, Default)]` (unit form) / `#[derive(Grouped, Wrap)]` (typed form). + - **`docs/_zh_CN/pages/other/features.md`** — Same changes as the English `features.md`. + - **`mingling/src/gen_program.rs`** — Doc comments updated from "created by the `pack!` macro" to "generated by the `gen_program!` macro" for `Entry`, `ErrorRendererNotFound`, `EntryFallback`, and `CompletionContext`. The "You can register it using `with_dispatcher`" doc on `CMDCompletion` was removed (since `with_dispatcher` no longer exists). The "and many others" phrase in the `macros` module doc was trimmed to remove `pack!` and friends. + + **Migration guide:** + + | Old `pack!`-family macro usage | New equivalent | + | ------------------------------------------ | ----------------------------------------------------------------------------------------- | + | `pack!(TypeName = Inner);` | `#[derive(Grouped, Wrap)] pub struct TypeName(Inner);` | + | `pack!(TypeName = (A, B));` | `#[derive(Grouped, Wrap)] pub struct TypeName((A, B));` | + | `pack!(TypeName = ());` (unit) | `#[derive(Grouped, Wrap, Default)] pub struct TypeName(());` | + | `pack_err!(ErrorName);` (simple) | `#[derive(Grouped, Default)] pub struct ErrorName;` | + | `pack_err!(ErrorName = Inner);` (typed) | `#[derive(Grouped, Wrap)] pub struct ErrorName(Inner);` | + | `pack_structural!(TypeName = Inner);` | `#[derive(serde::Serialize, StructuralData, Grouped, Wrap)] pub struct TypeName(Inner);` | + | `pack_err_structural!(ErrorName);` | `#[derive(serde::Serialize, StructuralData, Grouped, Default)] pub struct ErrorName;` | + | `pack_err_structural!(ErrorName = Inner);` | `#[derive(serde::Serialize, StructuralData, Grouped, Wrap)] pub struct ErrorName(Inner);` | + + Behavioral differences to note when migrating: + + - **Field access** — The old `pack!` types exposed a `pub inner` field. The new `#[derive(Grouped, Wrap)]` types are tuple structs whose single field is accessed as `.0` (or `.0.0` / `.0.1` when the inner type is itself a tuple, e.g. `(String, String)`). All internal call sites and examples were updated accordingly (e.g. `StateConfigEdit((String, String))` now accesses `kv.0.0` / `kv.0.1`, `StatePkgEnable((String, String))` accesses `p.0.0` / `p.0.1`). + - **Constructor** — `TypeName::new(inner)` becomes `TypeName(inner)`. `Dispatcher::begin` now constructs the entry as `#pack(args)` instead of `#pack::new(args)`. + - **`name` field / `info` field** — `pack_err!`'s auto-generated `name: String` field (snake_cased at compile time) and `info: Type` field are gone. Unit errors use `#[derive(Grouped, Default)]` and are constructed as plain unit values (`ErrorFoo`), while typed errors wrap their payload as `.0` (accessed as `err.0` instead of `err.info`). + - **`Default`** — `pack!`'s `Default` was conditional on the inner type. The new `#[derive(Grouped, Wrap, Default)]` requires the inner type to also impl `Default`; for unit-like errors use `#[derive(Grouped, Default)]` (a plain unit struct). + - **`AsRef` / `AsMut`** — The `Wrap` derive generates `Deref`/`DerefMut`, `From`, and `Into` but not `AsRef`/`AsMut`. Code relying on `AsRef`/`AsMut` from `pack!` should switch to `Deref` (`*value`) or field access. + - **Structured output** — `pack_structural!` / `pack_err_structural!` auto-derived `serde::Serialize` and `StructuralData`. The new equivalent requires adding `#[derive(serde::Serialize, StructuralData, ...)]` explicitly (and `use mingling::StructuralData;` when needed). + - **`register_type!`** — Both `Grouped` and `Wrap` derives invoke `register_type!` internally, so no separate registration call is needed. + + **Examples of internal updates in this release** (all files updated from `pack!`/`pack_err!`/etc. to the derive-based form): `examples/example-argument-picker`, `examples/example-async-support`, `examples/example-basic`, `examples/example-combine-pathf-dispatch-tree`, `examples/example-combine-pathf-metadata`, `examples/example-command-macro`, `examples/example-completion`, `examples/example-error-handling`, `examples/example-exitcode`, `examples/example-hook`, `examples/example-lazy-resources`, `examples/example-metadata`, `examples/example-outside-type`, `examples/example-panic-unwind`, `examples/example-pathfinder`, `examples/example-repl-basic`, `examples/example-setup`, `examples/example-structural-renderer`, `examples/example-unit-test`, `examples/full-todolist`, `mingling_cli/src/config/cmd_cfg.rs`, `mingling_cli/src/lib.rs`, `mingling_cli/src/linter/cmd_explain.rs`, `mingling_cli/src/linter/cmd_lint.rs`, `mingling_cli/src/linter/mlint_report.rs`, `mingling_cli/src/metadata/cmd_metadata.rs`, `mingling_cli/src/pkg_mgr.rs`, `mingling_cli/src/pkg_mgr/cmd_install.rs`, `mingling_cli/src/pkg_mgr/cmd_internal_loadpkgs.rs`, `mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs`, `mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs`, `mingling_cli/src/pkg_mgr/cmd_pkg_show.rs`, `mingling_cli/src/pkg_mgr/cmd_uninstall.rs`, `mingling_cli/src/proj_mgr/cmd_class_add.rs`, `mingling_cli/src/proj_mgr/cmd_proj_init.rs`, the `dispatcher!` / `entry!` / `gen_program!` / `program_comp_gen!` / `program_fallback_gen!` / `program_final_gen!` macro implementations, `mingling_macros/src/func/dispatcher_clap.rs`, `mingling/src/example_docs.rs`, `mingling/src/docs/lib.md`, `README.md`, `GETTING-STARTED.md`, and all docs pages / tests listed above. + + _Behavioral note:_ the runtime semantics of pipeline types are unchanged — `#[derive(Grouped, Wrap)]` produces types with the same `Grouped` identity, `Into<AnyOutput>`/`Into<ChainProcess>` routing, `Deref`/`DerefMut`, and `From`/`Into` conversions that `pack!` provided. The removal is purely an API move from magic macros to standard Rust derives, reducing macro surface area and making pipeline types inspectable and composable like any other struct. + --- ## Contents diff --git a/GETTING-STARTED.md b/GETTING-STARTED.md index c1b8e25..0730136 100644 --- a/GETTING-STARTED.md +++ b/GETTING-STARTED.md @@ -66,23 +66,24 @@ The `#[chain]` attribute turns a plain function into an execution step. Think of ```rust dispatcher!("greet", EntryGreet); -pack!(ResultGreeting = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { let greeting = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting).into() + ResultGreeting(greeting).into() } ``` Key points: - The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`. -- You chain results by calling `.to_chain()` on any `pack!`-ed type. +- You chain results by calling `.to_chain()` on any type defined with `#[derive(Grouped)]`. - You can have **multiple chain functions** for the same command, each transforming the data further. - With the `async` feature, chain functions can be `async fn`. @@ -93,11 +94,11 @@ Key points: The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`. ```rust -use mingling::macros::pack; use mingling::prelude::*; use std::io::Write; -pack!(ResultGreeting = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); #[renderer] fn render_greeting(greeting: ResultGreeting) -> RenderResult { @@ -132,7 +133,8 @@ Mingling provides a **Picker** for argument extraction. You can use `pick()` or ```rust // Features: ["picker"] dispatcher!("greet", EntryGreet); -pack!(ResultGreeting = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { @@ -140,7 +142,7 @@ fn handle_greet(args: EntryGreet) -> Next { .pick(&arg![String]) // positional argument: first string .pick_or(&arg![repeat: u8, 'r'], || 1) // optional flag with default value .unwrap(); - ResultGreeting::new(format!("{} x{}", name, count)).into() + ResultGreeting(format!("{} x{}", name, count)).into() } ``` @@ -195,7 +197,8 @@ With the `comp` feature, Mingling provides a fully dynamic completion system. Yo use mingling::{macros::suggest, ShellContext, Suggest}; dispatcher!("greet", EntryGreet); -pack!(ResultName = (u8, String)); +#[derive(Grouped, Wrap)] +pub struct ResultName((u8, String)); #[completion(EntryGreet)] fn complete_greet(ctx: &ShellContext) -> Suggest { @@ -268,29 +271,34 @@ fn complete_lang(_: &ShellContext) -> Suggest { ## 7. Error Handling -Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`: +Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `#[derive(Grouped)]` / `#[derive(Grouped, Wrap)]` and route to them with `.to_render()`: ```rust -use mingling::macros::pack; use mingling::prelude::*; use std::io::Write; dispatcher!("hello", EntryHello); -pack!(ResultName = String); -pack!(ErrorNoNameProvided = ()); -pack!(ErrorNameTooLong = u16); + +#[derive(Grouped, Wrap)] +pub struct ResultName(String); + +#[derive(Grouped)] +pub struct ErrorNoNameProvided; + +#[derive(Grouped, Wrap)] +pub struct ErrorNameTooLong(u16); #[chain] fn handle(args: EntryHello) -> Next { - let Some(name) = args.inner.first().cloned() else { - return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer + let Some(name) = args.0.first().cloned() else { + return ErrorNoNameProvided.to_render(); // ← early return to error renderer }; if name.len() > 10 { - return ErrorNameTooLong::new(name.len() as u16).to_render(); + return ErrorNameTooLong(name.len() as u16).to_render(); } - ResultName::new(name).to_render() // ← success path + ResultName(name).to_render() // ← success path } #[renderer] @@ -633,7 +641,9 @@ use std::io::Write; use std::time::Duration; dispatcher!("download", EntryDownload); -pack!(ResultDownloaded = String); + +#[derive(Grouped, Wrap)] +pub struct ResultDownloaded(String); #[chain] pub async fn handle_download(args: EntryDownload) -> Next { @@ -643,7 +653,7 @@ pub async fn handle_download(args: EntryDownload) -> Next { async fn download_file(name: String) -> ResultDownloaded { tokio::time::sleep(Duration::from_secs(1)).await; - ResultDownloaded::new(name) + ResultDownloaded(name) } #[renderer] @@ -670,7 +680,7 @@ use mingling::macros::gen_program; gen_program!(); ``` -It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations. +It must be placed **after** all your `dispatcher!` calls, `#[derive(Grouped, Wrap)]` type definitions, and `#[chain]`, `#[renderer]`, and `#[help]` declarations. --- @@ -679,7 +689,6 @@ It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[rend Here's a complete, runnable program: ```rust -use mingling::macros::pack; use mingling::prelude::*; use std::io::Write; @@ -690,16 +699,17 @@ fn main() { program.exec_and_exit(); } -pack!(ResultGreeting = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { let greeting = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting).into() + ResultGreeting(greeting).into() } #[renderer] @@ -39,7 +39,8 @@ Mingling abstracts the behavior of a program's lifecycle into three phases: **Di ```rust dispatcher!("current", EntryCurrent); -pack!(StateNext = ()); +#[derive(Grouped, Wrap, Default)] +pub struct StateNext(()); #[chain] fn handle_current(_: EntryCurrent) -> StateNext { @@ -66,22 +67,26 @@ use mingling::macros::buffer; use mingling::prelude::*; dispatcher!("calc", EntryCalculate); -pack!(StateSumNumbers = Vec<i32>); -pack!(ResultNumber = i32); + +#[derive(Grouped, Wrap)] +pub struct StateSumNumbers(Vec<i32>); + +#[derive(Grouped, Wrap)] +pub struct ResultNumber(i32); // Entry: parse arguments and pass state to the calculation step #[chain] fn handle_calc(args: EntryCalculate) -> StateSumNumbers { let numbers = args.pick(&arg![Vec<i32>]).unwrap(); - StateSumNumbers::new(numbers) + StateSumNumbers(numbers) } // Calculate: pass the result to the rendering step #[chain] fn handle_state_sum_numbers(sum: StateSumNumbers) -> ResultNumber { - let numbers = sum.inner; + let numbers = sum.0; let total: i32 = numbers.iter().sum(); - ResultNumber::new(total) + ResultNumber(total) } // Renderer: return the render result and let the framework handle output 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/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(); diff --git a/examples/example-argument-picker/src/main.rs b/examples/example-argument-picker/src/main.rs index 99eee30..aefb86e 100644 --- a/examples/example-argument-picker/src/main.rs +++ b/examples/example-argument-picker/src/main.rs @@ -44,17 +44,32 @@ use mingling::setup::picker::BasicProgramSetup; dispatcher!("calc", EntryCalculate); -pack_err!(ErrorNumberANotProvided); -pack_err!(ErrorNumberBNotProvided); -pack_err!(ErrorNumberOperatorNotProvided); -pack_err!(ErrorDivisionByZero); +#[derive(Grouped, Default)] +pub struct ErrorNumberANotProvided; -pack!(StateAdd = (f32, f32)); -pack!(StateSubtract = (f32, f32)); -pack!(StateMultiply = (f32, f32)); -pack!(StateDivide = (f32, f32)); +#[derive(Grouped, Default)] +pub struct ErrorNumberBNotProvided; -pack!(ResultNumber = f32); +#[derive(Grouped, Default)] +pub struct ErrorNumberOperatorNotProvided; + +#[derive(Grouped, Default)] +pub struct ErrorDivisionByZero; + +#[derive(Grouped, Wrap)] +pub struct StateAdd((f32, f32)); + +#[derive(Grouped, Wrap)] +pub struct StateSubtract((f32, f32)); + +#[derive(Grouped, Wrap)] +pub struct StateMultiply((f32, f32)); + +#[derive(Grouped, Wrap)] +pub struct StateDivide((f32, f32)); + +#[derive(Grouped, Wrap)] +pub struct ResultNumber(f32); #[derive(Grouped)] struct StateCalculate { @@ -135,13 +150,13 @@ fn handle_calc(args: EntryCalculate) -> Next { // Use the arg! macro to define a positional argument of type f32 // | // vvvvvvvvvv - args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain()) + args.pick_or_route(&arg![f32], || ErrorNumberANotProvided.to_chain()) .pick_or_route(&arg![Operator], || { - ErrorNumberOperatorNotProvided::default().to_chain() + ErrorNumberOperatorNotProvided.to_chain() }) // Returns a routable type when not found or fails to parse // | - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain()) + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + .pick_or_route(&arg![f32], || ErrorNumberBNotProvided.to_chain()) // Use `to_result` to parse arguments // and convert to Result<(Tuple, ...), Route> type .to_result() @@ -149,7 +164,7 @@ fn handle_calc(args: EntryCalculate) -> Next { // --------- IMPORTANT --------- if operator == Operator::Slash && number_b == 0. { - return ErrorDivisionByZero::default().to_chain(); + return ErrorDivisionByZero.to_chain(); } StateCalculate { @@ -163,41 +178,41 @@ fn handle_calc(args: EntryCalculate) -> Next { #[chain] fn handle_state_calculate(state: StateCalculate) -> Next { match (state.operator, state.number_a, state.number_b) { - (Operator::Plus, a, b) => StateAdd::new((a, b)).to_chain(), - (Operator::Dash, a, b) => StateSubtract::new((a, b)).to_chain(), - (Operator::Slash, a, b) => StateDivide::new((a, b)).to_chain(), - (Operator::Star, a, b) => StateMultiply::new((a, b)).to_chain(), + (Operator::Plus, a, b) => StateAdd((a, b)).to_chain(), + (Operator::Dash, a, b) => StateSubtract((a, b)).to_chain(), + (Operator::Slash, a, b) => StateDivide((a, b)).to_chain(), + (Operator::Star, a, b) => StateMultiply((a, b)).to_chain(), } } #[chain] fn handle_state_add(state_add: StateAdd) -> ResultNumber { - let (a, b) = state_add.inner; - ResultNumber::new(a + b) + let (a, b) = state_add.0; + ResultNumber(a + b) } #[chain] fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber { - let (a, b) = state_subtract.inner; - ResultNumber::new(a - b) + let (a, b) = state_subtract.0; + ResultNumber(a - b) } #[chain] fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber { - let (a, b) = state_multiply.inner; - ResultNumber::new(a * b) + let (a, b) = state_multiply.0; + ResultNumber(a * b) } #[chain] fn handle_state_divide(state_divide: StateDivide) -> ResultNumber { - let (a, b) = state_divide.inner; - ResultNumber::new(a / b) + let (a, b) = state_divide.0; + ResultNumber(a / b) } #[renderer] fn render_result_number(result: ResultNumber, setting: &ResNumberDisplaySetting) -> String { let round = setting.round; - let result = if round { result.round() } else { result.inner }; + let result = if round { result.round() } else { result.0 }; format!("Result: {}", result) } diff --git a/examples/example-async-support/src/main.rs b/examples/example-async-support/src/main.rs index 090602a..7e212d9 100644 --- a/examples/example-async-support/src/main.rs +++ b/examples/example-async-support/src/main.rs @@ -41,7 +41,8 @@ async fn main() { dispatcher!("download", EntryDownload); -pack!(ResultDownloaded = String); +#[derive(Grouped, Wrap)] +pub struct ResultDownloaded(String); // --------- IMPORTANT --------- #[chain] @@ -65,5 +66,5 @@ gen_program!(); async fn fake_download(file_name: String) -> ResultDownloaded { tokio::time::sleep(std::time::Duration::from_secs(1)).await; - ResultDownloaded::new(file_name) + ResultDownloaded(file_name) } diff --git a/examples/example-basic/src/main.rs b/examples/example-basic/src/main.rs index 44736ad..ce41b2d 100644 --- a/examples/example-basic/src/main.rs +++ b/examples/example-basic/src/main.rs @@ -17,11 +17,11 @@ use mingling::prelude::*; use std::io::Write; // Define the `greet` subcommand -// _____________________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") -// / _____________________ dispatcher name -// | / _________ entry, records raw arguments -// | | / ^^^^^^^^^^^^^ -// vvvvv vvvvvvvv vvvvvvvvvv \_ equivalent to pack!(EntryGreet = Vec<String>) +// _________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") +// / +// | _________ entry, records raw arguments +// | / ^^^^^^^^^^^^^ +// vvvvv vvvvvvvvvv \_ a newtype wrapper around Vec<String> dispatcher!("greet", EntryGreet); fn main() { @@ -33,21 +33,22 @@ fn main() { } // Quickly wrap a type into a type recognizable by the current program -// ____________________ Wrapped type name -// / _______ Wrapped type inner value -// | / -// vvvvvvvvvv vvvvvv -pack!(ResultName = String); +// ___________________ Registers this type into ThisProgram +// / _______ Adds DerefMut, Deref, Into, From wrappers +// | / +// vvvvvvvvvv vvvvv +#[derive(Grouped, Wrap)] +pub struct ResultName(String); // Define the `handle_greet` chain for parsing input text // ____________________ Previous type: // / Mingling deduces types at runtime and routes them to this function // | _____ will be expanded to: -// | / impl Into<mingling::ChainProcess<ThisProgram>> +// | / ChainProcess<ThisProgram> #[chain] // vvvvvvvvvv vvvv fn handle_greet(args: EntryGreet) -> Next { let name: ResultName = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) diff --git a/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs b/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs index 2b7aba9..b04c683 100644 --- a/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs +++ b/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs @@ -4,12 +4,13 @@ use std::io::Write; dispatcher!("hello"); -pack!(ResultMessage = String); +#[derive(Grouped, Wrap)] +pub struct ResultMessage(String); #[chain] pub fn handle_my(args: EntryHello) -> Next { let name: ResultMessage = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) diff --git a/examples/example-combine-pathf-metadata/src/sub/mod.rs b/examples/example-combine-pathf-metadata/src/sub/mod.rs index ba56285..38efa2f 100644 --- a/examples/example-combine-pathf-metadata/src/sub/mod.rs +++ b/examples/example-combine-pathf-metadata/src/sub/mod.rs @@ -7,6 +7,7 @@ use std::io::Write; // Implicit dispatcher form — creates `CMDHello` / `EntryHello` in this module dispatcher!("hello"); + // Creates `CMDDescription` / `EntryDescription` in this module dispatcher!("desc", EntryDescription); @@ -27,14 +28,17 @@ pub fn hello_desc() -> Description { } } -pack!(ResultName = String); -pack!(DescResult = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); + +#[derive(Grouped, Wrap)] +pub struct DescResult(String); /// Chain for `hello` — reads the name and produces a `ResultName`. #[chain] pub fn handle_hello(args: EntryHello) -> Next { let name: ResultName = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) @@ -51,7 +55,7 @@ pub fn handle_desc(_args: EntryDescription) -> Next { None => "EntryHello has no description".to_string(), }; // --------- IMPORTANT --------- - DescResult::new(msg).to_render() + DescResult(msg).to_render() } /// Renders the greeting message with the provided name. diff --git a/examples/example-command-macro/src/main.rs b/examples/example-command-macro/src/main.rs index e525098..6740518 100644 --- a/examples/example-command-macro/src/main.rs +++ b/examples/example-command-macro/src/main.rs @@ -22,27 +22,30 @@ fn main() { ThisProgram::new().exec_and_exit(); } -pack!(ResultGreeting = String); -pack!(ResultGoodbye = ()); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); + +#[derive(Grouped)] +pub struct ResultGoodbye; // --------- IMPORTANT --------- // Auto-generates dispatcher!("hello.world", EntryHelloWorld); #[command] fn hello_world() -> ResultGreeting { - ResultGreeting::new("World".to_string()) + ResultGreeting("World".to_string()) } // Auto-generates dispatcher!("hello-world", EntryGreetSomeone); #[command(node = "greet-someone")] fn greet_someone(args: Vec<String>) -> ResultGreeting { let name = args.pick_or(&arg![String], || "World".to_string()).unwrap(); - ResultGreeting::new(name) + ResultGreeting(name) } // Auto-generates dispatcher!("goodbye", EntryGoodBye); #[command(entry = EntryGoodBye)] fn goodbye() -> ResultGoodbye { - ResultGoodbye::default() + ResultGoodbye } // --------- IMPORTANT --------- diff --git a/examples/example-completion/src/main.rs b/examples/example-completion/src/main.rs index d363697..e14326f 100644 --- a/examples/example-completion/src/main.rs +++ b/examples/example-completion/src/main.rs @@ -101,7 +101,8 @@ fn complete_greet_entry(ctx: &ShellContext) -> Suggest { // --------- IMPORTANT --------- dispatcher!("greet", EntryGreet); -pack!(ResultName = (u8, String)); +#[derive(Grouped, Wrap)] +pub struct ResultName((u8, String)); #[chain] fn handle_greet(args: EntryGreet) -> Next { @@ -116,7 +117,7 @@ fn handle_greet(args: EntryGreet) -> Next { /// Renders the greeting with the result name and repeat count. #[renderer] fn render_name(result: ResultName) -> RenderResult { - let (repeat, name) = result.inner; + let (repeat, name) = result.0; let mut render_result = RenderResult::new(); let mut parts = Vec::with_capacity(repeat as usize); for _ in 0..repeat { diff --git a/examples/example-error-handling/src/main.rs b/examples/example-error-handling/src/main.rs index 05b451b..ec7e6c9 100644 --- a/examples/example-error-handling/src/main.rs +++ b/examples/example-error-handling/src/main.rs @@ -29,35 +29,41 @@ use std::io::Write; dispatcher!("hello", EntryHello); // Define error types -pack!(ErrorNoNameProvided = ()); -pack!(ErrorNameTooLong = u16); -pack!(ErrorNameNotAvailable = ()); +#[derive(Grouped)] +pub struct ErrorNoNameProvided; + +#[derive(Grouped, Wrap)] +pub struct ErrorNameTooLong(u16); + +#[derive(Grouped)] +pub struct ErrorNameNotAvailable; // Define success type -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); // Pre-registered names static VEC_REGISTERED_NAMES: &[&str] = &["Alice", "Bob", "Charlie", "David", "Eve"]; #[chain] fn handle_hello(args: EntryHello) -> Next { - let Some(name) = args.inner.first().cloned() else { + let Some(name) = args.0.first().cloned() else { // If no name is provided, pass ErrorNoNameProvided - return ErrorNoNameProvided::default().to_render(); + return ErrorNoNameProvided.to_render(); }; if name.len() > 10 { // If the name is too long, pass ErrorNameTooLong - return ErrorNameTooLong::new(name.len() as u16).to_render(); + return ErrorNameTooLong(name.len() as u16).to_render(); } if VEC_REGISTERED_NAMES.contains(&name.as_str()) { // If the name already exists, pass ErrorNameNotAvailable - return ErrorNameNotAvailable::default().to_render(); + return ErrorNameNotAvailable.to_render(); } // If the name is valid, pass ResultName - ResultName::new(name).to_render() + ResultName(name).to_render() } /// Renders a successful greeting with the given name. @@ -96,12 +102,7 @@ fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { #[renderer] fn render_entry_fallback(err: EntryFallback) -> RenderResult { let mut render_result = RenderResult::new(); - writeln!( - render_result, - "Command not found: \"{}\"", - err.inner.join(" ") - ) - .ok(); + writeln!(render_result, "Command not found: \"{}\"", err.0.join(" ")).ok(); render_result } diff --git a/examples/example-exitcode/src/main.rs b/examples/example-exitcode/src/main.rs index d3e035e..c7f731d 100644 --- a/examples/example-exitcode/src/main.rs +++ b/examples/example-exitcode/src/main.rs @@ -36,18 +36,21 @@ fn main() { dispatcher!("hello", EntryHello); -pack!(ErrorNoNameProvided = ()); -pack!(ResultName = String); +#[derive(Grouped)] +pub struct ErrorNoNameProvided; + +#[derive(Grouped, Wrap)] +pub struct ResultName(String); #[chain] fn handle_hello(args: EntryHello) -> Next { - let Some(name) = args.inner.first().cloned() else { + let Some(name) = args.0.first().cloned() else { // If no name is provided, pass ErrorNoNameProvided - return ErrorNoNameProvided::default().to_render(); + return ErrorNoNameProvided.to_render(); }; // If the name is valid, pass ResultName - ResultName::new(name).to_render() + ResultName(name).to_render() } /// Renders a successful greeting with the given name. diff --git a/examples/example-hook/src/main.rs b/examples/example-hook/src/main.rs index 1807e5e..721cea0 100644 --- a/examples/example-hook/src/main.rs +++ b/examples/example-hook/src/main.rs @@ -53,12 +53,13 @@ fn main() { program.exec_and_exit(); } -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { let name: ResultName = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) diff --git a/examples/example-lazy-resources/src/main.rs b/examples/example-lazy-resources/src/main.rs index cc1604a..2243f14 100644 --- a/examples/example-lazy-resources/src/main.rs +++ b/examples/example-lazy-resources/src/main.rs @@ -52,7 +52,8 @@ fn init_res_large_data() -> ResLargeData { dispatcher!("show", EntryShow); dispatcher!("none", EntryNone); -pack!(ResultShow = BTreeMap<Key, Value>); +#[derive(Grouped, Wrap)] +pub struct ResultShow(BTreeMap<Key, Value>); fn main() { let mut program = ThisProgram::new(); diff --git a/examples/example-metadata/src/main.rs b/examples/example-metadata/src/main.rs index 94facb4..99a8aae 100644 --- a/examples/example-metadata/src/main.rs +++ b/examples/example-metadata/src/main.rs @@ -56,14 +56,17 @@ pub fn greet_desc() -> Description { } // --------- IMPORTANT --------- -pack!(ResultName = String); -pack!(DescResult = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); + +#[derive(Grouped, Wrap)] +pub struct DescResult(String); /// Chain for `greet` — reads the name and produces a `ResultName`. #[chain] fn handle_greet(args: EntryGreet) -> Next { let name: ResultName = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) @@ -81,7 +84,7 @@ fn handle_desc(_args: EntryDescription) -> Next { None => "EntryGreet has no description".to_string(), }; // --------- IMPORTANT --------- - DescResult::new(msg).to_render() + DescResult(msg).to_render() } /// Chain for `nodoc` — asks for metadata on an entry that has none. @@ -94,7 +97,7 @@ fn handle_nodoc(_args: EntryNoDescription) -> Next { None => "EntryDescription has no description".to_string(), }; // --------- IMPORTANT --------- - DescResult::new(msg).to_render() + DescResult(msg).to_render() } /// Renders the greeting message with the provided name. diff --git a/examples/example-outside-type/src/main.rs b/examples/example-outside-type/src/main.rs index a04727f..8721d9a 100644 --- a/examples/example-outside-type/src/main.rs +++ b/examples/example-outside-type/src/main.rs @@ -43,7 +43,8 @@ group!(ErrorIo = std::io::Error); // you can use this syntax to create an alias simultaneously // --------- IMPORTANT --------- -pack!(ParsedNumber = i32); +#[derive(Grouped, Wrap)] +pub struct ParsedNumber(i32); /// Parse the first argument as an `i32` /// @@ -51,9 +52,9 @@ pack!(ParsedNumber = i32); /// On failure, routes to `render_parse_error` via the registered outside type. #[chain] fn parse_number(args: EntryParse) -> Next { - let input = args.inner.first().cloned().unwrap_or_default(); + let input = args.0.first().cloned().unwrap_or_default(); match input.parse::<i32>() { - Ok(num) => ParsedNumber::new(num).to_chain(), + Ok(num) => ParsedNumber(num).to_chain(), Err(e) => e.to_chain(), } } diff --git a/examples/example-pack-err/Cargo.lock b/examples/example-pack-err/Cargo.lock deleted file mode 100644 index bcfbb41..0000000 --- a/examples/example-pack-err/Cargo.lock +++ /dev/null @@ -1,235 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "example-pack-err" -version = "0.1.0" -dependencies = [ - "mingling", - "serde", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "just_fmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "might_be_async" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca871cea620b07bd2e6da0c883891a25bead698c43a9ab64b0fd663a7a78d5f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "toml", -] - -[[package]] -name = "mingling" -version = "0.5.0" -dependencies = [ - "mingling_core", - "mingling_macros", - "serde", -] - -[[package]] -name = "mingling_core" -version = "0.5.0" -dependencies = [ - "just_fmt", - "might_be_async", - "serde", - "serde_json", -] - -[[package]] -name = "mingling_macros" -version = "0.5.0" -dependencies = [ - "just_fmt", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/example-pack-err/Cargo.toml b/examples/example-pack-err/Cargo.toml deleted file mode 100644 index ddab6fd..0000000 --- a/examples/example-pack-err/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "example-pack-err" -version = "0.1.0" -edition = "2024" - -[dependencies] -serde = { version = "1.0.228", features = ["derive"] } - -[dependencies.mingling] -path = "../../mingling" -features = [ - "structural_renderer", - "extras", -] - -[workspace] diff --git a/examples/example-pack-err/page.toml b/examples/example-pack-err/page.toml deleted file mode 100644 index 7423087..0000000 --- a/examples/example-pack-err/page.toml +++ /dev/null @@ -1,10 +0,0 @@ -[example] -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. -""" -tags = ["pack_err!", "extras", "structural_renderer", "--json"] -files = ["src/main.rs", "Cargo.toml"] diff --git a/examples/example-pack-err/src/main.rs b/examples/example-pack-err/src/main.rs deleted file mode 100644 index e30e4cb..0000000 --- a/examples/example-pack-err/src/main.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Example `pack_err!` -//! -//! > This example demonstrates how to use the `pack_err!` macro to define error types -//! > with automatic `name` field (set to snake_case at compile time) and optional `info` field. -//! > Also demonstrates `--json` serialization when `structural_renderer` is enabled. -//! -//! Run: -//! ```bash -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural --json -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural Cargo.toml --json -//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural src --json -//! ``` -//! -//! Output: -//! ```plaintext -//! Search path not provided -//! Not a directory: Cargo.toml -//! Found directory: src -//! {"name":"error_not_found"} -//! {"name":"error_not_dir","info":"Cargo.toml"} -//! {"inner":"src"} -//! {"name":"error_not_found_structural"} -//! {"name":"error_not_dir_structural","info":"Cargo.toml"} -//! ``` - -use mingling::prelude::*; -use mingling::setup::StructuralRendererSetup; -use std::io::Write; -use std::path::PathBuf; - -dispatcher!("find", EntryFind); -dispatcher!("find-structural", EntryFindStructural); - -// --------- IMPORTANT --------- -// `pack_err!` is a convenient macro for defining error types. -// -// Simple form: pack_err!(ErrorNotFound); -// Typed form: pack_err!(ErrorNotDir = PathBuf); -// -// The simple form generates a struct with `name: String` and `impl Default`. -// name = "error_not_found" (automatically snake_cased at compile time) -// -// The typed form additionally generates `pub fn new(info)`. -// name = "error_not_dir" -// -// When `structural_renderer` is enabled, the struct also gets -// `#[derive(serde::Serialize)]` for --json / --yaml output. -// --------- IMPORTANT --------- - -// Simple form — name = "error_not_found" -pack_err!(ErrorNotFound); - -// Typed form — name = "error_not_dir" -pack_err!(ErrorNotDir = PathBuf); - -// Simple form — with StructuralData support for --json / --yaml -pack_err_structural!(ErrorNotFoundStructural); - -// Typed form — with StructuralData support for --json / --yaml -pack_err_structural!(ErrorNotDirStructural = PathBuf); - -// Success type with StructuralData support -pack_structural!(ResultPath = PathBuf); - -#[chain] -fn handle_find(args: EntryFind) -> Next { - let Some(path_str) = args.inner.first().cloned() else { - // No path provided → use the simple error form (Default) - return ErrorNotFound::default().to_render(); - }; - - let path = PathBuf::from(&path_str); - if path.is_dir() { - // Is a directory → success - ResultPath::new(path).to_render() - } else { - // Not a directory (or doesn't exist) → use the typed error form - ErrorNotDir::new(path).to_render() - } -} - -#[chain] -fn handle_find_structural(args: EntryFindStructural) -> Next { - let Some(path_str) = args.inner.first().cloned() else { - // No path provided → use the simple error form (Default) - return ErrorNotFoundStructural::default().to_render(); - }; - - let path = PathBuf::from(&path_str); - if path.is_dir() { - // Is a directory → success - ResultPath::new(path).to_render() - } else { - // Not a directory (or doesn't exist) → use the typed error form - ErrorNotDirStructural::new(path).to_render() - } -} - -/// Renders the successful result with the found directory path. -#[renderer] -fn render_result_path(path: ResultPath) -> RenderResult { - let mut render_result = RenderResult::new(); - writeln!(render_result, "Found directory: {}", path.display()).ok(); - render_result -} - -/// Renders the error when no search path is provided. -#[renderer] -fn render_error_not_found(_: ErrorNotFound) -> RenderResult { - let mut render_result = RenderResult::new(); - writeln!(render_result, "Search path not provided").ok(); - render_result -} - -/// Renders the error when the given path is not a directory. -#[renderer] -fn render_error_not_dir(err: ErrorNotDir) -> RenderResult { - let mut render_result = RenderResult::new(); - writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); - render_result -} - -/// Renders the structural error when no search path is provided. -#[renderer] -fn render_error_not_found_structural(_: ErrorNotFoundStructural) -> RenderResult { - let mut render_result = RenderResult::new(); - writeln!(render_result, "Search path not provided").ok(); - render_result -} - -/// Renders the structural error when the given path is not a directory. -#[renderer] -fn render_error_not_dir_structural(err: ErrorNotDirStructural) -> RenderResult { - let mut render_result = RenderResult::new(); - writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); - render_result -} - -gen_program!(); - -fn main() { - let mut program = ThisProgram::new(); - - // Add StructuralRendererSetup to support --json / --yaml flags - program.with_setup(StructuralRendererSetup); - - let _ = program.exec(); -} diff --git a/examples/example-pack-err/test.toml b/examples/example-pack-err/test.toml deleted file mode 100644 index c4509cb..0000000 --- a/examples/example-pack-err/test.toml +++ /dev/null @@ -1,35 +0,0 @@ -[[runs]] -input = [ "find" ] - -expect.exit-code = 0 -expect.result = "Search path not provided" - -[[runs]] -input = [ "find", "Cargo.toml" ] - -expect.exit-code = 0 -expect.result = "Not a directory: Cargo.toml" - -[[runs]] -input = [ "find", "examples" ] - -expect.exit-code = 0 -expect.result = "Found directory: examples" - -[[runs]] -input = [ "find-structural", "--json" ] - -expect.exit-code = 0 -expect.result = "{\"name\":\"error_not_found_structural\"}" - -[[runs]] -input = [ "find-structural", "Cargo.toml", "--json" ] - -expect.exit-code = 0 -expect.result = "{\"name\":\"error_not_dir_structural\",\"info\":\"Cargo.toml\"}" - -[[runs]] -input = [ "find-structural", "examples", "--json" ] - -expect.exit-code = 0 -expect.result = "{\"inner\":\"examples\"}" diff --git a/examples/example-panic-unwind/src/main.rs b/examples/example-panic-unwind/src/main.rs index 59adf07..f91b0f0 100644 --- a/examples/example-panic-unwind/src/main.rs +++ b/examples/example-panic-unwind/src/main.rs @@ -20,7 +20,9 @@ use mingling::{hook::ProgramHook, prelude::*}; use std::io::Write; dispatcher!("panic", EntryPanic); -pack!(NotPanic = ()); + +#[derive(Grouped)] +pub struct NotPanic; fn main() { let mut program = ThisProgram::new(); @@ -47,7 +49,7 @@ fn handle_panic(prev: EntryPanic) -> Next { // Panic happens here, will be caught panic!("{}", s) } - None => NotPanic::default().into(), + None => NotPanic.into(), } } diff --git a/examples/example-pathfinder/src/sub/mod.rs b/examples/example-pathfinder/src/sub/mod.rs index cf0a97a..b90c618 100644 --- a/examples/example-pathfinder/src/sub/mod.rs +++ b/examples/example-pathfinder/src/sub/mod.rs @@ -3,12 +3,14 @@ use mingling::prelude::*; use std::io::Write; dispatcher!("greet", EntryGreet); -pack!(ResultName = String); + +#[derive(Grouped, Wrap)] +pub struct ResultName(String); #[chain] pub fn handle_greet(args: EntryGreet) -> Next { let name: ResultName = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) diff --git a/examples/example-repl-basic/src/main.rs b/examples/example-repl-basic/src/main.rs index d0fad6d..9b81ced 100644 --- a/examples/example-repl-basic/src/main.rs +++ b/examples/example-repl-basic/src/main.rs @@ -70,7 +70,8 @@ fn main() { } // Create error route -pack!(ErrorDirectoryNotExist = PathBuf); +#[derive(Grouped, Wrap)] +pub struct ErrorDirectoryNotExist(PathBuf); // Create commands: cd ls exit dispatcher!("cd", EntryCd); @@ -79,16 +80,18 @@ dispatcher!("exit", EntryExit); dispatcher!("clear", EntryClear); // Define data needed for the cd command's execution phase -pack!(StateChangeDirectory = String); +#[derive(Grouped, Wrap)] +pub struct StateChangeDirectory(String); // Define data needed for the ls command's rendering phase -pack!(ResultList = Vec<String>); +#[derive(Grouped, Wrap)] +pub struct ResultList(Vec<String>); // Parse cd command arguments #[chain] fn parse_cd_args(prev: EntryCd) -> Next { let join = prev.pick_or_default(&arg![String]).unwrap(); - StateChangeDirectory::new(join).into() + StateChangeDirectory(join).into() } // Execute directory change @@ -96,12 +99,12 @@ fn parse_cd_args(prev: EntryCd) -> Next { fn handle_cd(prev: StateChangeDirectory, current_dir: &mut ResCurrentDir) -> Next { use just_fmt::fmt_path::fmt_path; - let join = prev.inner; + let join = prev.0; let new_dir = fmt_path(current_dir.dir.join(join)).unwrap_or_default(); // If the path is not found, route to error handling if !new_dir.exists() { - return ErrorDirectoryNotExist::new(new_dir).to_render(); + return ErrorDirectoryNotExist(new_dir).to_render(); } current_dir.dir = new_dir; @@ -126,14 +129,14 @@ fn handle_ls(_prev: EntryLs, current_dir: &ResCurrentDir) -> Next { .collect(); // Render ResultList - ResultList::new(entries).to_render() + ResultList(entries).to_render() } /// Render ResultList data #[renderer] fn render_list(list: ResultList) -> RenderResult { let mut render_result = RenderResult::new(); - for item in list.inner { + for item in list.0 { writeln!(render_result, "{}", item).ok(); } render_result @@ -160,12 +163,7 @@ fn handle_clear(_prev: EntryClear) { #[renderer] fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) -> RenderResult { let mut render_result = RenderResult::new(); - writeln!( - render_result, - "Directory not found: {}", - err.inner.display() - ) - .ok(); + writeln!(render_result, "Directory not found: {}", err.0.display()).ok(); render_result } diff --git a/examples/example-setup/src/main.rs b/examples/example-setup/src/main.rs index 523a567..59c503f 100644 --- a/examples/example-setup/src/main.rs +++ b/examples/example-setup/src/main.rs @@ -55,13 +55,14 @@ fn custom_setup(program: &mut Program<ThisProgram>) { dispatcher!("greet", EntryGreet); -pack!(ResultGreeting = String); +#[derive(Grouped, Wrap)] +pub struct ResultGreeting(String); /// Chain: reads the `ResAppName` and `ResAppVersion` resources. #[chain] fn handle_greet(args: EntryGreet, app: &ResAppName, version: &ResAppVersion) -> Next { let who = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()); diff --git a/examples/example-structural-renderer/src/main.rs b/examples/example-structural-renderer/src/main.rs index a1bddbd..c1cfd69 100644 --- a/examples/example-structural-renderer/src/main.rs +++ b/examples/example-structural-renderer/src/main.rs @@ -32,7 +32,8 @@ fn main() { } // --------- IMPORTANT --------- -// For beautiful output structure, do not use `pack!` to wrap the types that need to be output. +// For beautiful output structure, do not wrap the types that need to be output +// in a newtype; instead, use a named struct. // Instead, manually implement // ____________________________________ Mark as structured data so it can be rendered // / ____________________ Implement serde::Serialize @@ -48,7 +49,7 @@ struct Info { } // This will output: {"member_name":"name","member_age":32} structure -// If using pack!(Info = (String, i32)); +// If wrapping with a tuple newtype (e.g. `#[derive(Grouped, Wrap)] pub struct Info((String, i32));`) // Output: {"inner":["name", 32]} // --------- IMPORTANT --------- diff --git a/examples/example-unit-test/src/main.rs b/examples/example-unit-test/src/main.rs index 29ff9da..e4c6504 100644 --- a/examples/example-unit-test/src/main.rs +++ b/examples/example-unit-test/src/main.rs @@ -36,30 +36,30 @@ mod tests { let hello_with_valid_name = handle_hello(entry!("Peter")).into(); assert_render_result!(hello_with_valid_name); let result_name = unpack_chain_process!(hello_with_valid_name, ResultName); - assert_eq!(result_name.inner, "Peter"); + assert_eq!(result_name.0, "Peter"); } #[test] fn test_render_result_name() { - let r = render_result_name(ResultName::new("Peter".into())); + let r = render_result_name(ResultName("Peter".into())); assert_eq!(r.to_string().as_str(), "Hello, Peter!") } #[test] fn test_render_error_no_name_provided() { - let r = render_error_no_name_provided(ErrorNoNameProvided::default()); + let r = render_error_no_name_provided(ErrorNoNameProvided); assert_eq!(r.to_string().as_str(), "No name provided") } #[test] fn test_render_error_name_not_available() { - let r = render_error_name_not_available(ErrorNameNotAvailable::default()); + let r = render_error_name_not_available(ErrorNameNotAvailable); assert_eq!(r.to_string().as_str(), "Name not available") } #[test] fn test_render_error_name_too_long() { - let r = render_error_name_too_long(ErrorNameTooLong::new(17)); + let r = render_error_name_too_long(ErrorNameTooLong(17)); assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10") } // --------- IMPORTANT --------- @@ -67,29 +67,35 @@ mod tests { dispatcher!("hello", EntryHello); -pack!(ErrorNoNameProvided = ()); -pack!(ErrorNameTooLong = u16); -pack!(ErrorNameNotAvailable = ()); +#[derive(Grouped)] +pub struct ErrorNoNameProvided; -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ErrorNameTooLong(u16); + +#[derive(Grouped)] +pub struct ErrorNameNotAvailable; + +#[derive(Grouped, Wrap)] +pub struct ResultName(String); static VEC_REGISTERED_NAMES: &[&str] = &["Alice", "Bob", "Charlie", "David", "Eve"]; #[chain] fn handle_hello(args: EntryHello) -> Next { - let Some(name) = args.inner.first().cloned() else { - return ErrorNoNameProvided::default().to_render(); + let Some(name) = args.0.first().cloned() else { + return ErrorNoNameProvided.to_render(); }; if name.len() > 10 { - return ErrorNameTooLong::new(name.len() as u16).to_render(); + return ErrorNameTooLong(name.len() as u16).to_render(); } if VEC_REGISTERED_NAMES.contains(&name.as_str()) { - return ErrorNameNotAvailable::default().to_render(); + return ErrorNameNotAvailable.to_render(); } - ResultName::new(name).to_render() + ResultName(name).to_render() } /// Renders a successful greeting with the given name. @@ -128,12 +134,7 @@ fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { #[renderer] fn render_entry_fallback(err: EntryFallback) -> RenderResult { let mut render_result = RenderResult::new(); - writeln!( - render_result, - "Command not found: \"{}\"", - err.inner.join(" ") - ) - .ok(); + writeln!(render_result, "Command not found: \"{}\"", err.0.join(" ")).ok(); render_result } diff --git a/examples/full-todolist/src/main.rs b/examples/full-todolist/src/main.rs index eea417a..9c7bfe8 100644 --- a/examples/full-todolist/src/main.rs +++ b/examples/full-todolist/src/main.rs @@ -36,15 +36,25 @@ dispatcher!("clean"); // Define states -pack!(StateAddTodo = String); -pack!(StateCompleteTodo = i32); -pack!(StateListTodo = bool); +#[derive(Grouped, Wrap)] +pub struct StateAddTodo(String); + +#[derive(Grouped, Wrap)] +pub struct StateCompleteTodo(i32); + +#[derive(Grouped, Wrap)] +pub struct StateListTodo(bool); // Define errors -pack!(ErrorNoTaskDescriptionProvided = ()); -pack!(ErrorNoIndexProvided = ()); -pack!(ErrorIndexOutOfBounds = ()); +#[derive(Grouped)] +pub struct ErrorNoTaskDescriptionProvided; + +#[derive(Grouped)] +pub struct ErrorNoIndexProvided; + +#[derive(Grouped)] +pub struct ErrorIndexOutOfBounds; fn main() { let mut program = ThisProgram::new(); @@ -76,11 +86,11 @@ fn main() { fn handle_add(args: EntryAdd) -> Next { let task: String = route! { args.pick_or_route(&arg![String], || { - ErrorNoTaskDescriptionProvided::new(()).to_chain() + ErrorNoTaskDescriptionProvided.to_chain() }) .to_result() }; - StateAddTodo::new(task).to_chain() + StateAddTodo(task).to_chain() } #[chain] @@ -92,7 +102,7 @@ fn handle_state_add_todo( let todolist = todolist.get_mut(); // Unpack state and read description - let description = state.inner; + let description = state.0; todolist.items.push(Todo { item: description, @@ -117,10 +127,10 @@ fn handle_list(_args: EntryList, todolist: &mut LazyRes<ResTodoList>) -> Next { #[chain] fn handle_complete(args: EntryComplete) -> Next { let index: i32 = route! { - args.pick_or_route(&arg![i32], || ErrorNoIndexProvided::new(()).to_chain()) + args.pick_or_route(&arg![i32], || ErrorNoIndexProvided.to_chain()) .to_result() }; - StateCompleteTodo::new(index).to_chain() + StateCompleteTodo(index).to_chain() } #[chain] @@ -129,12 +139,12 @@ fn handle_state_complete_todo( todolist: &mut LazyRes<ResTodoList>, ) -> Next { let todolist = todolist.get_mut(); - let index = state.inner as usize; + let index = state.0 as usize; if index < todolist.items.len() { todolist.items[index].completed = true; todolist.clone().to_render() } else { - ErrorIndexOutOfBounds::new(()).to_render() + ErrorIndexOutOfBounds.to_render() } } diff --git a/mingling/src/docs/lib.md b/mingling/src/docs/lib.md index 91858d0..3b1f328 100644 --- a/mingling/src/docs/lib.md +++ b/mingling/src/docs/lib.md @@ -29,12 +29,13 @@ fn main() { program.exec_and_exit(); } -pack!(ResultName = String); +#[derive(Grouped, Wrap)] +pub struct ResultName(String); #[chain] fn handle_greet(args: EntryGreet) -> Next { let name: ResultName = args - .inner + .0 .first() .cloned() .unwrap_or_else(|| "World".to_string()) diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 9406ddc..d37e762 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -64,17 +64,32 @@ /// /// dispatcher!("calc", EntryCalculate); /// -/// pack_err!(ErrorNumberANotProvided); -/// pack_err!(ErrorNumberBNotProvided); -/// pack_err!(ErrorNumberOperatorNotProvided); -/// pack_err!(ErrorDivisionByZero); +/// #[derive(Grouped, Default)] +/// pub struct ErrorNumberANotProvided; /// -/// pack!(StateAdd = (f32, f32)); -/// pack!(StateSubtract = (f32, f32)); -/// pack!(StateMultiply = (f32, f32)); -/// pack!(StateDivide = (f32, f32)); +/// #[derive(Grouped, Default)] +/// pub struct ErrorNumberBNotProvided; /// -/// pack!(ResultNumber = f32); +/// #[derive(Grouped, Default)] +/// pub struct ErrorNumberOperatorNotProvided; +/// +/// #[derive(Grouped, Default)] +/// pub struct ErrorDivisionByZero; +/// +/// #[derive(Grouped, Wrap)] +/// pub struct StateAdd((f32, f32)); +/// +/// #[derive(Grouped, Wrap)] +/// pub struct StateSubtract((f32, f32)); +/// +/// #[derive(Grouped, Wrap)] +/// pub struct StateMultiply((f32, f32)); +/// +/// #[derive(Grouped, Wrap)] +/// pub struct StateDivide((f32, f32)); +/// +/// #[derive(Grouped, Wrap)] +/// pub struct ResultNumber(f32); /// /// #[derive(Grouped)] /// struct StateCalculate { @@ -155,13 +170,13 @@ /// // Use the arg! macro to define a positional argument of type f32 /// // | /// // vvvvvvvvvv -/// args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain()) +/// args.pick_or_route(&arg![f32], || ErrorNumberANotProvided.to_chain()) /// .pick_or_route(&arg![Operator], || { -/// ErrorNumberOperatorNotProvided::default().to_chain() +/// ErrorNumberOperatorNotProvided.to_chain() /// }) // Returns a routable type when not found or fails to parse /// // | -/// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv -/// .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain()) +/// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +/// .pick_or_route(&arg![f32], || ErrorNumberBNotProvided.to_chain()) /// // Use `to_result` to parse arguments /// // and convert to Result<(Tuple, ...), Route> type /// .to_result() @@ -169,7 +184,7 @@ /// // --------- IMPORTANT --------- /// /// if operator == Operator::Slash && number_b == 0. { -/// return ErrorDivisionByZero::default().to_chain(); +/// return ErrorDivisionByZero.to_chain(); /// } /// /// StateCalculate { @@ -183,41 +198,41 @@ /// #[chain] /// fn handle_state_calculate(state: StateCalculate) -> Next { /// match (state.operator, state.number_a, state.number_b) { -/// (Operator::Plus, a, b) => StateAdd::new((a, b)).to_chain(), -/// (Operator::Dash, a, b) => StateSubtract::new((a, b)).to_chain(), -/// (Operator::Slash, a, b) => StateDivide::new((a, b)).to_chain(), -/// (Operator::Star, a, b) => StateMultiply::new((a, b)).to_chain(), +/// (Operator::Plus, a, b) => StateAdd((a, b)).to_chain(), +/// (Operator::Dash, a, b) => StateSubtract((a, b)).to_chain(), +/// (Operator::Slash, a, b) => StateDivide((a, b)).to_chain(), +/// (Operator::Star, a, b) => StateMultiply((a, b)).to_chain(), /// } /// } /// /// #[chain] /// fn handle_state_add(state_add: StateAdd) -> ResultNumber { -/// let (a, b) = state_add.inner; -/// ResultNumber::new(a + b) +/// let (a, b) = state_add.0; +/// ResultNumber(a + b) /// } /// /// #[chain] /// fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber { -/// let (a, b) = state_subtract.inner; -/// ResultNumber::new(a - b) +/// let (a, b) = state_subtract.0; +/// ResultNumber(a - b) /// } /// /// #[chain] /// fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber { -/// let (a, b) = state_multiply.inner; -/// ResultNumber::new(a * b) +/// let (a, b) = state_multiply.0; +/// ResultNumber(a * b) /// } /// /// #[chain] /// fn handle_state_divide(state_divide: StateDivide) -> ResultNumber { -/// let (a, b) = state_divide.inner; -/// ResultNumber::new(a / b) +/// let (a, b) = state_divide.0; +/// ResultNumber(a / b) /// } /// /// #[renderer] /// fn render_result_number(result: ResultNumber, setting: &ResNumberDisplaySetting) -> String { /// let round = setting.round; -/// let result = if round { result.round() } else { result.inner }; +/// let result = if round { result.round() } else { result.0 }; /// format!("Result: {}", result) /// } /// @@ -310,7 +325,8 @@ pub mod example_argument_picker {} /// /// dispatcher!("download", EntryDownload); /// -/// pack!(ResultDownloaded = String); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultDownloaded(String); /// /// // --------- IMPORTANT --------- /// #[chain] @@ -334,7 +350,7 @@ pub mod example_argument_picker {} /// /// async fn fake_download(file_name: String) -> ResultDownloaded { /// tokio::time::sleep(std::time::Duration::from_secs(1)).await; -/// ResultDownloaded::new(file_name) +/// ResultDownloaded(file_name) /// } /// ``` pub mod example_async_support {} @@ -372,11 +388,11 @@ pub mod example_async_support {} /// use std::io::Write; /// /// // Define the `greet` subcommand -/// // _____________________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") -/// // / _____________________ dispatcher name -/// // | / _________ entry, records raw arguments -/// // | | / ^^^^^^^^^^^^^ -/// // vvvvv vvvvvvvv vvvvvvvvvv \_ equivalent to pack!(EntryGreet = Vec<String>) +/// // _________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") +/// // / +/// // | _________ entry, records raw arguments +/// // | / ^^^^^^^^^^^^^ +/// // vvvvv vvvvvvvvvv \_ a newtype wrapper around Vec<String> /// dispatcher!("greet", EntryGreet); /// /// fn main() { @@ -388,21 +404,22 @@ pub mod example_async_support {} /// } /// /// // Quickly wrap a type into a type recognizable by the current program -/// // ____________________ Wrapped type name -/// // / _______ Wrapped type inner value -/// // | / -/// // vvvvvvvvvv vvvvvv -/// pack!(ResultName = String); +/// // ___________________ Registers this type into ThisProgram +/// // / _______ Adds DerefMut, Deref, Into, From wrappers +/// // | / +/// // vvvvvvvvvv vvvvv +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName(String); /// /// // Define the `handle_greet` chain for parsing input text /// // ____________________ Previous type: /// // / Mingling deduces types at runtime and routes them to this function /// // | _____ will be expanded to: -/// // | / impl Into<mingling::ChainProcess<ThisProgram>> +/// // | / ChainProcess<ThisProgram> /// #[chain] // vvvvvvvvvv vvvv /// fn handle_greet(args: EntryGreet) -> Next { /// let name: ResultName = args -/// .inner +/// .0 /// .first() /// .cloned() /// .unwrap_or_else(|| "World".to_string()) @@ -745,27 +762,30 @@ pub mod example_combine_pathf_metadata {} /// ThisProgram::new().exec_and_exit(); /// } /// -/// pack!(ResultGreeting = String); -/// pack!(ResultGoodbye = ()); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultGreeting(String); +/// +/// #[derive(Grouped)] +/// pub struct ResultGoodbye; /// /// // --------- IMPORTANT --------- /// // Auto-generates dispatcher!("hello.world", EntryHelloWorld); /// #[command] /// fn hello_world() -> ResultGreeting { -/// ResultGreeting::new("World".to_string()) +/// ResultGreeting("World".to_string()) /// } /// /// // Auto-generates dispatcher!("hello-world", EntryGreetSomeone); /// #[command(node = "greet-someone")] /// fn greet_someone(args: Vec<String>) -> ResultGreeting { /// let name = args.pick_or(&arg![String], || "World".to_string()).unwrap(); -/// ResultGreeting::new(name) +/// ResultGreeting(name) /// } /// /// // Auto-generates dispatcher!("goodbye", EntryGoodBye); /// #[command(entry = EntryGoodBye)] /// fn goodbye() -> ResultGoodbye { -/// ResultGoodbye::default() +/// ResultGoodbye /// } /// // --------- IMPORTANT --------- /// @@ -918,7 +938,8 @@ pub mod example_command_macro {} /// // --------- IMPORTANT --------- /// /// dispatcher!("greet", EntryGreet); -/// pack!(ResultName = (u8, String)); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName((u8, String)); /// /// #[chain] /// fn handle_greet(args: EntryGreet) -> Next { @@ -933,7 +954,7 @@ pub mod example_command_macro {} /// /// Renders the greeting with the result name and repeat count. /// #[renderer] /// fn render_name(result: ResultName) -> RenderResult { -/// let (repeat, name) = result.inner; +/// let (repeat, name) = result.0; /// let mut render_result = RenderResult::new(); /// let mut parts = Vec::with_capacity(repeat as usize); /// for _ in 0..repeat { @@ -1215,35 +1236,41 @@ pub mod example_enum_tag {} /// dispatcher!("hello", EntryHello); /// /// // Define error types -/// pack!(ErrorNoNameProvided = ()); -/// pack!(ErrorNameTooLong = u16); -/// pack!(ErrorNameNotAvailable = ()); +/// #[derive(Grouped)] +/// pub struct ErrorNoNameProvided; +/// +/// #[derive(Grouped, Wrap)] +/// pub struct ErrorNameTooLong(u16); +/// +/// #[derive(Grouped)] +/// pub struct ErrorNameNotAvailable; /// /// // Define success type -/// pack!(ResultName = String); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName(String); /// /// // Pre-registered names /// static VEC_REGISTERED_NAMES: &[&str] = &["Alice", "Bob", "Charlie", "David", "Eve"]; /// /// #[chain] /// fn handle_hello(args: EntryHello) -> Next { -/// let Some(name) = args.inner.first().cloned() else { +/// let Some(name) = args.0.first().cloned() else { /// // If no name is provided, pass ErrorNoNameProvided -/// return ErrorNoNameProvided::default().to_render(); +/// return ErrorNoNameProvided.to_render(); /// }; /// /// if name.len() > 10 { /// // If the name is too long, pass ErrorNameTooLong -/// return ErrorNameTooLong::new(name.len() as u16).to_render(); +/// return ErrorNameTooLong(name.len() as u16).to_render(); /// } /// /// if VEC_REGISTERED_NAMES.contains(&name.as_str()) { /// // If the name already exists, pass ErrorNameNotAvailable -/// return ErrorNameNotAvailable::default().to_render(); +/// return ErrorNameNotAvailable.to_render(); /// } /// /// // If the name is valid, pass ResultName -/// ResultName::new(name).to_render() +/// ResultName(name).to_render() /// } /// /// /// Renders a successful greeting with the given name. @@ -1282,12 +1309,7 @@ pub mod example_enum_tag {} /// #[renderer] /// fn render_entry_fallback(err: EntryFallback) -> RenderResult { /// let mut render_result = RenderResult::new(); -/// writeln!( -/// render_result, -/// "Command not found: \"{}\"", -/// err.inner.join(" ") -/// ) -/// .ok(); +/// writeln!(render_result, "Command not found: \"{}\"", err.0.join(" ")).ok(); /// render_result /// } /// @@ -1351,18 +1373,21 @@ pub mod example_error_handling {} /// /// dispatcher!("hello", EntryHello); /// -/// pack!(ErrorNoNameProvided = ()); -/// pack!(ResultName = String); +/// #[derive(Grouped)] +/// pub struct ErrorNoNameProvided; +/// +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName(String); /// /// #[chain] /// fn handle_hello(args: EntryHello) -> Next { -/// let Some(name) = args.inner.first().cloned() else { +/// let Some(name) = args.0.first().cloned() else { /// // If no name is provided, pass ErrorNoNameProvided -/// return ErrorNoNameProvided::default().to_render(); +/// return ErrorNoNameProvided.to_render(); /// }; /// /// // If the name is valid, pass ResultName -/// ResultName::new(name).to_render() +/// ResultName(name).to_render() /// } /// /// /// Renders a successful greeting with the given name. @@ -1527,12 +1552,13 @@ pub mod example_help {} /// program.exec_and_exit(); /// } /// -/// pack!(ResultName = String); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName(String); /// /// #[chain] /// fn handle_greet(args: EntryGreet) -> Next { /// let name: ResultName = args -/// .inner +/// .0 /// .first() /// .cloned() /// .unwrap_or_else(|| "World".to_string()) @@ -1656,7 +1682,8 @@ pub mod example_implicit_dispatcher {} /// dispatcher!("show", EntryShow); /// dispatcher!("none", EntryNone); /// -/// pack!(ResultShow = BTreeMap<Key, Value>); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultShow(BTreeMap<Key, Value>); /// /// fn main() { /// let mut program = ThisProgram::new(); @@ -1774,14 +1801,17 @@ pub mod example_lazy_resources {} /// } /// // --------- IMPORTANT --------- /// -/// pack!(ResultName = String); -/// pack!(DescResult = String); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName(String); +/// +/// #[derive(Grouped, Wrap)] +/// pub struct DescResult(String); /// /// /// Chain for `greet` — reads the name and produces a `ResultName`. /// #[chain] /// fn handle_greet(args: EntryGreet) -> Next { /// let name: ResultName = args -/// .inner +/// .0 /// .first() /// .cloned() /// .unwrap_or_else(|| "World".to_string()) @@ -1799,7 +1829,7 @@ pub mod example_lazy_resources {} /// None => "EntryGreet has no description".to_string(), /// }; /// // --------- IMPORTANT --------- -/// DescResult::new(msg).to_render() +/// DescResult(msg).to_render() /// } /// /// /// Chain for `nodoc` — asks for metadata on an entry that has none. @@ -1812,7 +1842,7 @@ pub mod example_lazy_resources {} /// None => "EntryDescription has no description".to_string(), /// }; /// // --------- IMPORTANT --------- -/// DescResult::new(msg).to_render() +/// DescResult(msg).to_render() /// } /// /// /// Renders the greeting message with the provided name. @@ -1897,7 +1927,8 @@ pub mod example_metadata {} /// // you can use this syntax to create an alias simultaneously /// // --------- IMPORTANT --------- /// -/// pack!(ParsedNumber = i32); +/// #[derive(Grouped, Wrap)] +/// pub struct ParsedNumber(i32); /// /// /// Parse the first argument as an `i32` /// /// @@ -1905,9 +1936,9 @@ pub mod example_metadata {} /// /// On failure, routes to `render_parse_error` via the registered outside type. /// #[chain] /// fn parse_number(args: EntryParse) -> Next { -/// let input = args.inner.first().cloned().unwrap_or_default(); +/// let input = args.0.first().cloned().unwrap_or_default(); /// match input.parse::<i32>() { -/// Ok(num) => ParsedNumber::new(num).to_chain(), +/// Ok(num) => ParsedNumber(num).to_chain(), /// Err(e) => e.to_chain(), /// } /// } @@ -1950,181 +1981,6 @@ pub mod example_metadata {} /// gen_program!(); /// ``` pub mod example_outside_type {} -/// Example `pack_err!` -/// -/// > This example demonstrates how to use the `pack_err!` macro to define error types -/// > with automatic `name` field (set to snake_case at compile time) and optional `info` field. -/// > Also demonstrates `--json` serialization when `structural_renderer` is enabled. -/// -/// Run: -/// ```bash -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural --json -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural Cargo.toml --json -/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find-structural src --json -/// ``` -/// -/// Output: -/// ```plaintext -/// Search path not provided -/// Not a directory: Cargo.toml -/// Found directory: src -/// {"name":"error_not_found"} -/// {"name":"error_not_dir","info":"Cargo.toml"} -/// {"inner":"src"} -/// {"name":"error_not_found_structural"} -/// {"name":"error_not_dir_structural","info":"Cargo.toml"} -/// ``` -/// -/// Source code (./Cargo.toml) -/// ```toml -/// [package] -/// name = "example-pack-err" -/// version = "0.1.0" -/// edition = "2024" -/// -/// [dependencies] -/// serde = { version = "1.0.228", features = ["derive"] } -/// -/// [dependencies.mingling] -/// path = "../../mingling" -/// features = [ -/// "structural_renderer", -/// "extras", -/// ] -/// -/// [workspace] -/// ``` -/// -/// Source code (./src/main.rs) -/// ```ignore -/// use mingling::prelude::*; -/// use mingling::setup::StructuralRendererSetup; -/// use std::io::Write; -/// use std::path::PathBuf; -/// -/// dispatcher!("find", EntryFind); -/// dispatcher!("find-structural", EntryFindStructural); -/// -/// // --------- IMPORTANT --------- -/// // `pack_err!` is a convenient macro for defining error types. -/// // -/// // Simple form: pack_err!(ErrorNotFound); -/// // Typed form: pack_err!(ErrorNotDir = PathBuf); -/// // -/// // The simple form generates a struct with `name: String` and `impl Default`. -/// // name = "error_not_found" (automatically snake_cased at compile time) -/// // -/// // The typed form additionally generates `pub fn new(info)`. -/// // name = "error_not_dir" -/// // -/// // When `structural_renderer` is enabled, the struct also gets -/// // `#[derive(serde::Serialize)]` for --json / --yaml output. -/// // --------- IMPORTANT --------- -/// -/// // Simple form — name = "error_not_found" -/// pack_err!(ErrorNotFound); -/// -/// // Typed form — name = "error_not_dir" -/// pack_err!(ErrorNotDir = PathBuf); -/// -/// // Simple form — with StructuralData support for --json / --yaml -/// pack_err_structural!(ErrorNotFoundStructural); -/// -/// // Typed form — with StructuralData support for --json / --yaml -/// pack_err_structural!(ErrorNotDirStructural = PathBuf); -/// -/// // Success type with StructuralData support -/// pack_structural!(ResultPath = PathBuf); -/// -/// #[chain] -/// fn handle_find(args: EntryFind) -> Next { -/// let Some(path_str) = args.inner.first().cloned() else { -/// // No path provided → use the simple error form (Default) -/// return ErrorNotFound::default().to_render(); -/// }; -/// -/// let path = PathBuf::from(&path_str); -/// if path.is_dir() { -/// // Is a directory → success -/// ResultPath::new(path).to_render() -/// } else { -/// // Not a directory (or doesn't exist) → use the typed error form -/// ErrorNotDir::new(path).to_render() -/// } -/// } -/// -/// #[chain] -/// fn handle_find_structural(args: EntryFindStructural) -> Next { -/// let Some(path_str) = args.inner.first().cloned() else { -/// // No path provided → use the simple error form (Default) -/// return ErrorNotFoundStructural::default().to_render(); -/// }; -/// -/// let path = PathBuf::from(&path_str); -/// if path.is_dir() { -/// // Is a directory → success -/// ResultPath::new(path).to_render() -/// } else { -/// // Not a directory (or doesn't exist) → use the typed error form -/// ErrorNotDirStructural::new(path).to_render() -/// } -/// } -/// -/// /// Renders the successful result with the found directory path. -/// #[renderer] -/// fn render_result_path(path: ResultPath) -> RenderResult { -/// let mut render_result = RenderResult::new(); -/// writeln!(render_result, "Found directory: {}", path.display()).ok(); -/// render_result -/// } -/// -/// /// Renders the error when no search path is provided. -/// #[renderer] -/// fn render_error_not_found(_: ErrorNotFound) -> RenderResult { -/// let mut render_result = RenderResult::new(); -/// writeln!(render_result, "Search path not provided").ok(); -/// render_result -/// } -/// -/// /// Renders the error when the given path is not a directory. -/// #[renderer] -/// fn render_error_not_dir(err: ErrorNotDir) -> RenderResult { -/// let mut render_result = RenderResult::new(); -/// writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); -/// render_result -/// } -/// -/// /// Renders the structural error when no search path is provided. -/// #[renderer] -/// fn render_error_not_found_structural(_: ErrorNotFoundStructural) -> RenderResult { -/// let mut render_result = RenderResult::new(); -/// writeln!(render_result, "Search path not provided").ok(); -/// render_result -/// } -/// -/// /// Renders the structural error when the given path is not a directory. -/// #[renderer] -/// fn render_error_not_dir_structural(err: ErrorNotDirStructural) -> RenderResult { -/// let mut render_result = RenderResult::new(); -/// writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); -/// render_result -/// } -/// -/// gen_program!(); -/// -/// fn main() { -/// let mut program = ThisProgram::new(); -/// -/// // Add StructuralRendererSetup to support --json / --yaml flags -/// program.with_setup(StructuralRendererSetup); -/// -/// let _ = program.exec(); -/// } -/// ``` -pub mod example_pack_err {} /// Example Panic Unwind /// /// > This example introduces how to catch Panic in the Mingling program loop @@ -2171,7 +2027,9 @@ pub mod example_pack_err {} /// use std::io::Write; /// /// dispatcher!("panic", EntryPanic); -/// pack!(NotPanic = ()); +/// +/// #[derive(Grouped)] +/// pub struct NotPanic; /// /// fn main() { /// let mut program = ThisProgram::new(); @@ -2198,7 +2056,7 @@ pub mod example_pack_err {} /// // Panic happens here, will be caught /// panic!("{}", s) /// } -/// None => NotPanic::default().into(), +/// None => NotPanic.into(), /// } /// } /// @@ -2364,7 +2222,8 @@ pub mod example_pathfinder {} /// } /// /// // Create error route -/// pack!(ErrorDirectoryNotExist = PathBuf); +/// #[derive(Grouped, Wrap)] +/// pub struct ErrorDirectoryNotExist(PathBuf); /// /// // Create commands: cd ls exit /// dispatcher!("cd", EntryCd); @@ -2373,16 +2232,18 @@ pub mod example_pathfinder {} /// dispatcher!("clear", EntryClear); /// /// // Define data needed for the cd command's execution phase -/// pack!(StateChangeDirectory = String); +/// #[derive(Grouped, Wrap)] +/// pub struct StateChangeDirectory(String); /// /// // Define data needed for the ls command's rendering phase -/// pack!(ResultList = Vec<String>); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultList(Vec<String>); /// /// // Parse cd command arguments /// #[chain] /// fn parse_cd_args(prev: EntryCd) -> Next { /// let join = prev.pick_or_default(&arg![String]).unwrap(); -/// StateChangeDirectory::new(join).into() +/// StateChangeDirectory(join).into() /// } /// /// // Execute directory change @@ -2390,12 +2251,12 @@ pub mod example_pathfinder {} /// fn handle_cd(prev: StateChangeDirectory, current_dir: &mut ResCurrentDir) -> Next { /// use just_fmt::fmt_path::fmt_path; /// -/// let join = prev.inner; +/// let join = prev.0; /// let new_dir = fmt_path(current_dir.dir.join(join)).unwrap_or_default(); /// /// // If the path is not found, route to error handling /// if !new_dir.exists() { -/// return ErrorDirectoryNotExist::new(new_dir).to_render(); +/// return ErrorDirectoryNotExist(new_dir).to_render(); /// } /// /// current_dir.dir = new_dir; @@ -2420,14 +2281,14 @@ pub mod example_pathfinder {} /// .collect(); /// /// // Render ResultList -/// ResultList::new(entries).to_render() +/// ResultList(entries).to_render() /// } /// /// /// Render ResultList data /// #[renderer] /// fn render_list(list: ResultList) -> RenderResult { /// let mut render_result = RenderResult::new(); -/// for item in list.inner { +/// for item in list.0 { /// writeln!(render_result, "{}", item).ok(); /// } /// render_result @@ -2454,12 +2315,7 @@ pub mod example_pathfinder {} /// #[renderer] /// fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) -> RenderResult { /// let mut render_result = RenderResult::new(); -/// writeln!( -/// render_result, -/// "Directory not found: {}", -/// err.inner.display() -/// ) -/// .ok(); +/// writeln!(render_result, "Directory not found: {}", err.0.display()).ok(); /// render_result /// } /// @@ -2636,13 +2492,14 @@ pub mod example_resources {} /// /// dispatcher!("greet", EntryGreet); /// -/// pack!(ResultGreeting = String); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultGreeting(String); /// /// /// Chain: reads the `ResAppName` and `ResAppVersion` resources. /// #[chain] /// fn handle_greet(args: EntryGreet, app: &ResAppName, version: &ResAppVersion) -> Next { /// let who = args -/// .inner +/// .0 /// .first() /// .cloned() /// .unwrap_or_else(|| "World".to_string()); @@ -2718,7 +2575,8 @@ pub mod example_setup {} /// } /// /// // --------- IMPORTANT --------- -/// // For beautiful output structure, do not use `pack!` to wrap the types that need to be output. +/// // For beautiful output structure, do not wrap the types that need to be output +/// // in a newtype; instead, use a named struct. /// // Instead, manually implement /// // ____________________________________ Mark as structured data so it can be rendered /// // / ____________________ Implement serde::Serialize @@ -2734,7 +2592,7 @@ pub mod example_setup {} /// } /// // This will output: {"member_name":"name","member_age":32} structure /// -/// // If using pack!(Info = (String, i32)); +/// // If wrapping with a tuple newtype (e.g. `#[derive(Grouped, Wrap)] pub struct Info((String, i32));`) /// // Output: {"inner":["name", 32]} /// /// // --------- IMPORTANT --------- @@ -2812,30 +2670,30 @@ pub mod example_structural_renderer {} /// let hello_with_valid_name = handle_hello(entry!("Peter")).into(); /// assert_render_result!(hello_with_valid_name); /// let result_name = unpack_chain_process!(hello_with_valid_name, ResultName); -/// assert_eq!(result_name.inner, "Peter"); +/// assert_eq!(result_name.0, "Peter"); /// } /// /// #[test] /// fn test_render_result_name() { -/// let r = render_result_name(ResultName::new("Peter".into())); +/// let r = render_result_name(ResultName("Peter".into())); /// assert_eq!(r.to_string().as_str(), "Hello, Peter!") /// } /// /// #[test] /// fn test_render_error_no_name_provided() { -/// let r = render_error_no_name_provided(ErrorNoNameProvided::default()); +/// let r = render_error_no_name_provided(ErrorNoNameProvided); /// assert_eq!(r.to_string().as_str(), "No name provided") /// } /// /// #[test] /// fn test_render_error_name_not_available() { -/// let r = render_error_name_not_available(ErrorNameNotAvailable::default()); +/// let r = render_error_name_not_available(ErrorNameNotAvailable); /// assert_eq!(r.to_string().as_str(), "Name not available") /// } /// /// #[test] /// fn test_render_error_name_too_long() { -/// let r = render_error_name_too_long(ErrorNameTooLong::new(17)); +/// let r = render_error_name_too_long(ErrorNameTooLong(17)); /// assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10") /// } /// // --------- IMPORTANT --------- @@ -2843,29 +2701,35 @@ pub mod example_structural_renderer {} /// /// dispatcher!("hello", EntryHello); /// -/// pack!(ErrorNoNameProvided = ()); -/// pack!(ErrorNameTooLong = u16); -/// pack!(ErrorNameNotAvailable = ()); +/// #[derive(Grouped)] +/// pub struct ErrorNoNameProvided; +/// +/// #[derive(Grouped, Wrap)] +/// pub struct ErrorNameTooLong(u16); +/// +/// #[derive(Grouped)] +/// pub struct ErrorNameNotAvailable; /// -/// pack!(ResultName = String); +/// #[derive(Grouped, Wrap)] +/// pub struct ResultName(String); /// /// static VEC_REGISTERED_NAMES: &[&str] = &["Alice", "Bob", "Charlie", "David", "Eve"]; /// /// #[chain] /// fn handle_hello(args: EntryHello) -> Next { -/// let Some(name) = args.inner.first().cloned() else { -/// return ErrorNoNameProvided::default().to_render(); +/// let Some(name) = args.0.first().cloned() else { +/// return ErrorNoNameProvided.to_render(); /// }; /// /// if name.len() > 10 { -/// return ErrorNameTooLong::new(name.len() as u16).to_render(); +/// return ErrorNameTooLong(name.len() as u16).to_render(); /// } /// /// if VEC_REGISTERED_NAMES.contains(&name.as_str()) { -/// return ErrorNameNotAvailable::default().to_render(); +/// return ErrorNameNotAvailable.to_render(); /// } /// -/// ResultName::new(name).to_render() +/// ResultName(name).to_render() /// } /// /// /// Renders a successful greeting with the given name. @@ -2904,12 +2768,7 @@ pub mod example_structural_renderer {} /// #[renderer] /// fn render_entry_fallback(err: EntryFallback) -> RenderResult { /// let mut render_result = RenderResult::new(); -/// writeln!( -/// render_result, -/// "Command not found: \"{}\"", -/// err.inner.join(" ") -/// ) -/// .ok(); +/// writeln!(render_result, "Command not found: \"{}\"", err.0.join(" ")).ok(); /// render_result /// } /// diff --git a/mingling/src/gen_program.rs b/mingling/src/gen_program.rs index 2c476a2..ed60668 100644 --- a/mingling/src/gen_program.rs +++ b/mingling/src/gen_program.rs @@ -12,7 +12,7 @@ pub type Next = ChainProcess<ThisProgram>; /// The generic program entry point. /// -/// This type is created by the `pack!` macro as a variant of the +/// This type is generated by the `gen_program!` macro as a variant of the /// program's output type set (`ThisProgram`). pub struct Entry { /// The arguments provided by the user @@ -41,7 +41,7 @@ pub enum ThisProgram { /// A struct representing a "renderer not found" error. /// -/// This type is created by the `pack!` macro as a variant of the +/// This type is generated by the `gen_program!` macro as a variant of the /// program's output type set (`ThisProgram`). pub struct ErrorRendererNotFound { /// The name of the renderer that was not found @@ -50,7 +50,7 @@ pub struct ErrorRendererNotFound { /// A struct representing a "dispatcher not found" error. /// -/// This type is created by the `pack!` macro as a variant of the +/// This type is generated by the `gen_program!` macro as a variant of the /// program's output type set (`ThisProgram`). pub struct EntryFallback { /// The arguments provided by the user @@ -61,8 +61,6 @@ pub struct EntryFallback { pub struct ResultEmpty; /// A dispatcher representing the subcommand `__comp` itself. -/// -/// **You can register it using `with_dispatcher`.** #[cfg(feature = "comp")] pub struct CMDCompletion; @@ -71,7 +69,7 @@ pub struct CMDCompletion; /// This struct holds the raw command-line arguments that were passed to the `__comp` /// subcommand, which will be used to compute completion suggestions. /// -/// This type is created by the `pack!` macro as a variant of the +/// This type is generated by the `gen_program!` macro as a variant of the /// program's output type set (`ThisProgram`). #[cfg(feature = "comp")] pub struct CompletionContext { @@ -85,7 +83,7 @@ pub struct CompletionContext { /// which together provide the information needed to render completion candidates /// to the user's shell. /// -/// This type is created by the `pack!` macro as a variant of the +/// This type is generated by the `gen_program!` macro as a variant of the /// program's output type set (`ThisProgram`). #[cfg(feature = "comp")] pub struct CompletionSuggest { diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 6a7074b..55240e5 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -51,7 +51,7 @@ pub mod consts { /// /// This module re-exports all macros provided by the `mingling_macros` crate, /// including `dispatcher!`, `chain!`, `renderer!`, -/// `gen_program!`, `pack!`, and many others. These macros form the core +/// `gen_program!`, and many others. These macros form the core /// building blocks of the Mingling framework. /// /// For detailed documentation, usage examples, and the full list of available @@ -84,13 +84,6 @@ pub mod macros { pub use mingling_macros::help; pub use mingling_macros::metadata; pub use mingling_macros::mlint; - pub use mingling_macros::pack; - #[cfg(feature = "extras")] - pub use mingling_macros::pack_err; - #[cfg(all(feature = "structural_renderer", feature = "extras"))] - pub use mingling_macros::pack_err_structural; - #[cfg(feature = "structural_renderer")] - pub use mingling_macros::pack_structural; #[cfg(feature = "comp")] #[doc(hidden)] pub use mingling_macros::program_comp_gen; @@ -212,19 +205,7 @@ pub mod prelude { #[cfg(feature = "macros")] pub use crate::macros::gen_program; #[cfg(feature = "macros")] - pub use crate::macros::pack; - #[cfg(all(feature = "extras", feature = "macros"))] - pub use crate::macros::pack_err; - #[cfg(feature = "macros")] pub use crate::macros::renderer; - #[cfg(all( - feature = "macros", - feature = "structural_renderer", - feature = "extras" - ))] - pub use mingling_macros::pack_err_structural; - #[cfg(all(feature = "macros", feature = "structural_renderer"))] - pub use mingling_macros::pack_structural; pub use mingling_macros::r_append; pub use mingling_macros::r_eprint; pub use mingling_macros::r_eprintln; diff --git a/mingling_cli/src/config/cmd_cfg.rs b/mingling_cli/src/config/cmd_cfg.rs index c10195e..8baa4a1 100644 --- a/mingling_cli/src/config/cmd_cfg.rs +++ b/mingling_cli/src/config/cmd_cfg.rs @@ -1,8 +1,6 @@ use mingling::{ - LazyRes, Routable, ShellContext, Suggest, - macros::{ - arg, buffer, chain, command, completion, metadata, pack, r_println, renderer, suggest, - }, + Grouped, LazyRes, Routable, ShellContext, Suggest, Wrap, + macros::{arg, buffer, chain, command, completion, metadata, r_println, renderer, suggest}, metadata::Description, picker::{EntryPicker, PickerArg, value::Flag}, }; @@ -11,10 +9,17 @@ use crate::{Entry, Next, config::ResMlingConfig}; const FLAG_PAIR: PickerArg<Flag> = arg![pair: Flag]; -pack!(StateConfigEdit = (String, String)); -pack!(ResultConfigKeyValuePair = String); -pack!(ResultConfigValue = String); -pack!(ResultConfig = ()); +#[derive(Grouped, Wrap)] +pub struct StateConfigEdit((String, String)); + +#[derive(Grouped, Wrap)] +pub struct ResultConfigKeyValuePair(String); + +#[derive(Grouped, Wrap)] +pub struct ResultConfigValue(String); + +#[derive(Grouped)] +pub struct ResultConfig; #[command] pub fn cfg(args: Entry) -> Next { @@ -27,19 +32,19 @@ pub fn cfg(args: Entry) -> Next { match (key, value) { (Some(k), Some(v)) => { // Edit - StateConfigEdit::new((k, v)).into() + StateConfigEdit((k, v)).into() } (Some(k), None) => { // Display if *show_pair { - ResultConfigKeyValuePair::new(k).to_render() + ResultConfigKeyValuePair(k).to_render() } else { - ResultConfigValue::new(k).to_render() + ResultConfigValue(k).to_render() } } (None, None) => { // List - ResultConfig::new(()).to_render() + ResultConfig.to_render() } _ => { unreachable!("This path is unreachable given the positional parsing done by arg-picker") @@ -50,13 +55,13 @@ pub fn cfg(args: Entry) -> Next { #[chain] pub fn handle_state_config_edit(kv: StateConfigEdit, config: &mut LazyRes<ResMlingConfig>) { let config = config.get_mut(); - config.edit(&kv.0, &kv.1); + config.edit(&kv.0.0, &kv.0.1); } #[renderer(buffer)] pub fn render_config_kvp(r: ResultConfigKeyValuePair, config: &mut LazyRes<ResMlingConfig>) { let config = config.get_ref(); - let key = r.inner; + let key = r.0; let value = config.get(&key); r_println!( "\"{}\" = \"{}\"", @@ -68,7 +73,7 @@ pub fn render_config_kvp(r: ResultConfigKeyValuePair, config: &mut LazyRes<ResMl #[renderer(buffer)] pub fn render_config_value(r: ResultConfigValue, config: &mut LazyRes<ResMlingConfig>) { let config = config.get_ref(); - let key = r.inner; + let key = r.0; let value = config.get(&key); r_println!("{}", value) } diff --git a/mingling_cli/src/lib.rs b/mingling_cli/src/lib.rs index 902ab32..e82300e 100644 --- a/mingling_cli/src/lib.rs +++ b/mingling_cli/src/lib.rs @@ -45,7 +45,7 @@ pub fn complete_global(_ctx: &ShellContext) -> Suggest { #[renderer] pub fn handle_fallback(args: EntryFallback) -> RenderResult { let mut r = RenderResult::new(); - let args = args.inner; + let args = args.0; if !args.is_empty() { eprintln_cargo!( r, diff --git a/mingling_cli/src/linter/cmd_explain.rs b/mingling_cli/src/linter/cmd_explain.rs index 5346255..923e890 100644 --- a/mingling_cli/src/linter/cmd_explain.rs +++ b/mingling_cli/src/linter/cmd_explain.rs @@ -1,10 +1,7 @@ use crate::{Next, eprintln_cargo, linter::registry::ResLintRegistry}; use mingling::{ - Grouped, LazyRes, RenderResult, Routable, ShellContext, Suggest, SuggestItem, - macros::{ - arg, buffer, chain, completion, dispatcher, metadata, pack, pack_err, r_println, renderer, - routeify, - }, + Grouped, LazyRes, RenderResult, Routable, ShellContext, Suggest, SuggestItem, Wrap, + macros::{arg, buffer, chain, completion, dispatcher, metadata, r_println, renderer, routeify}, metadata::Description, picker::EntryPicker, }; @@ -16,9 +13,14 @@ pub fn desc_explain() -> Description { "Explain the meaning of the specified Lint".into() } -pack!(StateExplainLint = String); -pack_err!(ErrorNoExplainLintProvided); -pack_err!(ErrorNoSuchLint = String); +#[derive(Grouped, Wrap)] +pub struct StateExplainLint(String); + +#[derive(Grouped, Default)] +pub struct ErrorNoExplainLintProvided; + +#[derive(Grouped, Wrap)] +pub struct ErrorNoSuchLint(String); #[derive(Debug, Default, Grouped)] pub struct ResultExplainLint { @@ -33,11 +35,9 @@ pub struct ResultExplainLint { #[chain(routeify)] pub fn handle_explain(args: EntryExplain) -> Next { let lint_name = args - .pick_or_route(&arg![String], || { - ErrorNoExplainLintProvided::default().to_chain() - }) + .pick_or_route(&arg![String], || ErrorNoExplainLintProvided.to_chain()) .to_result()?; - StateExplainLint::new(lint_name).into() + StateExplainLint(lint_name).into() } #[chain] @@ -46,9 +46,9 @@ pub fn handle_state_explain_lint( registry: &mut LazyRes<ResLintRegistry>, ) -> Next { let registry = registry.get_ref(); - let lint_name = p.inner; + let lint_name = p.0; let Some(entry) = registry.lints.iter().find(|l| l.name == lint_name) else { - return ErrorNoSuchLint::new(lint_name).to_chain(); + return ErrorNoSuchLint(lint_name).to_chain(); }; ResultExplainLint { lint_name: entry.name.clone(), @@ -87,7 +87,7 @@ pub fn render_error_no_such_lint( ) -> RenderResult { let mut r = RenderResult::new(); let registry = registry.get_ref(); - eprintln_cargo!(r, "No such lint: \"{}\"", err.info); + eprintln_cargo!(r, "No such lint: \"{}\"", err.0); r_println!(r, ""); r_println!(r, "Available lints:"); for entry in registry.lints.iter() { diff --git a/mingling_cli/src/linter/cmd_lint.rs b/mingling_cli/src/linter/cmd_lint.rs index 07c18c5..7cab63a 100644 --- a/mingling_cli/src/linter/cmd_lint.rs +++ b/mingling_cli/src/linter/cmd_lint.rs @@ -1,10 +1,11 @@ use crate::linter::mlint_report::{MlintReport, StateLintReports}; use cargo_metadata::Metadata; use mingling::consts::REMAINS; -use mingling::macros::{arg, chain, completion, dispatcher, metadata, pack, suggest}; +use mingling::macros::{arg, chain, completion, dispatcher, metadata, suggest}; use mingling::metadata::Description; use mingling::picker::parselib::ParserStyle; use mingling::picker::{EntryPicker, PickerArg}; +use mingling::{Grouped, Wrap}; use mingling::{LazyRes, ShellContext, Suggest}; use tokio::task::JoinSet; @@ -77,7 +78,8 @@ async fn linter_main(metadata: &Metadata) -> Vec<MlintReport> { all_reports } -pack!(StateBeginLinter = ()); +#[derive(Grouped, Wrap)] +pub struct StateBeginLinter(()); #[chain] pub fn handle_lint(args: EntryLint) -> StateBeginLinter { @@ -88,7 +90,7 @@ pub fn handle_lint(args: EntryLint) -> StateBeginLinter { // If with_checker is not set, proceed directly to the mingling lint phase let Some(with_checker) = with_checker else { - return StateBeginLinter::new(()); + return StateBeginLinter(()); }; let with_checker: Vec<&str> = with_checker.split(',').collect(); @@ -97,7 +99,7 @@ pub fn handle_lint(args: EntryLint) -> StateBeginLinter { // Run the outer checker (e.g. cargo check) with output passed through directly execute_checker(&with_checker, checker_args.as_slice()); - StateBeginLinter::new(()) + StateBeginLinter(()) } /// Run the outer checker (e.g. cargo check) with output passed through directly. @@ -135,7 +137,7 @@ pub async fn handle_state_begin_linter( ) -> StateLintReports { let metadata = metadata.get_ref().data(); let reports = linter_main(metadata).await; - StateLintReports::new(reports) + StateLintReports(reports) } #[completion(EntryLint)] diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs index b594b9d..d7bf652 100644 --- a/mingling_cli/src/linter/mlint_report.rs +++ b/mingling_cli/src/linter/mlint_report.rs @@ -9,8 +9,8 @@ use cargo_metadata::{Message, PackageId}; use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING}; use annotate_snippets::{AnnotationKind, Group, Patch, Renderer, Snippet}; -use mingling::macros::{buffer, chain, pack, r_append, r_eprintln, renderer}; -use mingling::{RendererInvoker, Routable}; +use mingling::macros::{buffer, chain, r_append, r_eprintln, renderer}; +use mingling::{Grouped, RendererInvoker, Routable, Wrap}; use crate::Next; use crate::metadata::setup::ResUsingJson; @@ -416,22 +416,25 @@ impl MlintReport { } } -pack!(StateLintReports = Vec<MlintReport>); -pack!(ResultLintReportsAnnotateSnippet = Vec<MlintReport>); -pack!(ResultLintReportsJson = Vec<MlintReport>); +#[derive(Grouped, Wrap)] +pub struct StateLintReports(pub Vec<MlintReport>); +#[derive(Grouped, Wrap)] +pub struct ResultLintReportsAnnotateSnippet(Vec<MlintReport>); +#[derive(Grouped, Wrap)] +pub struct ResultLintReportsJson(Vec<MlintReport>); #[chain] pub fn handle_state_lint_reports(reports: StateLintReports, using_json: &ResUsingJson) -> Next { if using_json.using { - ResultLintReportsJson::new(reports.inner).to_render() + ResultLintReportsJson(reports.0).to_render() } else { - ResultLintReportsAnnotateSnippet::new(reports.inner).to_render() + ResultLintReportsAnnotateSnippet(reports.0).to_render() } } #[renderer(buffer)] pub fn render_lint_reports(reports: ResultLintReportsAnnotateSnippet) { - for report in reports.inner { + for report in reports.0 { r_eprintln!("{}", report.to_annotate_snippet_render()); } } @@ -441,7 +444,7 @@ pub fn render_lint_reports_json( reports: ResultLintReportsJson, message_renderer: &RendererInvoker<Message>, ) { - for report in reports.inner { + for report in reports.0 { let message = report.to_compiler_message(); let result = message_renderer.invoke(message); r_append!(result); diff --git a/mingling_cli/src/metadata/cmd_metadata.rs b/mingling_cli/src/metadata/cmd_metadata.rs index 3ee9f09..7f8d4ad 100644 --- a/mingling_cli/src/metadata/cmd_metadata.rs +++ b/mingling_cli/src/metadata/cmd_metadata.rs @@ -1,7 +1,7 @@ use cargo_metadata::Metadata; use mingling::{ - LazyRes, - macros::{chain, dispatcher, metadata, pack}, + Grouped, LazyRes, Wrap, + macros::{chain, dispatcher, metadata}, metadata::Description, }; @@ -16,7 +16,8 @@ pub fn desc_metadata() -> Description { .into() } -pack!(ResultMetadata = ResMetadata); +#[derive(Grouped, Wrap)] +pub struct ResultMetadata(ResMetadata); #[chain] pub fn handle_metadata(_: EntryMetadata, metadata: &mut LazyRes<ResMetadata>) -> Metadata { diff --git a/mingling_cli/src/pkg_mgr.rs b/mingling_cli/src/pkg_mgr.rs index e140991..8493804 100644 --- a/mingling_cli/src/pkg_mgr.rs +++ b/mingling_cli/src/pkg_mgr.rs @@ -8,16 +8,23 @@ pub mod cmd_uninstall; use std::path::PathBuf; use mingling::{ - Program, RenderResult, - macros::{pack_err, program_setup, r_println, renderer}, + Grouped, Program, RenderResult, Wrap, + macros::{program_setup, r_println, renderer}, }; use crate::{ThisProgram, eprintln_cargo, hprintln_cargo}; -pack_err!(ErrorRootPackageNotFound); -pack_err!(ErrorNoDataDirectory); -pack_err!(ErrorPackageSpecInvalid = String); -pack_err!(ErrorPackageNameRequired); +#[derive(Grouped, Default)] +pub struct ErrorRootPackageNotFound; + +#[derive(Grouped, Default)] +pub struct ErrorNoDataDirectory; + +#[derive(Grouped, Wrap)] +pub struct ErrorPackageSpecInvalid(String); + +#[derive(Grouped, Default)] +pub struct ErrorPackageNameRequired; /// The `mingling/packages` packages directory under the user's data directory. #[derive(Debug, Default, Clone)] @@ -55,7 +62,7 @@ pub fn render_error_no_data_directory(_: ErrorNoDataDirectory) -> RenderResult { #[renderer] pub fn render_error_package_spec_invalid(err: ErrorPackageSpecInvalid) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "invalid package spec: {}", err.info); + eprintln_cargo!(r, "invalid package spec: {}", err.0); r } diff --git a/mingling_cli/src/pkg_mgr/cmd_install.rs b/mingling_cli/src/pkg_mgr/cmd_install.rs index 731ba98..8b2f3b7 100644 --- a/mingling_cli/src/pkg_mgr/cmd_install.rs +++ b/mingling_cli/src/pkg_mgr/cmd_install.rs @@ -2,8 +2,8 @@ use std::{env, fs, io, path::PathBuf, process::Command}; use cargo_metadata::TargetKind; use mingling::{ - Grouped, LazyRes, RenderResult, Routable, ShellContext, Suggest, - macros::{arg, chain, command, completion, metadata, pack_err, renderer, routeify, suggest}, + Grouped, LazyRes, RenderResult, Routable, ShellContext, Suggest, Wrap, + macros::{arg, chain, command, completion, metadata, renderer, routeify, suggest}, metadata::Description, picker::{EntryPicker, PickerArg, value::Flag}, }; @@ -15,9 +15,14 @@ use crate::{ println_cargo, }; -pack_err!(ErrorBuildFailed = String); -pack_err!(ErrorBinaryNotFound = String); -pack_err!(ErrorPkgEnableFailed = String); +#[derive(Grouped, Wrap)] +pub struct ErrorBuildFailed(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorBinaryNotFound(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorPkgEnableFailed(String); /// Flag: `--enable` — run `mling pkg-enable` after a successful install /// to enable the package being installed. @@ -75,13 +80,13 @@ pub fn install( let metadata = metadata.get_ref().data(); let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let root_package = metadata .root_package() .or_else(|| metadata.workspace_packages().first().copied()) - .ok_or(ErrorRootPackageNotFound::default())?; + .ok_or(ErrorRootPackageNotFound)?; StateInstallBuild { workspace_root: metadata.workspace_root.clone().into_std_path_buf(), @@ -103,11 +108,9 @@ pub fn handle_state_install_build(state: StateInstallBuild) -> Next { .args(["build", "--release"]) .current_dir(&state.workspace_root) .status() - .map_err(|e| { - ErrorBuildFailed::new(format!("failed to run `cargo build --release`: {e}")) - })?; + .map_err(|e| ErrorBuildFailed(format!("failed to run `cargo build --release`: {e}")))?; if !status.success() { - return ErrorBuildFailed::new(format!("`cargo build --release` failed with {status}")) + return ErrorBuildFailed(format!("`cargo build --release` failed with {status}")) .to_chain(); } @@ -146,7 +149,7 @@ pub fn handle_state_install_copy( let bin_file = format!("{}{}", target.name, state.exe_suffix); let src = state.release_dir.join(&bin_file); if !src.is_file() { - return ErrorBinaryNotFound::new(bin_file).to_chain(); + return ErrorBinaryNotFound(bin_file).to_chain(); } let dst = state.install_dir.join(&bin_file); fs::copy(&src, &dst).map_err(|e| { @@ -181,7 +184,7 @@ pub fn handle_state_install_copy( let root_package = metadata .root_package() .or_else(|| metadata.workspace_packages().first().copied()) - .ok_or(ErrorRootPackageNotFound::default())?; + .ok_or(ErrorRootPackageNotFound)?; return StateInstallEnable { install_dir: state.install_dir, installed: state.installed, @@ -207,13 +210,11 @@ pub fn handle_state_install_enable(state: StateInstallEnable) -> Next { .args(["pkg-enable", &spec]) .status() .map_err(|e| { - ErrorPkgEnableFailed::new(format!("failed to run `mling pkg-enable {spec}`: {e}")) + ErrorPkgEnableFailed(format!("failed to run `mling pkg-enable {spec}`: {e}")) })?; if !status.success() { - return ErrorPkgEnableFailed::new(format!( - "`mling pkg-enable {spec}` failed with {status}" - )) - .to_chain(); + return ErrorPkgEnableFailed(format!("`mling pkg-enable {spec}` failed with {status}")) + .to_chain(); } ResultInstall { @@ -236,21 +237,21 @@ pub fn render_result_install(result: ResultInstall) -> RenderResult { #[renderer] pub fn render_error_build_failed(err: ErrorBuildFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } #[renderer] pub fn render_error_binary_not_found(err: ErrorBinaryNotFound) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "binary not found: {}", err.info); + eprintln_cargo!(r, "binary not found: {}", err.0); r } #[renderer] pub fn render_error_pkg_enable_failed(err: ErrorPkgEnableFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } diff --git a/mingling_cli/src/pkg_mgr/cmd_internal_loadpkgs.rs b/mingling_cli/src/pkg_mgr/cmd_internal_loadpkgs.rs index b4928ce..f0ac88a 100644 --- a/mingling_cli/src/pkg_mgr/cmd_internal_loadpkgs.rs +++ b/mingling_cli/src/pkg_mgr/cmd_internal_loadpkgs.rs @@ -4,8 +4,8 @@ use std::{ }; use mingling::{ - Routable, - macros::{buffer, command, pack, r_println, renderer, routeify}, + Grouped, Routable, Wrap, + macros::{buffer, command, r_println, renderer, routeify}, }; use crate::{ @@ -14,16 +14,18 @@ use crate::{ }; // Version directory paths of every enabled package. -pack!(ResultLoadPkgsPaths = Vec<PathBuf>); +#[derive(Grouped, Wrap)] +pub struct ResultLoadPkgsPaths(Vec<PathBuf>); // Completion script paths of every enabled package. -pack!(ResultLoadPkgsComps = Vec<PathBuf>); +#[derive(Grouped, Wrap)] +pub struct ResultLoadPkgsComps(Vec<PathBuf>); #[command(node = "__loadpkgs_path", routeify)] pub fn load_packages_paths(packages_dir: &ResPackagesDir) -> Next { let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let paths = enabled_version_dirs(packages_dir).map_err(|e| { io::Error::new( @@ -31,14 +33,14 @@ pub fn load_packages_paths(packages_dir: &ResPackagesDir) -> Next { format!("failed to read {}: {e}", packages_dir.display()), ) })?; - ResultLoadPkgsPaths::new(paths).to_chain() + ResultLoadPkgsPaths(paths).to_chain() } #[command(node = "__loadpkgs_comp_scripts", routeify)] pub fn load_packages_comp_scripts(packages_dir: &ResPackagesDir) -> Next { let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let scripts = comp_scripts(packages_dir).map_err(|e| { io::Error::new( @@ -46,19 +48,19 @@ pub fn load_packages_comp_scripts(packages_dir: &ResPackagesDir) -> Next { format!("failed to read {}: {e}", packages_dir.display()), ) })?; - ResultLoadPkgsComps::new(scripts).to_chain() + ResultLoadPkgsComps(scripts).to_chain() } #[renderer(buffer)] pub fn render_result_load_pkgs_paths(r: ResultLoadPkgsPaths) { - for path in r.inner { + for path in r.0 { r_println!("{}", path.display()); } } #[renderer(buffer)] pub fn render_result_load_pkgs_comps(r: ResultLoadPkgsComps) { - for path in r.inner { + for path in r.0 { r_println!("{}", path.display()); } } diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs index 041bb09..b4989eb 100644 --- a/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs +++ b/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs @@ -1,8 +1,8 @@ use std::{fs, io}; use mingling::{ - Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem, - macros::{arg, chain, command, completion, metadata, pack, pack_err, renderer, routeify}, + Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem, Wrap, + macros::{arg, chain, command, completion, metadata, renderer, routeify}, metadata::Description, picker::{EntryPicker, PickerArg}, }; @@ -18,10 +18,12 @@ use crate::{ /// Positional argument: package name pub static ARG_NAME: PickerArg<String> = arg![String]; -pack_err!(ErrorPackageNotEnabled = String); +#[derive(Grouped, Wrap)] +pub struct ErrorPackageNotEnabled(String); // The name of the package to disable -pack!(StatePkgDisable = String); +#[derive(Grouped, Wrap)] +pub struct StatePkgDisable(String); #[derive(Debug, Default, Grouped)] pub struct ResultPkgDisable { @@ -37,30 +39,30 @@ pub fn desc_pkg_disable() -> Description { #[command(node = "pkg-disable", routeify)] pub fn package_disable(args: EntryPkgDisable, packages_dir: &ResPackagesDir) -> Next { let name = args - .pick_or_route(&ARG_NAME, || ErrorPackageNameRequired::default().to_chain()) + .pick_or_route(&ARG_NAME, || ErrorPackageNameRequired.to_chain()) .to_result()?; let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } if name.contains('/') || name.contains('\\') || name.contains("..") || name.contains('@') { - return ErrorPackageSpecInvalid::new(name).to_chain(); + return ErrorPackageSpecInvalid(name).to_chain(); } - StatePkgDisable::new(name).to_chain() + StatePkgDisable(name).to_chain() } #[chain(routeify)] pub fn handle_state_pkg_disable(p: StatePkgDisable, packages_dir: &ResPackagesDir) -> Next { - let name = p.inner; + let name = p.0; let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let file = packages_dir.join(&name); if !file.is_file() { - return ErrorPackageNotEnabled::new(name).to_chain(); + return ErrorPackageNotEnabled(name).to_chain(); } fs::remove_file(&file).map_err(|e| { io::Error::new( @@ -86,7 +88,7 @@ pub fn render_result_pkg_disable(result: ResultPkgDisable) -> RenderResult { #[renderer] pub fn render_error_package_not_enabled(err: ErrorPackageNotEnabled) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "package is not enabled: {}", err.info); + eprintln_cargo!(r, "package is not enabled: {}", err.0); r } diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs index 8f6a234..1235cd3 100644 --- a/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs +++ b/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs @@ -1,8 +1,8 @@ use std::{fs, io}; use mingling::{ - Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem, - macros::{arg, chain, command, completion, metadata, pack, pack_err, renderer, routeify}, + Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem, Wrap, + macros::{arg, chain, command, completion, metadata, renderer, routeify}, metadata::Description, picker::{EntryPicker, PickerArg}, }; @@ -18,8 +18,11 @@ use crate::{ /// Positional argument: package spec (`foo`, `foo@0`, `foo@0.1`, `foo@0.1.2`) pub static ARG_SPEC: PickerArg<String> = arg![String]; -pack_err!(ErrorNoMatchingVersion = String); -pack!(StatePkgEnable = (String, String)); +#[derive(Grouped, Wrap)] +pub struct ErrorNoMatchingVersion(String); + +#[derive(Grouped, Wrap)] +pub struct StatePkgEnable((String, String)); #[derive(Debug, Default, Grouped)] pub struct ResultPkgEnable { @@ -35,14 +38,14 @@ pub fn desc_pkg_enable() -> Description { #[command(node = "pkg-enable", routeify)] pub fn package_enable(args: EntryPkgEnable, packages_dir: &ResPackagesDir) -> Next { let spec = args - .pick_or_route(&ARG_SPEC, || ErrorPackageNameRequired::default().to_chain()) + .pick_or_route(&ARG_SPEC, || ErrorPackageNameRequired.to_chain()) .to_result()?; let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } if spec.contains('/') || spec.contains('\\') || spec.contains("..") { - return ErrorPackageSpecInvalid::new(spec).to_chain(); + return ErrorPackageSpecInvalid(spec).to_chain(); } let (name, version_part) = match spec.split_once('@') { @@ -78,18 +81,18 @@ pub fn package_enable(args: EntryPkgEnable, packages_dir: &ResPackagesDir) -> Ne } let Some((_, version)) = candidates.into_iter().max_by(|a, b| a.1.cmp(&b.1)) else { - return ErrorNoMatchingVersion::new(spec).to_chain(); + return ErrorNoMatchingVersion(spec).to_chain(); }; - StatePkgEnable::new((name, version.to_string())).to_chain() + StatePkgEnable((name, version.to_string())).to_chain() } #[chain(routeify)] pub fn handle_state_pkg_enable(p: StatePkgEnable, packages_dir: &ResPackagesDir) -> Next { - let (name, version) = p.inner; + let (name, version) = p.0; let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let file = packages_dir.join(&name); @@ -110,7 +113,7 @@ pub fn render_result_pkg_enable(result: ResultPkgEnable) -> RenderResult { #[renderer] pub fn render_error_no_matching_version(err: ErrorNoMatchingVersion) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "no matching version for: {}", err.info); + eprintln_cargo!(r, "no matching version for: {}", err.0); r } diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_show.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_show.rs index 88c79ef..ea26021 100644 --- a/mingling_cli/src/pkg_mgr/cmd_pkg_show.rs +++ b/mingling_cli/src/pkg_mgr/cmd_pkg_show.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, fs, io}; use colored::Colorize; use mingling::{ Grouped, RenderResult, Routable, - macros::{buffer, command, metadata, pack_err, r_println, renderer, routeify}, + macros::{buffer, command, metadata, r_println, renderer, routeify}, metadata::Description, }; @@ -15,8 +15,10 @@ use crate::{ #[derive(Debug, Default, Clone)] pub struct PkgShowEntry { pub name: String, + /// Enabled version, from the content of the enable file. pub enabled: Option<String>, + /// Installed versions, newest first. pub versions: Vec<String>, } @@ -31,13 +33,14 @@ pub fn desc_pkg_show() -> Description { "Show locally installed packages".into() } -pack_err!(ErrorNoPackagesInstalled); +#[derive(Grouped, Default)] +pub struct ErrorNoPackagesInstalled; #[command(node = "pkg-show", routeify)] pub fn package_show(packages_dir: &ResPackagesDir) -> Next { let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let mut entries: BTreeMap<String, PkgShowEntry> = BTreeMap::new(); @@ -84,7 +87,7 @@ pub fn package_show(packages_dir: &ResPackagesDir) -> Next { let packages: Vec<PkgShowEntry> = entries.into_values().collect(); if packages.is_empty() { - return ErrorNoPackagesInstalled::default().into(); + return ErrorNoPackagesInstalled.into(); } ResultPkgShow { packages }.to_chain() diff --git a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs index 2b288d6..0c3886c 100644 --- a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs +++ b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs @@ -1,8 +1,8 @@ use std::{fs, io, path::PathBuf}; use mingling::{ - LazyRes, RenderResult, Routable, ShellContext, Suggest, SuggestItem, - macros::{arg, chain, command, completion, metadata, pack, pack_err, renderer, routeify}, + Grouped, LazyRes, RenderResult, Routable, ShellContext, Suggest, SuggestItem, Wrap, + macros::{arg, chain, command, completion, metadata, renderer, routeify}, metadata::Description, picker::{EntryPicker, PickerArg}, }; @@ -20,16 +20,20 @@ use crate::{ pub static ARG_PACKAGE: PickerArg<Option<String>> = arg![Option<String>]; // Directory names to remove, e.g. `["omg@0.1.0", "omg@0.1.1"]` -pack!(StateUninstallPackages = Vec<String>); +#[derive(Grouped, Wrap)] +pub struct StateUninstallPackages(Vec<String>); // Directories that were successfully removed. -pack!(ResultPackageUninstalled = Vec<PathBuf>); +#[derive(Grouped, Wrap)] +pub struct ResultPackageUninstalled(Vec<PathBuf>); // Directories that were not installed. -pack_err!(ErrorPackageNotInstall = Vec<PathBuf>); +#[derive(Grouped, Wrap)] +pub struct ErrorPackageNotInstall(Vec<PathBuf>); // No installed package matched the given spec. -pack_err!(ErrorNoMatchingPackages); +#[derive(Grouped, Default)] +pub struct ErrorNoMatchingPackages; /// `{data_dir}/mingling/packages` #[metadata(EntryUninstall)] @@ -46,7 +50,7 @@ pub fn uninstall( let spec = args.pick(&ARG_PACKAGE).to_result()?; let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let targets = match spec { @@ -56,13 +60,13 @@ pub fn uninstall( let root_package = metadata .root_package() .or_else(|| metadata.workspace_packages().first().copied()) - .ok_or(ErrorRootPackageNotFound::default())?; + .ok_or(ErrorRootPackageNotFound)?; vec![format!("{}@{}", root_package.name, root_package.version)] } // `name` matches every installed version, `name@version` matches exactly Some(spec) => { if spec.contains('/') || spec.contains('\\') || spec.contains("..") { - return ErrorPackageSpecInvalid::new(spec).to_chain(); + return ErrorPackageSpecInvalid(spec).to_chain(); } if spec.contains('@') { vec![spec] @@ -87,7 +91,7 @@ pub fn uninstall( } }; - StateUninstallPackages::new(targets).to_chain() + StateUninstallPackages(targets).to_chain() } #[chain(routeify)] @@ -97,13 +101,13 @@ pub fn handle_state_uninstall_packages( ) -> Next { let packages_dir = &packages_dir.path; if packages_dir.as_os_str().is_empty() { - return ErrorNoDataDirectory::default().to_chain(); + return ErrorNoDataDirectory.to_chain(); } let mut removed = Vec::new(); let mut not_installed = Vec::new(); - for name in p.inner { + for name in p.0 { let dir = packages_dir.join(&name); if !dir.exists() { not_installed.push(dir); @@ -116,18 +120,18 @@ pub fn handle_state_uninstall_packages( } if removed.is_empty() && not_installed.is_empty() { - return ErrorNoMatchingPackages::default().to_chain(); + return ErrorNoMatchingPackages.to_chain(); } if !removed.is_empty() { - return ResultPackageUninstalled::new(removed).to_chain(); + return ResultPackageUninstalled(removed).to_chain(); } - ErrorPackageNotInstall::new(not_installed).to_chain() + ErrorPackageNotInstall(not_installed).to_chain() } #[renderer] pub fn render_result_package_uninstalled(r: ResultPackageUninstalled) -> RenderResult { let mut result = RenderResult::new(); - for dir in r.inner { + for dir in r.0 { println_cargo!(result, "Uninstalled: {}", dir.display()); } result @@ -136,7 +140,7 @@ pub fn render_result_package_uninstalled(r: ResultPackageUninstalled) -> RenderR #[renderer] pub fn render_error_package_not_install(err: ErrorPackageNotInstall) -> RenderResult { let mut result = RenderResult::new(); - for dir in err.info { + for dir in err.0 { eprintln_cargo!(result, "not installed: {}", dir.display()); } result diff --git a/mingling_cli/src/proj_mgr/cmd_class_add.rs b/mingling_cli/src/proj_mgr/cmd_class_add.rs index 9c729a9..f9272a5 100644 --- a/mingling_cli/src/proj_mgr/cmd_class_add.rs +++ b/mingling_cli/src/proj_mgr/cmd_class_add.rs @@ -6,8 +6,8 @@ use std::{ use just_fmt::{camel_case, kebab_case, pascal_case, snake_case}; use just_template::Template; use mingling::{ - Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem, - macros::{arg, chain, command, completion, metadata, pack, pack_err, renderer, routeify}, + Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem, Wrap, + macros::{arg, chain, command, completion, metadata, renderer, routeify}, metadata::Description, picker::EntryPicker, res::ResCurrentDir, @@ -29,7 +29,8 @@ pub struct ClassEntry { pub description: String, } -pack!(StateClassAdd = (String, String)); +#[derive(Grouped, Wrap)] +pub struct StateClassAdd((String, String)); /// Result of adding a class: the generated file path. #[derive(Debug, Default, Grouped)] @@ -37,19 +38,28 @@ pub struct ResultClassAdd { pub output: PathBuf, } -pack_err!(ErrorClassNameRequired = ()); -pack_err!(ErrorClassConfigMissing = String); -pack_err!(ErrorClassNotFound = String); -pack_err!(ErrorClassTemplateMissing = String); -pack_err!(ErrorClassWriteFailed = String); +#[derive(Grouped)] +pub struct ErrorClassNameRequired; + +#[derive(Grouped, Wrap)] +pub struct ErrorClassConfigMissing(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorClassNotFound(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorClassTemplateMissing(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorClassWriteFailed(String); #[command(node = "class-add", routeify)] pub fn class_add(args: EntryClassAdd) -> Next { let (class_name, name) = args - .pick_or_route(&arg![String], || ErrorClassNameRequired::new(()).to_chain()) - .pick_or_route(&arg![String], || ErrorClassNameRequired::new(()).to_chain()) + .pick_or_route(&arg![String], || ErrorClassNameRequired.to_chain()) + .pick_or_route(&arg![String], || ErrorClassNameRequired.to_chain()) .to_result()?; - StateClassAdd::new((class_name, name)).to_chain() + StateClassAdd((class_name, name)).to_chain() } /// Walk upward from `start` to find the first directory containing `.mling`. @@ -91,11 +101,11 @@ fn deverbatim(path: &Path) -> PathBuf { /// name-derived parameters into `<output-dir>/<snake_case>.rs`. #[chain(routeify)] pub fn handle_state_class_add(state: StateClassAdd, cwd: &ResCurrentDir) -> Next { - let (class_name, name) = state.inner; + let (class_name, name) = state.0; // Resolve the project root: the nearest ancestor directory with `.mling`. let Some(project_root) = find_project_root(cwd) else { - return ErrorClassConfigMissing::new(format!( + return ErrorClassConfigMissing(format!( "no `.mling` directory found from {} upward; run this inside a mingling project", cwd.display() )) @@ -105,14 +115,14 @@ pub fn handle_state_class_add(state: StateClassAdd, cwd: &ResCurrentDir) -> Next // Read `.mling/classes.toml` (the class registry). let classes_path = project_root.join(".mling").join("classes.toml"); let content = fs::read_to_string(&classes_path).map_err(|e| { - ErrorClassConfigMissing::new(format!("failed to read {}: {e}", classes_path.display())) + ErrorClassConfigMissing(format!("failed to read {}: {e}", classes_path.display())) })?; let classes = parse_classes(&content) - .map_err(|e| ErrorClassConfigMissing::new(format!("invalid classes.toml: {e}")))?; + .map_err(|e| ErrorClassConfigMissing(format!("invalid classes.toml: {e}")))?; // Find the requested class type. let Some(entry) = classes.iter().find(|c| c.name == class_name) else { - return ErrorClassNotFound::new(format!( + return ErrorClassNotFound(format!( "class `{class_name}` not found in {}", classes_path.display() )) @@ -122,7 +132,7 @@ pub fn handle_state_class_add(state: StateClassAdd, cwd: &ResCurrentDir) -> Next // Read the class template (relative to `.mling/`). let template_path = project_root.join(".mling").join(&entry.template); let template_content = fs::read_to_string(&template_path).map_err(|e| { - ErrorClassTemplateMissing::new(format!("failed to read {}: {e}", template_path.display())) + ErrorClassTemplateMissing(format!("failed to read {}: {e}", template_path.display())) })?; // Derive the name variants used by the template placeholders. @@ -139,7 +149,7 @@ pub fn handle_state_class_add(state: StateClassAdd, cwd: &ResCurrentDir) -> Next tmpl.insert_param("upper_snake_case".to_string(), upper_snake); tmpl.insert_param("camel_case".to_string(), camel); let expanded = tmpl.expand().ok_or_else(|| { - ErrorClassWriteFailed::new(format!( + ErrorClassWriteFailed(format!( "failed to expand class template: {}", template_path.display() )) @@ -149,11 +159,10 @@ pub fn handle_state_class_add(state: StateClassAdd, cwd: &ResCurrentDir) -> Next let output_dir = project_root.join(&entry.output_dir); let output = output_dir.join(format!("{snake}.rs")); fs::create_dir_all(&output_dir).map_err(|e| { - ErrorClassWriteFailed::new(format!("failed to create {}: {e}", output_dir.display())) - })?; - fs::write(&output, expanded).map_err(|e| { - ErrorClassWriteFailed::new(format!("failed to write {}: {e}", output.display())) + ErrorClassWriteFailed(format!("failed to create {}: {e}", output_dir.display())) })?; + fs::write(&output, expanded) + .map_err(|e| ErrorClassWriteFailed(format!("failed to write {}: {e}", output.display())))?; ResultClassAdd { output }.to_chain() } @@ -215,28 +224,28 @@ pub fn render_error_class_name_required(_err: ErrorClassNameRequired) -> RenderR #[renderer] pub fn render_error_class_config_missing(err: ErrorClassConfigMissing) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } #[renderer] pub fn render_error_class_not_found(err: ErrorClassNotFound) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } #[renderer] pub fn render_error_class_template_missing(err: ErrorClassTemplateMissing) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } #[renderer] pub fn render_error_class_write_failed(err: ErrorClassWriteFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } diff --git a/mingling_cli/src/proj_mgr/cmd_proj_init.rs b/mingling_cli/src/proj_mgr/cmd_proj_init.rs index 1ef8d1e..edad83c 100644 --- a/mingling_cli/src/proj_mgr/cmd_proj_init.rs +++ b/mingling_cli/src/proj_mgr/cmd_proj_init.rs @@ -7,8 +7,8 @@ use std::{ use just_fmt::snake_case; use just_template::Template; use mingling::{ - Grouped, LazyRes, RenderResult, Routable, - macros::{arg, chain, command, metadata, pack, pack_err, r_println, renderer, routeify}, + Grouped, LazyRes, RenderResult, Routable, Wrap, + macros::{arg, chain, command, metadata, r_println, renderer, routeify}, metadata::Description, picker::EntryPicker, res::ResCurrentDir, @@ -30,8 +30,11 @@ const RULE_FILENAME: &str = "rule.toml"; /// The directory under the project root where the template cache lives. const CACHE_DIR_NAME: &str = "tmpl-cache"; -pack!(StateProjectGenerate = ()); -pack!(StateProjectChecklistReady = Vec<String>); +#[derive(Grouped)] +pub struct StateProjectGenerate; + +#[derive(Grouped, Wrap)] +pub struct StateProjectChecklistReady(Vec<String>); /// Result of the checklist phase: the extracted checklist handed to the user. #[derive(Debug, Default, Grouped)] @@ -46,20 +49,31 @@ pub struct ResultProjectGenerate { pub hidden: Vec<PathBuf>, } -pack_err!(ErrorTemplateNotProvided = ()); -pack_err!(ErrorTemplateCopyFailed = String); -pack_err!(ErrorTemplateFetchFailed = String); -pack_err!(ErrorChecklistMissing = String); -pack_err!(ErrorRuleParseFailed = String); -pack_err!(ErrorTemplateExpandFailed = String); +#[derive(Grouped)] +pub struct ErrorTemplateNotProvided; + +#[derive(Grouped, Wrap)] +pub struct ErrorTemplateCopyFailed(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorTemplateFetchFailed(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorChecklistMissing(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorRuleParseFailed(String); + +#[derive(Grouped, Wrap)] +pub struct ErrorTemplateExpandFailed(String); #[command(node = "proj-init", routeify)] pub fn proj_init(args: Entry, cwd: &ResCurrentDir) -> Next { // Check if the checklist.toml file exists in the current directory if cwd.join(CHECKLIST_FILENAME).exists() { - StateProjectGenerate::new(()).into() + StateProjectGenerate.into() } else { - StateProjectChecklistReady::new(args.inner).into() + StateProjectChecklistReady(args.0).into() } } @@ -73,7 +87,7 @@ pub fn handle_state_proj_checklist_ready( ) -> Next { let source: TemplateSource = args .pick_or_route(&arg![TemplateSource], || { - ErrorTemplateNotProvided::new(()).to_chain() + ErrorTemplateNotProvided.to_chain() }) .to_result()?; @@ -88,7 +102,7 @@ pub fn handle_state_proj_checklist_ready( configured }); resolve_git(&source_url, &reference, &variant, &cache_dir()) - .map_err(ErrorTemplateFetchFailed::new)? + .map_err(ErrorTemplateFetchFailed)? } }; @@ -96,15 +110,15 @@ pub fn handle_state_proj_checklist_ready( // directory; create it if it doesn't exist let tmpl_cache = cwd.join(".mling").join(CACHE_DIR_NAME); fs::create_dir_all(&tmpl_cache).map_err(|e| { - ErrorTemplateCopyFailed::new(format!("failed to create {}: {e}", tmpl_cache.display())) + ErrorTemplateCopyFailed(format!("failed to create {}: {e}", tmpl_cache.display())) })?; copy_dir_contents(&template_root, &tmpl_cache) - .map_err(|e| ErrorTemplateCopyFailed::new(e.to_string()))?; + .map_err(|e| ErrorTemplateCopyFailed(e.to_string()))?; // Move the internal checklist.toml to ./ for the user to fill in let checklist_src = tmpl_cache.join(CHECKLIST_FILENAME); if !checklist_src.is_file() { - return ErrorChecklistMissing::new(format!( + return ErrorChecklistMissing(format!( "no checklist.toml found inside {}", template_root.display() )) @@ -112,7 +126,7 @@ pub fn handle_state_proj_checklist_ready( } let checklist_dst = cwd.join(CHECKLIST_FILENAME); fs::rename(&checklist_src, &checklist_dst).map_err(|e| { - ErrorTemplateCopyFailed::new(format!( + ErrorTemplateCopyFailed(format!( "failed to move checklist.toml to {}: {e}", checklist_dst.display() )) @@ -130,7 +144,7 @@ pub fn handle_state_proj_checklist_ready( pub fn handle_state_project_generate(_: StateProjectGenerate, cwd: &ResCurrentDir) -> Next { let tmpl_cache = cwd.join(".mling").join(CACHE_DIR_NAME); if !tmpl_cache.is_dir() { - return ErrorChecklistMissing::new(format!( + return ErrorChecklistMissing(format!( "template cache not found at {}; run `mling proj-init` with a template directory first", tmpl_cache.display() )) @@ -140,22 +154,22 @@ pub fn handle_state_project_generate(_: StateProjectGenerate, cwd: &ResCurrentDi // Read the user-filled checklist.toml let checklist_path = cwd.join(CHECKLIST_FILENAME); let checklist_content = fs::read_to_string(&checklist_path).map_err(|e| { - ErrorChecklistMissing::new(format!("failed to read {}: {e}", checklist_path.display())) + ErrorChecklistMissing(format!("failed to read {}: {e}", checklist_path.display())) })?; let answers = parse_checklist(&checklist_content) - .map_err(|e| ErrorRuleParseFailed::new(format!("invalid checklist.toml: {e}")))?; + .map_err(|e| ErrorRuleParseFailed(format!("invalid checklist.toml: {e}")))?; // Read rule.toml (template rules) let rule_content = fs::read_to_string(tmpl_cache.join(RULE_FILENAME)) - .map_err(|e| ErrorRuleParseFailed::new(format!("failed to read rule.toml: {e}")))?; + .map_err(|e| ErrorRuleParseFailed(format!("failed to read rule.toml: {e}")))?; let rules = parse_rules(&rule_content) - .map_err(|e| ErrorRuleParseFailed::new(format!("invalid rule.toml: {e}")))?; + .map_err(|e| ErrorRuleParseFailed(format!("invalid rule.toml: {e}")))?; // Compute final answers from checklist values + defaults declared in rule.toml let answers = resolve_answers(&answers, &rules); // Mutually exclusive toggle groups must not both be enabled. - validate_mutexes(&answers, &rules).map_err(ErrorRuleParseFailed::new)?; + validate_mutexes(&answers, &rules).map_err(ErrorRuleParseFailed)?; // Derive the crate name from the program name (e.g. `my-cli` -> `my_cli`). let mut params: HashMap<String, String> = answers.clone(); @@ -171,7 +185,7 @@ pub fn handle_state_project_generate(_: StateProjectGenerate, cwd: &ResCurrentDi // Expand all template entries to the project root let mut generated = Vec::new(); expand_tree(&tmpl_cache, cwd, ¶ms, &mut generated, true) - .map_err(ErrorTemplateExpandFailed::new)?; + .map_err(ErrorTemplateExpandFailed)?; // hide-file: delete the corresponding generated file when the rule is true @@ -182,7 +196,7 @@ pub fn handle_state_project_generate(_: StateProjectGenerate, cwd: &ResCurrentDi } let target = cwd.join(hide.file.trim_start_matches("./")); remove_path(&target).map_err(|e| { - ErrorTemplateExpandFailed::new(format!("failed to hide {}: {e}", target.display())) + ErrorTemplateExpandFailed(format!("failed to hide {}: {e}", target.display())) })?; hidden.push(target); } @@ -194,19 +208,19 @@ pub fn handle_state_project_generate(_: StateProjectGenerate, cwd: &ResCurrentDi } let target = cwd.join(hide.dir.trim_start_matches("./")); remove_path(&target).map_err(|e| { - ErrorTemplateExpandFailed::new(format!("failed to hide {}: {e}", target.display())) + ErrorTemplateExpandFailed(format!("failed to hide {}: {e}", target.display())) })?; hidden.push(target); } // Clean up the cache fs::remove_dir_all(&tmpl_cache).map_err(|e| { - ErrorTemplateExpandFailed::new(format!("failed to remove {}: {e}", tmpl_cache.display())) + ErrorTemplateExpandFailed(format!("failed to remove {}: {e}", tmpl_cache.display())) })?; // Project generated; remove the temporary checklist file remove_path(&checklist_path).map_err(|e| { - ErrorTemplateExpandFailed::new(format!( + ErrorTemplateExpandFailed(format!( "failed to remove {}: {e}", checklist_path.display() )) @@ -327,35 +341,35 @@ pub fn render_error_template_not_provided(_err: ErrorTemplateNotProvided) -> Ren #[renderer] pub fn render_error_template_copy_failed(err: ErrorTemplateCopyFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "failed to copy template: {}", err.info); + eprintln_cargo!(r, "failed to copy template: {}", err.0); r } #[renderer] pub fn render_error_template_fetch_failed(err: ErrorTemplateFetchFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "failed to fetch template: {}", err.info); + eprintln_cargo!(r, "failed to fetch template: {}", err.0); r } #[renderer] pub fn render_error_checklist_missing(err: ErrorChecklistMissing) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } #[renderer] pub fn render_error_rule_parse_failed(err: ErrorRuleParseFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } #[renderer] pub fn render_error_template_expand_failed(err: ErrorTemplateExpandFailed) -> RenderResult { let mut r = RenderResult::new(); - eprintln_cargo!(r, "{}", err.info); + eprintln_cargo!(r, "{}", err.0); r } diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 46238d6..c2dd952 100644 --- a/mingling_macros/src/attr/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -133,7 +133,7 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) { Ok(parsed) => ::mingling::Routable::<#program_path>::to_chain(parsed), Err(e) => { - return ::mingling::Routable::<#program_path>::to_render(#error_struct::new(format!("{}", e.render().ansi()))) + return ::mingling::Routable::<#program_path>::to_render(#error_struct(format!("{}", e.render().ansi()))) }, } } @@ -143,7 +143,8 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke // Generate the error pack type let error_pack = options.error_struct.as_ref().map(|error_struct| { quote! { - ::mingling::macros::pack!(#error_struct = String); + #[derive(::mingling::Grouped, ::mingling::Wrap, Default)] + pub struct #error_struct(pub ::std::string::String); } }); @@ -188,7 +189,7 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke // Keep the original struct definition #input_struct - // Generate the error wrapper type via pack! + // Generate the error wrapper type #error_pack // Generate the help block if enabled diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 57d0ec5..baa23db 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -9,13 +9,6 @@ pub(crate) mod gen_program; pub(crate) mod group; #[cfg(all(feature = "structural_renderer", feature = "extras"))] pub(crate) mod group_structural; -pub(crate) mod pack; -#[cfg(feature = "extras")] -pub(crate) mod pack_err; -#[cfg(all(feature = "structural_renderer", feature = "extras"))] -pub(crate) mod pack_err_structural; -#[cfg(feature = "structural_renderer")] -pub(crate) mod pack_structural; #[cfg(feature = "comp")] pub(crate) mod program_comp_gen; pub(crate) mod program_fallback_gen; diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index c4d67a9..41dd2d0 100644 --- a/mingling_macros/src/func/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -115,7 +115,9 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { let program_type = crate::default_program_path(); let expanded = quote! { - ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec<String>); + #[derive(::mingling::Grouped, ::mingling::Wrap, Default)] + #(#entry_attrs)* + pub struct #pack(pub ::std::vec::Vec<::std::string::String>); #(#cmd_attrs)* #[doc(hidden)] @@ -127,7 +129,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { impl From<#pack> for crate::Entry { fn from(value: #pack) -> Self { - crate::Entry::new(value.inner) + crate::Entry(value.0) } } @@ -136,7 +138,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { impl ::mingling::Dispatcher<#program_type> for #hidden_dispatcher { fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> { use ::mingling::Grouped; - ::mingling::Routable::to_chain(#pack::new(args)) + ::mingling::Routable::to_chain(#pack(args)) } } }; @@ -149,7 +151,7 @@ fn get_comp_entry(entry_name: &Ident) -> TokenStream2 { let comp_entry = quote! { impl ::mingling::CompletionEntry for #entry_name { fn get_input(self) -> Vec<String> { - self.inner.clone() + self.0.clone() } } }; diff --git a/mingling_macros/src/func/entry.rs b/mingling_macros/src/func/entry.rs index 82dfd3e..18e485f 100644 --- a/mingling_macros/src/func/entry.rs +++ b/mingling_macros/src/func/entry.rs @@ -57,7 +57,7 @@ pub(crate) fn entry(input: TokenStream) -> TokenStream { let expanded = match parsed { EntryInput::Typed { ident, .. } => { quote! { - #ident::new(vec![#(#string_exprs),*]) + #ident(vec![#(#string_exprs),*]) } } EntryInput::Untyped { .. } => { diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs index a53f86f..c0a7ea8 100644 --- a/mingling_macros/src/func/gen_program.rs +++ b/mingling_macros/src/func/gen_program.rs @@ -57,7 +57,8 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { /// Alias for the current program type `ThisProgram` pub type Next = ::mingling::ChainProcess<ThisProgram>; - ::mingling::macros::pack!(Entry = Vec<String>); + #[derive(::mingling::Grouped, ::mingling::Wrap, Default)] + pub struct Entry(pub ::std::vec::Vec<::std::string::String>); impl ::mingling::Routable<ThisProgram> for ::mingling::ChainProcess<ThisProgram> { diff --git a/mingling_macros/src/func/pack.rs b/mingling_macros/src/func/pack.rs deleted file mode 100644 index d4fcb60..0000000 --- a/mingling_macros/src/func/pack.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Doc Not Optimize -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, Result as SynResult, Token, Type}; - -struct PackInput { - attrs: Vec<Attribute>, - type_name: Ident, - inner_type: Type, -} - -impl Parse for PackInput { - fn parse(input: ParseStream) -> SynResult<Self> { - let attrs = input.call(Attribute::parse_outer)?; - let type_name: Ident = input.parse()?; - input.parse::<Token![=]>()?; - let inner_type: Type = input.parse()?; - - Ok(Self { - attrs, - type_name, - inner_type, - }) - } -} - -#[allow(clippy::too_many_lines)] -pub(crate) fn pack(input: TokenStream) -> TokenStream { - let pack_input = syn::parse_macro_input!(input as PackInput); - - let group_name = crate::default_program_path(); - let type_name = pack_input.type_name; - let inner_type = pack_input.inner_type; - let attrs = pack_input.attrs; - - // Generate the struct definition - // Note: No longer derives Serialize under structural_renderer. - // Use pack_structual! for structured output support. - let struct_def = quote! { - #(#attrs)* - pub struct #type_name { - pub(crate) inner: #inner_type, - } - }; - - // Generate the new() method - let new_impl = quote! { - impl #type_name { - /// Creates a new instance of the wrapper type - pub fn new(inner: #inner_type) -> Self { - Self { inner } - } - } - }; - - // Generate From and Into implementations - let from_into_impl = quote! { - impl From<#inner_type> for #type_name { - fn from(inner: #inner_type) -> Self { - Self::new(inner) - } - } - - impl From<#type_name> for #inner_type { - fn from(wrapper: #type_name) -> #inner_type { - wrapper.inner - } - } - }; - - // Generate AsRef and AsMut implementations - let as_ref_impl = quote! { - impl ::std::convert::AsRef<#inner_type> for #type_name { - fn as_ref(&self) -> &#inner_type { - &self.inner - } - } - - impl ::std::convert::AsMut<#inner_type> for #type_name { - fn as_mut(&mut self) -> &mut #inner_type { - &mut self.inner - } - } - }; - - // Generate Deref and DerefMut implementations - let deref_impl = quote! { - impl ::std::ops::Deref for #type_name { - type Target = #inner_type; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl ::std::ops::DerefMut for #type_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - }; - - // Check if the inner type implements Default by generating conditional code - let default_impl = quote! { - impl ::std::default::Default for #type_name - where - #inner_type: ::std::default::Default, - { - fn default() -> Self { - Self::new(::std::default::Default::default()) - } - } - }; - - let register_impl = quote! { - ::mingling::macros::register_type!(#type_name); - }; - - let expanded = quote! { - #struct_def - - #new_impl - #from_into_impl - #as_ref_impl - #deref_impl - #default_impl - #register_impl - - impl Into<mingling::AnyOutput<#group_name>> for #type_name { - fn into(self) -> mingling::AnyOutput<#group_name> { - mingling::AnyOutput::new(self) - } - } - - impl Into<mingling::ChainProcess<#group_name>> for #type_name { - fn into(self) -> mingling::ChainProcess<#group_name> { - mingling::AnyOutput::new(self).route_chain() - } - } - - /// SAFETY: This is an internal implementation of the `pack!` macro, - /// guaranteeing that the enum value registered by the `register_type!` macro - /// is exactly the same as the actual return value, - /// which can be confirmed via the `Ident` in the `quote!` block. - unsafe impl ::mingling::Grouped<#group_name> for #type_name { - fn member_id() -> #group_name { - #group_name::#type_name - } - } - }; - - expanded.into() -} diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs deleted file mode 100644 index e925b82..0000000 --- a/mingling_macros/src/func/pack_err.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Doc Not Optimize -use just_fmt::snake_case; -use proc_macro::TokenStream; -use quote::quote; -use syn::{Ident, Token, Type, parse_macro_input}; - -enum PackErrInput { - /// `pack_err!(ErrorNotFound)` - Simple { type_name: Ident }, - /// `pack_err!(ErrorNotDir = PathBuf)` - Typed { - type_name: Ident, - inner_type: Box<Type>, - }, -} - -impl syn::parse::Parse for PackErrInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let type_name: Ident = input.parse()?; - - if input.peek(Token![=]) { - input.parse::<Token![=]>()?; - let inner_type: Type = input.parse()?; - Ok(Self::Typed { - type_name, - inner_type: Box::new(inner_type), - }) - } else { - Ok(Self::Simple { type_name }) - } - } -} - -#[allow(clippy::too_many_lines)] -pub(crate) fn pack_err(input: TokenStream) -> TokenStream { - let parsed = parse_macro_input!(input as PackErrInput); - - match parsed { - PackErrInput::Simple { type_name } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - // Note: No longer derives Serialize under structural_renderer. - // Use pack_err_structural for structured output support. - let derive = quote! { - #[derive(::mingling::Grouped)] - }; - - let expanded = quote! { - #derive - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - } - - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - name: #snake_name.into(), - } - } - } - - ::mingling::macros::register_type!(#type_name); - }; - - expanded.into() - } - PackErrInput::Typed { - type_name, - inner_type, - } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - // Note: No longer derives Serialize under structural_renderer. - // Use pack_err_structural for structured output support. - let derive = quote! { - #[derive(::mingling::Grouped)] - }; - - let expanded = quote! { - #derive - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - /// Additional context info for this error. - pub info: #inner_type, - } - - impl #type_name { - /// Creates a new error with the given info. - /// The `name` field is automatically set to the snake_case of the struct name. - pub fn new(info: #inner_type) -> Self { - Self { - name: #snake_name.into(), - info, - } - } - } - - ::mingling::macros::register_type!(#type_name); - }; - - expanded.into() - } - } -} diff --git a/mingling_macros/src/func/pack_err_structural.rs b/mingling_macros/src/func/pack_err_structural.rs deleted file mode 100644 index 950b8dc..0000000 --- a/mingling_macros/src/func/pack_err_structural.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Doc Not Optimize -use just_fmt::snake_case; -use proc_macro::TokenStream; -use quote::quote; -use syn::{Ident, Token, Type, parse_macro_input}; - -/// `pack_err_structural!` — like `pack_err!` but also marks the type as -/// supporting structured output via `StructuralData`. -pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { - let parsed = parse_macro_input!(input as PackErrInput); - - let type_name = match &parsed { - PackErrInput::Simple { type_name } | PackErrInput::Typed { type_name, .. } => { - type_name.clone() - } - }; - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - crate::get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - let structural_data = quote! { - impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} - impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} - }; - - // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed) - match parsed { - PackErrInput::Simple { type_name } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - let expanded = quote! { - #[derive(::mingling::Grouped, ::serde::Serialize)] - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - } - - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - name: #snake_name.into(), - } - } - } - - ::mingling::macros::register_type!(#type_name); - - #structural_data - }; - - expanded.into() - } - PackErrInput::Typed { - type_name, - inner_type, - } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - let expanded = quote! { - #[derive(::mingling::Grouped, ::serde::Serialize)] - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - /// Additional context info for this error. - pub info: #inner_type, - } - - impl #type_name { - /// Creates a new error with the given info. - /// The `name` field is automatically set to the snake_case of the struct name. - pub fn new(info: #inner_type) -> Self { - Self { - name: #snake_name.into(), - info, - } - } - } - - ::mingling::macros::register_type!(#type_name); - - #structural_data - }; - - expanded.into() - } - } -} - -// Re-use pack_err's input parser -enum PackErrInput { - Simple { - type_name: Ident, - }, - Typed { - type_name: Ident, - inner_type: Box<Type>, - }, -} - -impl syn::parse::Parse for PackErrInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let type_name: Ident = input.parse()?; - - if input.peek(Token![=]) { - input.parse::<Token![=]>()?; - let inner_type: Type = input.parse()?; - Ok(Self::Typed { - type_name, - inner_type: Box::new(inner_type), - }) - } else { - Ok(Self::Simple { type_name }) - } - } -} diff --git a/mingling_macros/src/func/pack_structural.rs b/mingling_macros/src/func/pack_structural.rs deleted file mode 100644 index fb13fbf..0000000 --- a/mingling_macros/src/func/pack_structural.rs +++ /dev/null @@ -1,170 +0,0 @@ -// Doc Not Optimize -use proc_macro::TokenStream; -use quote::quote; -use syn::Ident; - -use crate::get_global_set; - -/// `pack_structural!` — like `pack!` but also marks the type as supporting -/// structured output via `StructuralData`. -#[allow(clippy::too_many_lines)] -pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { - // Parse same input format as `pack!` - let input_parsed = syn::parse_macro_input!(input as PackStructuralInput); - let type_name = input_parsed.type_name; - let inner_type = input_parsed.inner_type; - let attrs = input_parsed.attrs; - let program_path = crate::default_program_path(); - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - // Struct definition (with Serialize derive, same as pack! under structural_renderer) - #[cfg(not(feature = "structural_renderer"))] - let struct_def = quote! { - #(#attrs)* - pub struct #type_name { - pub inner: #inner_type, - } - }; - - #[cfg(feature = "structural_renderer")] - let struct_def = quote! { - #(#attrs)* - #[derive(serde::Serialize)] - pub struct #type_name { - pub inner: #inner_type, - } - }; - - // Helper impls (same as pack!) - let new_impl = quote! { - impl #type_name { - pub fn new(inner: #inner_type) -> Self { - Self { inner } - } - } - }; - - let from_into_impl = quote! { - impl From<#inner_type> for #type_name { - fn from(inner: #inner_type) -> Self { - Self::new(inner) - } - } - impl From<#type_name> for #inner_type { - fn from(wrapper: #type_name) -> #inner_type { - wrapper.inner - } - } - }; - - let as_ref_impl = quote! { - impl ::std::convert::AsRef<#inner_type> for #type_name { - fn as_ref(&self) -> &#inner_type { - &self.inner - } - } - impl ::std::convert::AsMut<#inner_type> for #type_name { - fn as_mut(&mut self) -> &mut #inner_type { - &mut self.inner - } - } - }; - - let deref_impl = quote! { - impl ::std::ops::Deref for #type_name { - type Target = #inner_type; - fn deref(&self) -> &Self::Target { - &self.inner - } - } - impl ::std::ops::DerefMut for #type_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - }; - - let default_impl = quote! { - impl ::std::default::Default for #type_name - where - #inner_type: ::std::default::Default, - { - fn default() -> Self { - Self::new(::std::default::Default::default()) - } - } - }; - - let register_impl = quote! { - ::mingling::macros::register_type!(#type_name); - }; - - // StructuralData impl + sealed + registration - let structural_impl = quote! { - impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} - impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} - }; - - let expanded = quote! { - #struct_def - - #new_impl - #from_into_impl - #as_ref_impl - #deref_impl - #default_impl - #register_impl - #structural_impl - - impl Into<::mingling::AnyOutput<#program_path>> for #type_name { - fn into(self) -> ::mingling::AnyOutput<#program_path> { - ::mingling::AnyOutput::new(self) - } - } - - impl Into<::mingling::ChainProcess<#program_path>> for #type_name { - fn into(self) -> ::mingling::ChainProcess<#program_path> { - ::mingling::AnyOutput::new(self).route_chain() - } - } - - /// SAFETY: This is an internal implementation of the `pack_structural!` macro, - /// guaranteeing that the enum value registered by the `register_type!` macro - /// is exactly the same as the actual return value, - /// which can be confirmed via the `Ident` in the `quote!` block. - unsafe impl ::mingling::Grouped<#program_path> for #type_name { - fn member_id() -> #program_path { - #program_path::#type_name - } - } - }; - - expanded.into() -} - -/// Input for `pack_structural!` — same format as `pack!`. -struct PackStructuralInput { - attrs: Vec<syn::Attribute>, - type_name: Ident, - inner_type: syn::Type, -} - -impl syn::parse::Parse for PackStructuralInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let attrs = input.call(syn::Attribute::parse_outer)?; - let type_name: Ident = input.parse()?; - input.parse::<syn::Token![=]>()?; - let inner_type: syn::Type = input.parse()?; - Ok(Self { - attrs, - type_name, - inner_type, - }) - } -} diff --git a/mingling_macros/src/func/program_comp_gen.rs b/mingling_macros/src/func/program_comp_gen.rs index ed04379..cdd2597 100644 --- a/mingling_macros/src/func/program_comp_gen.rs +++ b/mingling_macros/src/func/program_comp_gen.rs @@ -11,11 +11,11 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { pub async fn __exec_completion(prev: CompletionContext) -> Next { use ::mingling::Grouped; - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + let read_ctx = ::mingling::ShellContext::try_from(prev.0); match read_ctx { Ok(ctx) => { let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest::new((ctx, suggest))) + ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest((ctx, suggest))) } Err(_) => std::process::exit(1), } @@ -29,11 +29,11 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { pub fn __exec_completion(prev: CompletionContext) -> Next { use ::mingling::Grouped; - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + let read_ctx = ::mingling::ShellContext::try_from(prev.0); match read_ctx { Ok(ctx) => { let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest::new((ctx, suggest))) + ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest((ctx, suggest))) } Err(_) => std::process::exit(1), } @@ -50,9 +50,8 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { mod __internal_completion_mod { use ::mingling::Grouped; ::mingling::macros::dispatcher!("__comp", CompletionContext); - ::mingling::macros::pack!( - CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) - ); + #[derive(::mingling::Grouped, ::mingling::Wrap)] + pub struct CompletionSuggest(pub (::mingling::ShellContext, ::mingling::Suggest)); } #internal_dispatcher_comp use __internal_completion_mod::CompletionContext; @@ -67,7 +66,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { #[::mingling::macros::renderer] pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { let result = ::mingling::RenderResult::default(); - let (ctx, suggest) = prev.inner; + let (ctx, suggest) = prev.0; ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(&ctx, suggest); result } diff --git a/mingling_macros/src/func/program_fallback_gen.rs b/mingling_macros/src/func/program_fallback_gen.rs index df53c60..1f3fdce 100644 --- a/mingling_macros/src/func/program_fallback_gen.rs +++ b/mingling_macros/src/func/program_fallback_gen.rs @@ -16,8 +16,12 @@ pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { }; let expanded = quote! { - ::mingling::macros::pack!(ErrorRendererNotFound = String); - ::mingling::macros::pack!(EntryFallback = Vec<String>); + #[derive(::mingling::Grouped, ::mingling::Wrap, Default)] + pub struct ErrorRendererNotFound(pub ::std::string::String); + + #[derive(::mingling::Grouped, ::mingling::Wrap, Default)] + pub struct EntryFallback(pub ::std::vec::Vec<::std::string::String>); + #pack_empty }; TokenStream::from(expanded) diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs index d549a2b..8123eaf 100644 --- a/mingling_macros/src/func/program_final_gen.rs +++ b/mingling_macros/src/func/program_final_gen.rs @@ -376,10 +376,10 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { type ResultEmpty = ResultEmpty; fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) + ::mingling::AnyOutput::new(ErrorRendererNotFound(member_id.to_string())) } fn build_entry_fallback(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(EntryFallback::new(args)) + ::mingling::AnyOutput::new(EntryFallback(args)) } fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { ::mingling::AnyOutput::new(ResultEmpty) diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 7da7db3..d0556dc 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -34,15 +34,13 @@ use attr::dispatcher_clap; use attr::program_setup; use attr::{chain, help, metadata, renderer}; use derive::{enum_tag, grouped, wrap}; +use func::dispatcher; #[cfg(feature = "extras")] use func::entry; #[cfg(feature = "extras")] pub(crate) use func::group as group_impl; -#[cfg(feature = "extras")] -use func::pack_err; #[cfg(feature = "comp")] use func::suggest; -use func::{dispatcher, pack}; use systems::res_injection; pub(crate) fn default_program_path() -> proc_macro2::TokenStream { quote::quote! { crate::ThisProgram } @@ -60,7 +58,7 @@ pub(crate) type Registry = OnceLock<Mutex<BTreeSet<String>>>; pub(crate) static STRUCTURAL_RENDERERS: Registry = OnceLock::new(); /// Types explicitly marked with `#[derive(StructuralData)]` or created via -/// `pack_structural!` / `group_structural!`. +/// `group_structural!`. #[cfg(feature = "structural_renderer")] pub(crate) static STRUCTURED_TYPES: Registry = OnceLock::new(); @@ -183,163 +181,6 @@ pub fn group_structural(input: TokenStream) -> TokenStream { func::group_structural::group_structural(input) } -/// Creates a type-safe wrapper struct around an inner type, with automatic -/// trait implementations for use in the Mingling chain/render pipeline. -/// -/// The generated struct implements: `From`/`Into`, `AsRef`/`AsMut`, `Deref`/`DerefMut`, -/// `Default` (conditional on inner type), and conversion into `AnyOutput` / -/// `ChainProcess` for routing. -/// -/// # Syntax -/// -/// ```rust,ignore -/// // Default program name (uses `ThisProgram`): -/// pack!(TypeName = InnerType); -/// -/// // Explicit program name: -/// pack!(MyProgram, TypeName = InnerType); -/// ``` -/// -/// # Example -/// -/// ```rust,ignore -/// use mingling::macros::pack; -/// -/// // Creates `Hello` wrapping `String`, registered under `ThisProgram`: -/// pack!(Hello = String); -/// -/// // Creates `Greeting` wrapping `String`, registered under `MyApp`: -/// pack!(MyApp, Greeting = String); -/// ``` -/// -/// After expansion, `Hello` has: -/// - `Hello::new(String)` — constructor -/// - `Hello::to_chain()` — routes to the next chain processor -/// - `Hello::to_render()` — routes to a renderer -/// - `From<String> for Hello`, `From<Hello> for String` -/// - `Deref<Target = String>`, `DerefMut` -/// - `AsRef<String>`, `AsMut<String>` -/// - `Default` if `String: Default` -/// - `Into<AnyOutput<ThisProgram>>`, `Into<ChainProcess<ThisProgram>>` -/// - Implements `Grouped<ThisProgram>` with `member_id()` returning the enum variant -/// -/// The struct is also registered via `register_type!` so that `gen_program!` -/// can include it in the program enum. -/// -/// When the `structural_renderer` feature is enabled, the struct also gets -/// `#[derive(serde::Serialize)]`. -#[proc_macro] -pub fn pack(input: TokenStream) -> TokenStream { - pack::pack(input) -} - -/// Like `pack!` but also marks the type as supporting structured output -/// (JSON / YAML / TOML / RON) via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// pack_structural!(Info = (String, i32)); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// pack!(Info = (String, i32)); -/// impl ::mingling::StructuralData for Info {} -/// ``` -/// -/// Requires the `structural_renderer` feature. -#[cfg(feature = "structural_renderer")] -#[proc_macro] -pub fn pack_structural(input: TokenStream) -> TokenStream { - func::pack_structural::pack_structural(input) -} - -/// Creates an error struct with a `name: String` field and optional `info: Type` field. -/// -/// This macro provides a concise way to define error types that implement `Grouped` -/// and are registered for inclusion in the program enum. -/// -/// The `name` field is automatically set to the `snake_case` version of the struct name -/// at compile time. -/// -/// # Syntax -/// -/// Two forms are supported: -/// -/// ```rust,ignore -/// // Simple form — generates a struct with only `name: String` and a `Default` impl: -/// pack_err!(ErrorNotFound); -/// -/// // Typed form — generates a struct with `name: String` + `info: Type` and a `new(info)` constructor: -/// pack_err!(ErrorNotDir = PathBuf); -/// ``` -/// -/// # Generated code -/// -/// For `pack_err!(ErrorNotFound)`: -/// -/// ```rust,ignore -/// #[derive(::mingling::Grouped)] -/// pub struct ErrorNotFound { -/// name: String, -/// } -/// -/// impl Default for ErrorNotFound { -/// fn default() -> Self { -/// Self { -/// name: "error_not_found".into(), -/// } -/// } -/// } -/// ``` -/// -/// For `pack_err!(ErrorNotDir = PathBuf)`: -/// -/// ```rust,ignore -/// #[derive(::mingling::Grouped)] -/// pub struct ErrorNotDir { -/// name: String, -/// info: PathBuf, -/// } -/// -/// impl ErrorNotDir { -/// pub fn new(info: PathBuf) -> Self { -/// Self { -/// name: "error_not_dir".into(), -/// info, -/// } -/// } -/// } -/// ``` -/// -/// When the `structural_renderer` feature is enabled, the struct also gets -/// `#[derive(serde::Serialize)]`. -/// -/// This macro is only available with the `extras` feature. -#[cfg(feature = "extras")] -#[proc_macro] -pub fn pack_err(input: TokenStream) -> TokenStream { - pack_err::pack_err(input) -} - -/// Like `pack_err!` but also marks the type for structured output -/// (JSON / YAML / TOML / RON) via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// pack_err_structural!(ErrorNotFound); -/// pack_err_structural!(ErrorNotDir = PathBuf); -/// ``` -/// -/// Requires the `structural_renderer` and `extras` features. -#[cfg(all(feature = "structural_renderer", feature = "extras"))] -#[proc_macro] -pub fn pack_err_structural(input: TokenStream) -> TokenStream { - func::pack_err_structural::pack_err_structural(input) -} - /// Early-returns the error from a `Result`, converting the `Ok` branch to the /// next chain process value. /// @@ -555,7 +396,7 @@ pub fn empty_result(input: TokenStream) -> TokenStream { /// /// The macro generates: /// -/// 1. **Entry struct** — A `pack!`-style wrapper around `Vec<String>` (the raw args). +/// 1. **Entry struct** — A newtype wrapper around `Vec<String>` (the raw args). /// Registered in the program enum via `register_type!`. /// 2. **Dispatcher struct** — A hidden zero-sized struct implementing [`Dispatcher<Program>`]: /// - `begin(args)` wraps `args` into the entry type and routes to chain. @@ -664,78 +505,89 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { /// # Sync Example /// /// ```rust,ignore -/// use mingling::macros::{chain, pack, gen_program}; +/// use mingling::macros::{chain, gen_program}; +/// use mingling::{Grouped, Wrap}; /// -/// pack!(MyOutput = String); +/// #[derive(Grouped, Wrap)] +/// pub struct MyOutput(String); /// /// #[chain] /// fn greet(prev: HelloEntry) -> Next { /// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); -/// MyOutput::new(name) +/// MyOutput(name) /// } /// ``` /// /// # Sync Example with Resource Injection /// /// ```rust,ignore -/// use mingling::macros::{chain, pack, gen_program}; +/// use mingling::macros::{chain, gen_program}; +/// use mingling::{Grouped, Wrap}; /// /// #[derive(Default, Clone)] /// struct UserName(String); /// -/// pack!(Greeting = String); -/// pack!(DisplayCount = ()); +/// #[derive(Grouped, Wrap)] +/// pub struct Greeting(String); +/// #[derive(Grouped, Wrap)] +/// pub struct DisplayCount(()); /// /// #[chain] /// fn greet(prev: HelloEntry, user_name: &UserName, count: &mut u64) -> Next { /// *count += 1; -/// Greeting::new(format!("Hello, {}!", user_name.0)) +/// Greeting(format!("Hello, {}!", user_name.0)) /// } /// ``` /// /// # Async Example (with `async` feature) /// /// ```rust,ignore -/// use mingling::macros::{chain, pack, gen_program}; +/// use mingling::macros::{chain, gen_program}; +/// use mingling::{Grouped, Wrap}; /// -/// pack!(MyOutput = String); +/// #[derive(Grouped, Wrap)] +/// pub struct MyOutput(String); /// /// #[chain] /// async fn greet(prev: HelloEntry) -> Next { /// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); /// some_async_fn(&name).await; -/// MyOutput::new(name) +/// MyOutput(name) /// } /// ``` /// /// # Async Example with Immutable Resource Injection /// /// ```rust,ignore -/// use mingling::macros::{chain, pack, gen_program}; +/// use mingling::macros::{chain, gen_program}; +/// use mingling::{Grouped, Wrap}; /// -/// pack!(MyOutput = String); +/// #[derive(Grouped, Wrap)] +/// pub struct MyOutput(String); /// /// #[chain] /// async fn greet(prev: HelloEntry, prefix: &Prefix) -> Next { /// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); /// some_async_fn(&name).await; -/// MyOutput::new(format!("{}{}", prefix.0, name)) +/// MyOutput(format!("{}{}", prefix.0, name)) /// } /// ``` /// /// # Async Example with Mutable Resource Injection /// /// ```rust,ignore -/// use mingling::macros::{chain, pack, gen_program}; +/// use mingling::macros::{chain, gen_program}; +/// use mingling::{Grouped, Wrap}; /// -/// pack!(MyOutput = String); +/// #[derive(Grouped, Wrap)] +/// pub struct MyOutput(String); /// /// #[chain] /// async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next { /// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); /// ec.exit_code = 42; /// some_async_fn(&name).await; -/// MyOutput::new(name) +/// MyOutput(name) /// } /// ``` /// @@ -777,10 +629,12 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { /// # Example /// /// ```rust,ignore -/// use mingling::macros::{renderer, pack, gen_program}; +/// use mingling::macros::{renderer, gen_program}; +/// use mingling::{Grouped, Wrap}; /// use std::io::Write; /// -/// pack!(Greeting = String); +/// #[derive(Grouped, Wrap)] +/// pub struct Greeting(String); /// /// #[renderer] /// fn render_greeting(prev: Greeting) -> RenderResult { @@ -1063,7 +917,7 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { /// Creates a packed entry value from a list of string literals. /// /// This is a convenience macro for constructing entry wrapper types (created -/// via `pack!` or `dispatcher!`) with test data, typically used in unit tests +/// via `dispatcher!`) with test data, typically used in unit tests /// or quick prototypes. /// /// # Syntax @@ -1071,9 +925,9 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { /// Two forms: /// /// ```rust,ignore -/// // Named form — wraps into a specific pack type: +/// // Named form — wraps into a specific entry type: /// entry!(MyEntry, ["a", "b", "c"]) -/// // Expands to: MyEntry::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]) +/// // Expands to: MyEntry(vec!["a".to_string(), "b".to_string(), "c".to_string()]) /// /// // Bracket form — returns Vec<String>.into() for type inference: /// entry!["a", "b", "c"] @@ -1085,7 +939,7 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { /// ```rust,ignore /// use mingling::macros::entry; /// -/// // Named form (with a specific pack type): +/// // Named form (with a specific entry type): /// let args = entry!(MyEntry, ["--name", "Alice", "--count", "5"]); /// /// // Bracket form (type inference): @@ -1094,8 +948,7 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// # See also /// -/// - `pack!` — For creating the wrapper types used with `entry!`. -/// - `dispatcher!` — Which implicitly creates entry types via `pack!`. +/// - `dispatcher!` — Which implicitly creates entry types. #[cfg(feature = "extras")] #[proc_macro] pub fn entry(input: TokenStream) -> TokenStream { @@ -1204,11 +1057,12 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { /// # Example /// /// ```rust,ignore -/// use mingling::macros::{help, pack, gen_program}; +/// use mingling::macros::{help, gen_program}; /// use mingling::{prelude::*, setup::BasicProgramSetup, RenderResult}; /// use std::io::Write; /// -/// pack!(MyEntry = Vec<String>); +/// #[derive(Grouped, Wrap)] +/// pub struct MyEntry(Vec<String>); /// /// #[help] /// fn help_my_entry(prev: MyEntry) -> RenderResult { @@ -1620,8 +1474,8 @@ pub fn derive_wrap(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// This is equivalent to using `pack!` but works with custom structs that -/// have named fields. For simple wrappers, prefer `pack!`. +/// This is equivalent to using `#[derive(Grouped)]` but works with custom structs that +/// have named fields. #[proc_macro_derive(Grouped, attributes(group))] pub fn derive_grouped(input: TokenStream) -> TokenStream { grouped::derive_grouped(input) @@ -1813,7 +1667,7 @@ pub fn program_comp_gen(input: TokenStream) -> TokenStream { /// Registers a type into the global packed types registry for inclusion in /// the program enum generated by `gen_program!`. /// -/// This macro is called internally by `pack!` and `#[derive(Grouped)]`(`macro.derive_grouped.html`) +/// This macro is called internally by `#[derive(Grouped)]` (`macro.derive_grouped.html`) /// and is generally not needed in user code. However, it can be used for manual /// registration if you are implementing custom type registration outside of /// the standard macros. @@ -1885,7 +1739,7 @@ pub fn program_fallback_gen(input: TokenStream) -> TokenStream { /// and its `ProgramCollect` implementation. /// /// This is the core code generation macro that: -/// 1. Collects all registered types (from `pack!`, `#[derive(Grouped)]`, etc.) and +/// 1. Collects all registered types (from `#[derive(Grouped)]`, etc.) and /// creates an enum with each type as a variant. /// 2. Generates the `Display` implementation for the enum. /// 3. Generates the `ProgramCollect` implementation that dispatches to all diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs index ef3624e..3279b51 100644 --- a/mingling_macros/src/systems.rs +++ b/mingling_macros/src/systems.rs @@ -1,8 +1,7 @@ -// Doc Not Optimize #[cfg(not(feature = "dispatch_tree"))] pub(crate) mod dispatch_list_gen; + #[cfg(feature = "dispatch_tree")] pub(crate) mod dispatch_tree_gen; + pub(crate) mod res_injection; -#[cfg(feature = "structural_renderer")] -pub(crate) mod structural_data; diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs deleted file mode 100644 index ac9b1ca..0000000 --- a/mingling_macros/src/systems/structural_data.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Doc Not Optimize -//! Legacy structural data module. -//! -//! Functions have been moved to: -//! - `derive::structural_data` — `derive_structural_data` -//! - `func::pack_structural` — `pack_structural` -//! - `func::group_structural` — `group_structural` diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index b7f923c..5b8b096 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -4,7 +4,7 @@ //! //! It provides a pluggable architecture via the `AnalyzePattern` trait, allowing different //! syntactic patterns to be registered and applied. Built-in patterns cover common structures -//! such as basic structs, packs, groups, derives, chains, renderers, help, completion, and +//! such as basic structs, groups, derives, chains, renderers, help, completion, and //! dispatch patterns (both standard and clap-based). //! //! The entry points are: @@ -18,14 +18,13 @@ use std::path::Path; use crate::error::MinglingPathfinderError; use crate::patterns::{ ChainPattern, CommandPattern, CompletionPattern, DispatcherClapPattern, DispatcherPattern, - GroupPattern, GroupedDerivePattern, HelpPattern, MetadataPattern, PackPattern, RendererPattern, + GroupPattern, GroupedDerivePattern, HelpPattern, MetadataPattern, RendererPattern, }; /// Creates a default `PatternAnalyzer` with all built-in patterns pre-registered. #[must_use] pub fn init() -> PatternAnalyzer { let mut analyzer = PatternAnalyzer::new(); - analyzer.add_pattern(PackPattern); analyzer.add_pattern(GroupPattern); analyzer.add_pattern(GroupedDerivePattern); analyzer.add_pattern(ChainPattern); diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs index fa0fc8b..43c60ac 100644 --- a/mingling_pathf/src/patterns.rs +++ b/mingling_pathf/src/patterns.rs @@ -10,7 +10,6 @@ pub use group::*; pub use grouped_derive::*; pub use help::*; pub use metadata::*; -pub use pack::*; pub use renderer::*; mod chain; @@ -22,5 +21,4 @@ mod group; mod grouped_derive; mod help; mod metadata; -mod pack; mod renderer; diff --git a/mingling_pathf/src/patterns/pack.rs b/mingling_pathf/src/patterns/pack.rs deleted file mode 100644 index 10327a7..0000000 --- a/mingling_pathf/src/patterns/pack.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Doc Not Optimize -//! The `PackPattern` matches types defined by `pack!`, `pack_err!`, `pack_structural!`, and `pack_err_structural!` macros. -//! It extracts the registered type name (e.g., `TypeName` from `pack!(TypeName = InnerType)`). -//! This is used to track packed type definitions for code generation or analysis. - -use syn::Item; - -use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; - -/// Matches types defined by `pack!`, `pack_err!`, `pack_structural!`, `pack_err_structural!` macros. -/// -/// Covered forms: -/// - `pack!(TypeName = InnerType)` -/// - `pack! { TypeName = InnerType }` -/// - `pack_err!(TypeName)` -/// - `pack_err!(TypeName = InnerType)` -/// - `pack_structural!` series same as above -pub struct PackPattern; - -impl AnalyzePattern for PackPattern { - fn contains(&self, content: &str) -> bool { - content.contains("pack!") - || content.contains("pack_err!") - || content.contains("pack_structural!") - || content.contains("pack_err_structural!") - } - - fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { - let Ok(syntax) = syn::parse_file(content) else { - return Vec::new(); - }; - - let mut items = Vec::new(); - - for item in &syntax.items { - match item { - // Top-level macro calls - Item::Macro(m) => { - if let Some(name) = try_extract_pack_name(m) { - items.push(AnalyzeItem::local(String::new(), name)); - } - } - // Macro calls inside inline modules - Item::Mod(item_mod) => { - if let Some((_, nested)) = &item_mod.content { - for n in nested { - if let Item::Macro(m) = n - && let Some(name) = try_extract_pack_name(m) - { - items.push(AnalyzeItem::local(item_mod.ident.to_string(), name)); - } - } - } - } - _ => {} - } - } - - items - } -} - -/// If the macro call is `pack!` / `pack_err!` / etc., extract the registered type name. -fn try_extract_pack_name(m: &syn::ItemMacro) -> Option<String> { - let macro_name = m.mac.path.segments.last()?.ident.to_string(); - - match macro_name.as_str() { - "pack" | "pack_err" | "pack_structural" | "pack_err_structural" => {} - _ => return None, - } - - let tokens = &m.mac.tokens; - - // `pack!(T)` or `pack!(T = U)` — the first ident is the type name - // Parse simply with syn - if let Ok(ident) = syn::parse2::<syn::Ident>(tokens.clone()) { - // pack!(TypeName) — just a single ident - return Some(ident.to_string()); - } - - // Try to parse `Ident = Type` - // Clone tokens first to avoid partial consumption - let stream = tokens.clone(); - let mut iter = stream.into_iter(); - - // Skip leading attributes/doc comments - loop { - if let proc_macro2::TokenTree::Ident(ident) = iter.next()? { - // Found the first ident, this is the type name - let type_name = ident.to_string(); - - // Check if `=` follows - if let Some(proc_macro2::TokenTree::Punct(p)) = iter.next() - && p.as_char() == '=' - { - // pack!(TypeName = InnerType) - return Some(type_name); - } - - // pack_err!(TypeName) — only a single ident - return Some(type_name); - } - } -} diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs index fff2597..22e1cc2 100644 --- a/mingling_pathf/test/src/lib.rs +++ b/mingling_pathf/test/src/lib.rs @@ -174,33 +174,6 @@ fn test_completion_analyze() { } #[test] -fn test_pack_analyze() { - let analyzer = mingling_pathf::pattern_analyzer::init(); - let file = current_dir().unwrap().join("src/test_files/test_pack.rs"); - - let r = analyzer.analyze_file(file).unwrap(); - let required: Vec<&str> = vec![ - "::ResultPack1", - "::ErrorPack1", - "::ErrorPack2", - "::ResultPack2", - "::ErrorPack3", - "::ErrorPack4", - "::sub::ResultPack1", - "::sub::ErrorPack1", - "::sub::ErrorPack2", - "::sub::ResultPack2", - "::sub::ErrorPack3", - "::sub::ErrorPack4", - ]; - - assert_eq!(r.len(), required.len()); - for entry in &required { - assert!(r.contains(*entry), "Result should contain: {}", entry); - } -} - -#[test] fn test_group_analyze() { let analyzer = mingling_pathf::pattern_analyzer::init(); let file = current_dir().unwrap().join("src/test_files/test_group.rs"); diff --git a/mingling_pathf/test/src/test_files/test_pack.rs b/mingling_pathf/test/src/test_files/test_pack.rs deleted file mode 100644 index 759e35f..0000000 --- a/mingling_pathf/test/src/test_files/test_pack.rs +++ /dev/null @@ -1,17 +0,0 @@ -mingling::macros::pack!(ResultPack1 = String); -mingling::macros::pack_err!(ErrorPack1); -mingling::macros::pack_err!(ErrorPack2 = PathBuf); - -pack!(ResultPack2 = (u8, String)); -pack_err!(ErrorPack3); -pack_err!(ErrorPack4 = PathBuf); - -pub mod sub { - mingling::macros::pack!(ResultPack1 = String); - mingling::macros::pack_err!(ErrorPack1); - mingling::macros::pack_err!(ErrorPack2 = PathBuf); - - pack!(ResultPack2 = (u8, String)); - pack_err!(ErrorPack3); - pack_err!(ErrorPack4 = PathBuf); -} |
