diff options
82 files changed, 1934 insertions, 934 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c70d3d..54cffcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,26 @@ None #### Optimizations: -None +1. **[`macros`]** Updated `route!` macro to use `Routable` trait instead of `Grouped` trait for error conversion, making the semantics clearer. The `route!` macro now calls `::mingling::Routable::to_chain(e)` on the error branch instead of `::mingling::Grouped::to_chain(e)`. + + Additionally, `ChainProcess<ThisProgram>` now implements `Routable<ThisProgram>`, allowing `route!` to work with `Result<Ok, ChainProcess<ThisProgram>>` patterns — where the error side is already a `ChainProcess` value that should be routed directly: + + When `ChainProcess` implements `Routable`, `to_chain()` re-routes the inner `AnyOutput` to the chain pipeline (preserving the existing `NextProcess::Chain`/`Renderer` flag), while `to_render()` re-routes it to the render pipeline. This enables seamless propagation of already-routed chain process values through the `route!` macro without double-wrapping. + + The `Routable` trait is defined in `mingling_core::asset::routable` and provides unified routing capabilities (`to_chain` / `to_render`) for any type that can be dispatched into the program's pipeline. A blanket implementation is provided for all `T: Grouped<C> + Send`, ensuring backward compatibility — existing types that implement `Grouped` automatically implement `Routable`. + +2. **[`macros`]** Restructured the `mingling_macros` crate's internal module hierarchy. The previously flat module structure has been reorganized into a logical directory-based layout: + + - **`attr/`** — Attribute macro implementations (e.g., `#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`, `#[dispatcher_clap]`, `#[program_setup]`) + - **`derive/`** — Derive macro implementations (e.g., `#[derive(Grouped)]`, `#[derive(EnumTag)]`) + - **`func/`** — Function-like macro implementations (e.g., `pack!`, `group!`, `dispatcher!`, `suggest!`, `entry!`, `node!`, `gen_program!` and its sub-macros) + - **`systems/`** — Cross-cutting systems (e.g., resource injection, dispatch tree generation, structural data derive support) + - **`extensions/`** — Extension point mechanism for attribute macros (unchanged) + - **`utils.rs`** — Shared utility module for future common helpers + + All public API items (`#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`) remain at the crate root (`lib.rs`) with identical signatures. Internal function visibility has been tightened from `pub fn` to `pub(crate) fn` for all module-internal functions that were previously publicly accessible only within the crate. + + _No migration is required for downstream code — all macros are re-exported with the same names and signatures as before._ #### Features: @@ -206,11 +225,43 @@ None The generated `proc` function now wraps the body result in `<UserReturnType as Into<ChainProcess<ThisProgram>>>::into(...)`, which: - Works for `Next` / `ChainProcess` via the identity `From<T> for T` implementation. - - Works for any pack type (`ResultGreeting`, etc.) via the `.into()` conversion generated by `pack!` / `#[derive(Groupped)]`. + - Works for any pack type (`ResultGreeting`, etc.) via the `.into()` conversion generated by `pack!` / `#[derive(Grouped)]`. - Works for `()` via the `From<()>` implementation on `ChainProcess`. The return type validation has been removed entirely — any valid Rust return type is accepted. If the type does not implement `Into<ChainProcess<ThisProgram>>`, a standard Rust compilation error will be produced at the call site. +10. **[`macros`]** **[`extensions`]** Added the `extensions` module to `mingling_macros`, providing an extension point mechanism for attribute macros (`#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`). The extension system allows identifiers in the attribute argument to be extracted and processed before the main macro logic runs. + + Each attribute macro now attempts to redispatch through `extensions::try_redispatch_simple()` (or `try_redispatch_completion` for `#[completion]`) before executing its standard logic. If extension identifiers are detected, the call is re-routed so that extensions are applied via additional `#[...]` attributes stacked on top of the inner core attribute. New extensions can be added without modifying the attribute macros themselves — only the `extensions` module needs to be updated to register new identifiers. + + This system is designed for future extensibility: as new cross-cutting concerns (e.g., logging, metrics, validation) are identified, they can be added as simple extension identifiers without touching the core macro logic. + +11. **[`extensions`]** **[`macros`]** Added the `#[routeify]` extension attribute macro that transforms `expr?` into `route!(expr)`, enabling concise error routing in chain functions using the `?` operator syntax. + + The `#[routeify]` macro can be used: + - **Standalone** — as a direct attribute: `#[routeify] fn handle(...) { ... }` + - **As an extension** — via the extension point system: `#[chain(routeify)] fn handle(...) { ... }` + + When used as a `#[chain]` extension, the `routeify` identifier is detected by the extension point mechanism, stripped from the `#[chain]` attribute arguments, and `#[routeify]` is applied as an outer attribute on top of `#[chain]`. The re-dispatch token stream now correctly generates `#[#exts]*` (i.e., `#[routeify] #[chain]`) instead of the previous bare `#exts` — fixing a bug where extension identifiers were emitted without the `#[...]` attribute delimiter, producing invalid token streams. + + ```rust + use mingling::macros::routeify; + + #[chain(routeify)] + fn handle_calc(args: EntryCalculate) -> Next { + let a = args.pick(&arg![f32]).to_result()?; + let op = args.pick(&arg![Operator]).to_result()?; + StateCalculate { number_a: a, operator: op, ... }.to_chain() + } + ``` + + The `#[routeify]` macro is feature-gated behind `extra_macros` and re-exported as `mingling::macros::routeify`. + + Internal changes: + - Added `mingling_macros/src/extensions/routeify.rs` with `routeify_impl` implementation. + - Updated `try_redispatch_simple` and `try_redispatch_completion` to emit `#[#exts]` instead of bare `#exts`, ensuring proper attribute syntax in the re-dispatched token stream. + - Registered `#[proc_macro_attribute] pub fn routeify` in `mingling_macros/src/lib.rs`. + #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros. @@ -263,6 +314,17 @@ None 3. **[`core`]** **[`ExitCodeSetup`]** Updated `ExitCodeSetup` to only override the exit code when `ResExitCode` has been modified (i.e., `exit_code != 0`). Previously, it unconditionally overrode the exit code, which could interfere with exit codes set by other hooks or the program's default exit flow. The `on_finish` hook now returns `ProgramControlUnit::OverrideExitCode(...)` only when the exit code is non-zero, and `ProgramControls::Empty` otherwise. The import of `ProgramControls` has been added accordingly. +4. **[`core`]** **[`macros`]** Renamed `Groupped` (typo) to `Grouped`. All references to the trait, derive macro, module files, and related types have been corrected throughout the codebase: + + - Trait: `Groupped<Group>` → `Grouped<Group>` + - Derive macro: `#[derive(Groupped)]` → `#[derive(Grouped)]` + - Serialize variant: `GrouppedSerialize` → `GroupedSerialize` + - Source files: `groupped.rs` → `grouped.rs` + - Pattern matcher: `GrouppedDerivePattern` → `GroupedDerivePattern` + - All `use` imports, type annotations, and trait bound references updated accordingly. + + This is a pure rename — no behavioral changes. All functionality remains identical. Downstream code using the old `Groupped` name must migrate to `Grouped`. + --- ## Release 0.2.2 (2026-07-10) diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index dbea3d2..e08d17b 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -171,12 +171,12 @@ For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants ```rust // Features: ["parser", "extra_macros"] -use mingling::{EnumTag, Groupped}; +use mingling::{EnumTag, Grouped}; use mingling::parser::PickableEnum; dispatcher!("lang.select", CMDLang => EntryLang); -#[derive(Debug, Default, EnumTag, Groupped)] +#[derive(Debug, Default, EnumTag, Grouped)] pub enum Language { #[default] Rust, @@ -473,7 +473,7 @@ use mingling::macros::dispatcher_clap; use mingling::prelude::*; use std::io::Write; -#[derive(Default, clap::Parser, Groupped)] +#[derive(Default, clap::Parser, Grouped)] #[dispatcher_clap( "greet", CMDGreet, help = true, // auto-generate #[help] from clap @@ -623,14 +623,14 @@ With the `structural_renderer` feature, users can add `--json` or `--yaml` flags // serde = "1" use mingling::{prelude::*, setup::StructuralRendererSetup}; -use mingling::Groupped; +use mingling::Grouped; use mingling::StructuralData; use serde::Serialize; use std::io::Write; dispatcher!("render", CMDRender => EntryRender); -#[derive(Default, StructuralData, Serialize, Groupped)] +#[derive(Default, StructuralData, Serialize, Grouped)] struct ResultInfo { name: String, age: i32, @@ -38,8 +38,8 @@ Additionally, the project is currently developed by me alone ([Weicao-CatilGrass Mingling abstracts the behavior of a program's lifecycle into three phases: **Dispatch**, **Execution**, and **Rendering**. Each phase is connected by types — the output of the current phase becomes the input of the next phase. For example: ```rust -dispatcher!("current", CMDCurrent => EntryGreet); -pack!(StateNext => ()); +dispatcher!("current", CMDCurrent => EntryCurrent); +pack!(StateNext = ()); #[chain] fn handle_current(_: EntryCurrent) -> StateNext { @@ -128,15 +128,15 @@ features = [] - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`repl`] Provides REPL capability (`program.exec_repl();`) - [x] [[0.2.0](https://docs.rs/mingling/0.2.0/mingling/)] Complete documentation, tests, and examples - [ ] Milestone.2 "More Comfortable Dev and User Experience" - - [ ] [`mling` / `mingling-cli`] - - [ ] **Mingling** Linter - - [ ] **Mingling** Project Generator - - [ ] **Mingling** Program Installer & Manager (For development) - - [ ] Helpdoc Editor - - [ ] [`picker`] A more efficient and intelligent argument parser - - [x] [`macros`] Remove `r_print!` / `r_println!` macros + - [ ] [`mling` / `mingling-cli`] + - [ ] **Mingling** Linter + - [ ] **Mingling** Project Generator + - [ ] **Mingling** Program Installer & Manager (For development) + - [ ] Helpdoc Editor + - [ ] [`picker`] A more efficient and intelligent argument parser + - [x] [`macros`] Remove `r_print!` / `r_println!` macros - [ ] Milestone.3 "Unplanned" - - [ ] ... + - [ ] ... ## Unplanned Features diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md index b2850c5..f845d11 100644 --- a/docs/_zh_CN/pages/3-define-a-chain.md +++ b/docs/_zh_CN/pages/3-define-a-chain.md @@ -48,7 +48,7 @@ Chain 函数签名里写着它需要什么——`args: EntryGreet` ```rust // pack!(ResultName = String) 大概生成了这样的代码 -#[derive(Groupped)] +#[derive(Grouped)] pub struct ResultName { pub inner: String, } diff --git a/docs/_zh_CN/pages/7-argument-parse-clap.md b/docs/_zh_CN/pages/7-argument-parse-clap.md index 7dce301..21f886c 100644 --- a/docs/_zh_CN/pages/7-argument-parse-clap.md +++ b/docs/_zh_CN/pages/7-argument-parse-clap.md @@ -25,7 +25,7 @@ features = ["derive", "color"] // Dependencies: // clap = "4" @@@ use mingling::macros::dispatcher_clap; -#[derive(Default, clap::Parser, Groupped)] +#[derive(Default, clap::Parser, Grouped)] #[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] pub struct EntryGreet { #[clap(default_value = "World")] @@ -64,7 +64,7 @@ fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { // clap = "4" @@@use mingling::setup::BasicProgramSetup; @@@use mingling::macros::dispatcher_clap; -@@@#[derive(Default, clap::Parser, Groupped)] +@@@#[derive(Default, clap::Parser, Grouped)] @@@#[dispatcher_clap("greet", CMDGreet)] @@@pub struct EntryGreet { @@@ name: String, diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md index 6b6b0f9..70bed79 100644 --- a/docs/_zh_CN/pages/advanced/2-structural-renderer.md +++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md @@ -62,7 +62,7 @@ fn render_info(r: ResultInfo) -> RenderResult { ## 自定义输出结构 -`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Groupped)]` 手动定义类型: +`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Grouped)]` 手动定义类型: ```rust // Features: ["structural_renderer"] @@ -74,7 +74,7 @@ fn render_info(r: ResultInfo) -> RenderResult { @@@use serde::Serialize; @@@dispatcher!("render", CMDRender => EntryRender); -#[derive(Serialize, StructuralData, Groupped)] +#[derive(Serialize, StructuralData, Grouped)] struct Info { name: String, age: i32, diff --git a/docs/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md index 9b820da..118e856 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(Groupped)]` 标记的类型都被分配到这个枚举的一个变体。 +每个被 `pack!` 或 `#[derive(Grouped)]` 标记的类型都被分配到这个枚举的一个变体。 ## ChainProcess:数据 + 路由 @@ -38,17 +38,17 @@ ChainProcess<G> 调度器根据 `NextProcess` 决定是继续循环还是退出渲染。 -## Groupped:谁是谁 +## Grouped:谁是谁 -调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Groupped` trait: +调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Grouped` trait: ``` -trait Groupped<G> { +trait Grouped<G> { fn member_id() -> G; } ``` -当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Groupped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。 +当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Grouped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。 `to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。 diff --git a/docs/_zh_CN/pages/other/features.md b/docs/_zh_CN/pages/other/features.md index d92e597..8bd386c 100644 --- a/docs/_zh_CN/pages/other/features.md +++ b/docs/_zh_CN/pages/other/features.md @@ -171,7 +171,7 @@ fn handle_hello(args: EntryHello) {} ### `group!` 将外部类型注册为程序组成员,无需修改原始类型的定义。 -类型名会直接作为枚举变体,与 `pack!` 或 `#[derive(Groupped)]` 一致。 +类型名会直接作为枚举变体,与 `pack!` 或 `#[derive(Grouped)]` 一致。 ```rust // Features: ["extra_macros"] @@ -274,12 +274,24 @@ analyze_and_build_type_mapping().unwrap(); **介绍:** -启用解析器模块,提供文本解析和语法分析功能。 +启用参数解析器模块,提供参数解析功能。 -开启后可以使用 `Picker` 进行零成本的参数提取,支持 `pick()` 和 `pick_or()` 等方法。 +开启后可以使用 `Picker` 进行简易的参数提取,支持 `pick()` 和 `pick_or()` 等方法。 详见 [示例](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-parse) +## 特性 `picker` + +**介绍:** + +引入依赖 `arg-picker`,为 Mingling 提供更高级的参数解析能力。 + +它可以与 `parser`、`clap` 特性共存,但建议不要和 `parser` 特性同时启用,因为两者的 API 极为相似。 + +`picker` 是独立于 Mingling 的参数解析器,不依赖 `mingling_core` 的内置参数提取 API。 + +详见 [示例](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-picker) + ## 特性 `repl` **介绍:** diff --git a/docs/_zh_CN/pages/other/naming_rule.md b/docs/_zh_CN/pages/other/naming_rule.md index 5f2ac34..c854ba1 100644 --- a/docs/_zh_CN/pages/other/naming_rule.md +++ b/docs/_zh_CN/pages/other/naming_rule.md @@ -96,7 +96,7 @@ Result + 内容 | `ResultGreetSomeone` | 问候结果 | | `ResultFruitList` | 水果列表结果 | -结果结构体期望被 Renderer 消费,内部结构应该为了渲染美观而设计。一般用 `#[derive(Groupped)]` 代替 `pack!()` 包装,以获得更灵活的字段控制。 +结果结构体期望被 Renderer 消费,内部结构应该为了渲染美观而设计。一般用 `#[derive(Grouped)]` 代替 `pack!()` 包装,以获得更灵活的字段控制。 ### 错误 diff --git a/docs/dev/pages/issues/add-picker2.md b/docs/dev/pages/issues/add-picker2.md index f5c3ca7..a2b5a10 100644 --- a/docs/dev/pages/issues/add-picker2.md +++ b/docs/dev/pages/issues/add-picker2.md @@ -40,7 +40,7 @@ fn handle_hello(args: EntryHello) { ```rust #[chain] fn handle_hello(args: EntryHello) -> Next { - // requires impl Groupped<_> + // requires impl Grouped<_> // | let parsed = args // vvvvvvvvvvvvvvvvvvv .pick_route::<String, _>(positional!(), ErrorNoNameProvided) @@ -90,11 +90,11 @@ pub struct PickerResult<Tuple> { ## 🕘 Progress - [x] In Progress - - [x] Added `Picker` struct and related call chain - - [x] Added `parselib` providing parsing logic - - [x] Added `Pickable` for extensibility - - [ ] Comprehensive testing! - - [ ] Improve documentation - - [ ] Add examples - - [ ] Update README + - [x] Added `Picker` struct and related call chain + - [x] Added `parselib` providing parsing logic + - [x] Added `Pickable` for extensibility + - [x] Comprehensive testing! + - [ ] Improve documentation + - [x] Add examples + - [ ] Update README - [ ] Complete diff --git a/docs/dev/pages/issues/remove-r-print-macro.md b/docs/dev/pages/issues/remove-r-print-macro.md index d6f6bff..43d2740 100644 --- a/docs/dev/pages/issues/remove-r-print-macro.md +++ b/docs/dev/pages/issues/remove-r-print-macro.md @@ -89,9 +89,9 @@ As for rendering in logic functions like `#[chain]`, that should be handled by a ## 🕘 Progress - [x] In Progress - - [x] Remove `r_println!` and `r_print!` macros - - [x] Modify `#[renderer]` and `#[help]` macros, remove implicit injection - - [x] Provide **no-return-value mode** and **RenderResult return value mode** for `#[renderer]` and `#[help]` macros - - [ ] Add new simplified syntax - - [x] Update documentation and test cases, ensure **all pass** -- [ ] Complete + - [x] Remove `r_println!` and `r_print!` macros + - [x] Modify `#[renderer]` and `#[help]` macros, remove implicit injection + - [x] Provide **no-return-value mode** and **RenderResult return value mode** for `#[renderer]` and `#[help]` macros + - [x] Add new simplified syntax + - [x] Update documentation and test cases, ensure **all pass** +- [x] Complete diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json index a539011..2a07365 100644 --- a/docs/example-pages/examples.json +++ b/docs/example-pages/examples.json @@ -31,6 +31,21 @@ ] }, { + "id": "example-argument-picker", + "name": "Argument Picker", + "icon": "📋", + "category": "parsing", + "desc": "Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line.\n", + "tags": [ + "arg-picker", + "SinglePickable" + ], + "files": [ + "src/main.rs", + "Cargo.toml" + ] + }, + { "id": "example-async-support", "name": "Async Support", "icon": "⚡", diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md index b2822bc..450522b 100644 --- a/docs/pages/3-define-a-chain.md +++ b/docs/pages/3-define-a-chain.md @@ -48,7 +48,7 @@ You've probably guessed it — `pack!(ResultName = String)` defines a type that ```rust // pack!(ResultName = String) generates code roughly like this -#[derive(Groupped)] +#[derive(Grouped)] pub struct ResultName { pub inner: String, } diff --git a/docs/pages/7-argument-parse-clap.md b/docs/pages/7-argument-parse-clap.md index b11e00e..86fce35 100644 --- a/docs/pages/7-argument-parse-clap.md +++ b/docs/pages/7-argument-parse-clap.md @@ -25,7 +25,7 @@ Add `#[dispatcher_clap]` on a `clap::Parser` struct to auto-generate a Dispatche // Dependencies: // clap = "4" @@@ use mingling::macros::dispatcher_clap; -#[derive(Default, clap::Parser, Groupped)] +#[derive(Default, clap::Parser, Grouped)] #[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] pub struct EntryGreet { #[clap(default_value = "World")] @@ -64,7 +64,7 @@ If you need `--help` support, register `BasicProgramSetup` in main and set the c // clap = "4" @@@use mingling::setup::BasicProgramSetup; @@@use mingling::macros::dispatcher_clap; -@@@#[derive(Default, clap::Parser, Groupped)] +@@@#[derive(Default, clap::Parser, Grouped)] @@@#[dispatcher_clap("greet", CMDGreet)] @@@pub struct EntryGreet { @@@ name: String, diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md index fab7530..910b197 100644 --- a/docs/pages/advanced/2-structural-renderer.md +++ b/docs/pages/advanced/2-structural-renderer.md @@ -62,7 +62,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, Groupped)]`: +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)]`: ```rust // Features: ["structural_renderer"] @@ -74,7 +74,7 @@ The default output from `pack_structural!` includes an `inner` field. For full c @@@use serde::Serialize; @@@dispatcher!("render", CMDRender => EntryRender); -#[derive(Serialize, StructuralData, Groupped)] +#[derive(Serialize, StructuralData, Grouped)] struct Info { name: String, age: i32, diff --git a/docs/pages/concepts/3-any-output.md b/docs/pages/concepts/3-any-output.md index f780377..f02805f 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(Groupped)]` is assigned to one variant of this enum. +Each type annotated with `pack!` or `#[derive(Grouped)]` is assigned to one variant of this enum. ## ChainProcess: Data + Routing @@ -38,17 +38,17 @@ This is why a Chain function returns `ChainProcess` instead of raw data—it bun The dispatcher reads `NextProcess` to decide whether to continue the loop or exit to rendering. -## Groupped: Who Is Who +## Grouped: Who Is Who -How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Groupped` trait: +How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Grouped` trait: ``` -trait Groupped<G> { +trait Grouped<G> { fn member_id() -> G; } ``` -When you use `pack!(ResultName = String)`, the macro automatically implements `Groupped` for `ResultName`, and `member_id()` returns the corresponding enum variant. The dispatcher looks at `member_id` and finds the matching Chain or Renderer. +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. `to_chain()` and `to_render()` are essentially convenience methods on `AnyOutput` that construct `ChainProcess::Ok(any, Chain)` and `ChainProcess::Ok(any, Renderer)` respectively. diff --git a/docs/pages/other/features.md b/docs/pages/other/features.md index 325bb75..7994d4c 100644 --- a/docs/pages/other/features.md +++ b/docs/pages/other/features.md @@ -171,7 +171,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(Groupped)]`. +The type's simple name is used as the enum variant, just like `pack!` or `#[derive(Grouped)]`. ```rust // Features: ["extra_macros"] @@ -274,12 +274,24 @@ See [example](https://mingling-rs.github.io/mingling/docs/example-viewer.html?na **Description:** -Enables the parser module, providing text parsing and analysis capabilities. +Enables the argument parser module, providing argument parsing functionality. -When enabled, you can use `Picker` for zero-cost argument extraction, supporting methods like `pick()` and `pick_or()`. +When enabled, you can use `Picker` for simple argument extraction, supporting methods like `pick()` and `pick_or()`. See [example](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-parse) +## Feature `picker` + +**Description:** + +Introduces the `arg-picker` dependency, providing more advanced argument parsing capabilities for Mingling. + +It can coexist with the `parser` and `clap` features, but it is recommended not to enable it alongside the `parser` feature, as their APIs are very similar. + +`picker` is an argument parser independent of Mingling and does not rely on the built-in argument extraction API of `mingling_core`. + +See [example](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-picker) + ## Feature `repl` **Description:** diff --git a/docs/pages/other/naming_rule.md b/docs/pages/other/naming_rule.md index 2ede61f..089a711 100644 --- a/docs/pages/other/naming_rule.md +++ b/docs/pages/other/naming_rule.md @@ -96,7 +96,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(Groupped)]` 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 use `#[derive(Grouped)]` instead of `pack!()` wrapping for more flexible field control. ### Error diff --git a/examples/example-argument-picker/Cargo.lock b/examples/example-argument-picker/Cargo.lock new file mode 100644 index 0000000..9a9baa4 --- /dev/null +++ b/examples/example-argument-picker/Cargo.lock @@ -0,0 +1,94 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arg-picker" +version = "0.1.0" +dependencies = [ + "arg-picker-macros", + "just_fmt", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "example-argument-picker" +version = "0.1.0" +dependencies = [ + "mingling", +] + +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "mingling" +version = "0.3.0" +dependencies = [ + "arg-picker", + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling_core" +version = "0.3.0" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.3.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/examples/example-argument-picker/Cargo.toml b/examples/example-argument-picker/Cargo.toml new file mode 100644 index 0000000..686b95b --- /dev/null +++ b/examples/example-argument-picker/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-argument-picker" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" + +# Enable `picker` features +features = ["picker", "extra_macros"] + +[workspace] diff --git a/examples/example-argument-picker/page.toml b/examples/example-argument-picker/page.toml new file mode 100644 index 0000000..563bccc --- /dev/null +++ b/examples/example-argument-picker/page.toml @@ -0,0 +1,10 @@ +[example] +id = "example-argument-picker" +name = "Argument Picker" +icon = "📋" +category = "parsing" +desc = """ +Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line. +""" +tags = ["arg-picker", "SinglePickable"] +files = ["src/main.rs", "Cargo.toml"] diff --git a/examples/example-argument-picker/src/main.rs b/examples/example-argument-picker/src/main.rs new file mode 100644 index 0000000..7fcc5db --- /dev/null +++ b/examples/example-argument-picker/src/main.rs @@ -0,0 +1,225 @@ +//! Example Argument Picker +//! +//! > Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line. +//! +//! Run: +//! ```bash +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + 1 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 7 * 7 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 --round +//! ``` +//! +//! Output: +//! ```plaintext +//! Result: 2 +//! Result: 49 +//! Error: First number (number_a) was not provided. +//! Error: Operator was not provided. +//! Error: Second number (number_b) was not provided. +//! Result: 1.3333334 +//! Result: 1 +//! ``` + +use mingling::{ + consts::REMAINS, + macros::route, + picker::{ + IntoPicker, PickerArgResult, SinglePickable, + parselib::{ParserStyle, UNIX_STYLE}, + value::Flag, + }, + prelude::*, +}; + +// --------- IMPORTANT --------- +// Use picker::BasicProgramSetup instead of the original BasicProgramSetup +// It uses arg-picker to rewrite the logic of the original BasicProgramSetup +use mingling::setup::picker::BasicProgramSetup; + +// --------- IMPORTANT --------- + +dispatcher!("calc", CMDCalculate => EntryCalculate); + +pack_err!(ErrorNumberANotProvided); +pack_err!(ErrorNumberBNotProvided); +pack_err!(ErrorNumberOperatorNotProvided); +pack_err!(ErrorDivisionByZero); + +pack!(StateAdd = (f32, f32)); +pack!(StateSubtract = (f32, f32)); +pack!(StateMultiply = (f32, f32)); +pack!(StateDivide = (f32, f32)); + +pack!(ResultNumber = f32); + +#[derive(Grouped)] +struct StateCalculate { + number_a: f32, + operator: Operator, + number_b: f32, +} + +#[derive(Debug, PartialEq, Eq)] +enum Operator { + Plus, + Dash, + Slash, + Star, +} + +// --------- IMPORTANT --------- +// Define SinglePickable for type Operator +// This allows the type to be picked as an argument +impl SinglePickable for Operator { + fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { + let Some(str) = str else { + return PickerArgResult::NotFound; + }; + let op = match str.chars().next() { + Some('+') => Operator::Plus, + Some('-') => Operator::Dash, + Some('*') => Operator::Star, + Some('/') => Operator::Slash, + _ => return PickerArgResult::NotFound, + }; + PickerArgResult::Parsed(op) + } +} +// --------- IMPORTANT --------- + +#[derive(Default, Clone)] +struct ResNumberDisplaySetting { + round: bool, +} + +fn main() { + let mut program = ThisProgram::new(); + + // Use ParserStyle to manage the arg-picker theme + ParserStyle::set_global_style(&UNIX_STYLE); + + // Enable picker::BasicProgramSetup + program.with_setup(BasicProgramSetup); + + // --------- IMPORTANT --------- + // Pre-process global arguments before executing commands + let (round, args) = program + .take_args() + // Use arg![round: Flag] to indicate the `--round` | `-R` flag + // | + // vvvvvvvvvvvvvvvv + .pick(&arg![round: Flag, 'R']) + // Use REMAINS to extract remaining arguments + // | + // vvvvvvvv + .pick(&REMAINS) + // Since Flag and REMAINS will not fail to parse, + // we can safely unwrap here + .unwrap(); + program.replace_args(args.into()); + + program.with_resource(ResNumberDisplaySetting { round: *round }); + // --------- IMPORTANT --------- + + program.with_dispatcher(CMDCalculate); + program.exec_and_exit(); +} + +#[chain] +fn handle_calc(args: EntryCalculate) -> Next { + // --------- IMPORTANT --------- + let (number_a, operator, number_b) = route!( + // Use the arg! macro to define a positional argument of type f32 + // | + // vvvvvvvvvv + args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain()) + .pick_or_route(&arg![Operator], || { + ErrorNumberOperatorNotProvided::default().to_chain() + }) // Returns a routable type when not found or fails to parse + // | + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain()) + // Use `to_result` to parse arguments + // and convert to Result<(Tuple, ...), Route> type + .to_result() + ); + // --------- IMPORTANT --------- + + if operator == Operator::Slash && number_b == 0. { + return ErrorDivisionByZero::default().to_chain(); + } + + StateCalculate { + number_a, + operator, + number_b, + } + .to_chain() +} + +#[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(), + } +} + +#[chain] +fn handle_state_add(state_add: StateAdd) -> ResultNumber { + let (a, b) = state_add.inner; + ResultNumber::new(a + b) +} + +#[chain] +fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber { + let (a, b) = state_subtract.inner; + ResultNumber::new(a - b) +} + +#[chain] +fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber { + let (a, b) = state_multiply.inner; + ResultNumber::new(a * b) +} + +#[chain] +fn handle_state_divide(state_divide: StateDivide) -> ResultNumber { + let (a, b) = state_divide.inner; + ResultNumber::new(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 }; + format!("Result: {}", result) +} + +#[renderer] +fn render_error_division_by_zero(_: ErrorDivisionByZero) -> String { + "Error: Division by zero is not allowed!".to_string() +} + +#[renderer] +fn render_error_number_a_not_provided(_: ErrorNumberANotProvided) -> String { + "Error: First number (number_a) was not provided.".to_string() +} + +#[renderer] +fn render_error_number_b_not_provided(_: ErrorNumberBNotProvided) -> String { + "Error: Second number (number_b) was not provided.".to_string() +} + +#[renderer] +fn render_error_number_operator_not_provided(_: ErrorNumberOperatorNotProvided) -> String { + "Error: Operator was not provided.".to_string() +} + +gen_program!(); diff --git a/examples/example-clap-binding/src/main.rs b/examples/example-clap-binding/src/main.rs index d99f8d1..0f839c8 100644 --- a/examples/example-clap-binding/src/main.rs +++ b/examples/example-clap-binding/src/main.rs @@ -38,7 +38,7 @@ //! For more information, try '--help'. //! ``` -use mingling::{Groupped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup}; +use mingling::{Grouped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup}; use std::io::Write; fn main() { @@ -67,10 +67,10 @@ fn main() { // Implement Clap Parser, and bind to Dispatcher // _______________________________ Default trait, provides fallback on parse failure // / ______________________ clap::Parser, parsing logic implemented by Clap -// | / ________ Implement mingling::Groupped +// | / ________ Implement mingling::Grouped // | | / to ensure Mingling can recognize the type -// vvvvvvv vvvvvvvvvvvv vvvvvvvv -#[derive(Default, clap::Parser, Groupped)] +// vvvvvvv vvvvvvvvvvvv vvvvvvv +#[derive(Default, clap::Parser, Grouped)] #[dispatcher_clap( "greet", CMDGreet, // Bind EntryGreet to "greet" command help = true, // Generate clap help for EntryGreet diff --git a/examples/example-custom-pickable/src/main.rs b/examples/example-custom-pickable/src/main.rs index d203815..b163278 100644 --- a/examples/example-custom-pickable/src/main.rs +++ b/examples/example-custom-pickable/src/main.rs @@ -14,15 +14,15 @@ //! Failed to parse address //! ``` -use mingling::{macros::route, parser::Pickable, prelude::*, Groupped}; +use mingling::{macros::route, parser::Pickable, prelude::*, Grouped}; use std::io::Write; // Define types that can be recognized by Mingling // ________________________ `Pickable` trait needs to implement Default -// / ________ The Groupped derive macro registers an ID for this type +// / ________ The Grouped derive macro registers an ID for this type // | / Mingling uses this ID to identify the type -// vvvvvvv vvvvvvvv -#[derive(Debug, Default, Clone, Groupped)] +// vvvvvvv vvvvvvv +#[derive(Debug, Default, Clone, Grouped)] pub struct Address { pub ip: [u8; 4], pub port: u16, diff --git a/examples/example-enum-tag/src/main.rs b/examples/example-enum-tag/src/main.rs index 91c5358..b57511d 100644 --- a/examples/example-enum-tag/src/main.rs +++ b/examples/example-enum-tag/src/main.rs @@ -16,7 +16,7 @@ //! ``` use mingling::{ - macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Groupped, ShellContext, + macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Grouped, ShellContext, Suggest, }; use std::io::Write; @@ -25,7 +25,7 @@ use std::io::Write; // ________ adds metadata to the enum, enabling it to: // / 1. Be used by the `suggest_enum!(Enum)` macro under the `comp` feature for autocompletion // vvvvvvv 2. Implement the `PickableEnum` trait -#[derive(Debug, Default, EnumTag, Groupped)] +#[derive(Debug, Default, EnumTag, Grouped)] pub enum ProgrammingLanguages { #[enum_desc("An efficient and flexible compiled language widely used for system programming")] C, diff --git a/examples/example-outside-type/src/main.rs b/examples/example-outside-type/src/main.rs index 925878a..3159d19 100644 --- a/examples/example-outside-type/src/main.rs +++ b/examples/example-outside-type/src/main.rs @@ -71,7 +71,7 @@ fn render_number(num: ParsedNumber) -> RenderResult { /// Renderer for parse errors — using the outside `ParseIntError` type. /// /// The `ParseIntError` type is registered via `group!` above, so it implements -/// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function. +/// `Grouped<ThisProgram>` and can be used directly in a `#[renderer]` function. #[renderer] fn render_parse_error(err: ParseIntError) -> RenderResult { let mut render_result = RenderResult::new(); diff --git a/examples/example-structural-renderer/src/main.rs b/examples/example-structural-renderer/src/main.rs index 13c4c84..070e75d 100644 --- a/examples/example-structural-renderer/src/main.rs +++ b/examples/example-structural-renderer/src/main.rs @@ -18,7 +18,7 @@ //! ``` use mingling::prelude::*; -use mingling::{Groupped, StructuralData, parser::Picker, setup::StructuralRendererSetup}; +use mingling::{parser::Picker, setup::StructuralRendererSetup, Grouped, StructuralData}; use serde::Serialize; use std::io::Write; @@ -35,12 +35,12 @@ fn main() { // --------- IMPORTANT --------- // For beautiful output structure, do not use `pack!` to wrap the types that need to be output. // Instead, manually implement -// __________________________________ Mark as structured data so it can be rendered -// / ____________________ Implement serde::Serialize -// | / _________ Implement mingling::Groupped -// | | / to ensure Mingling can recognize the type -// vvvvvvvvvvvv vvvvvvvvv vvvvvvvv -#[derive(StructuralData, Serialize, Groupped)] +// ____________________________________ Mark as structured data so it can be rendered +// / ____________________ Implement serde::Serialize +// | / _________ Implement mingling::Grouped +// | | / to ensure Mingling can recognize the type +// vvvvvvvvvvvv vvvvvvvvv vvvvvvv +#[derive(StructuralData, Serialize, Grouped)] struct Info { #[serde(rename = "member_name")] name: String, diff --git a/examples/full-todolist/src/todolist.rs b/examples/full-todolist/src/todolist.rs index 71338c3..d3582e3 100644 --- a/examples/full-todolist/src/todolist.rs +++ b/examples/full-todolist/src/todolist.rs @@ -1,13 +1,13 @@ //! Data structures, read and write logic for the todo list -use mingling::{Groupped, RenderResult, macros::renderer}; +use mingling::{Grouped, RenderResult, macros::renderer}; use serde::{Deserialize, Serialize}; use std::io::Write; use std::{env::current_dir, path::PathBuf}; use crate::ResProgramFlags; -#[derive(Debug, Serialize, Deserialize, Clone, Default, Groupped)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, Grouped)] pub struct ResTodoList { pub items: Vec<Todo>, } diff --git a/examples/test-examples.toml b/examples/test-examples.toml index d2fe602..e450919 100644 --- a/examples/test-examples.toml +++ b/examples/test-examples.toml @@ -277,3 +277,38 @@ expect.result = "Hello, Alice!" command = "hello" expect.exit-code = 0 expect.result = "Hello, World!" + +[[test.example-argument-picker]] +command = "calc 1 + 1" +expect.exit-code = 0 +expect.result = "Result: 2" + +[[test.example-argument-picker]] +command = "calc 7 * 7" +expect.exit-code = 0 +expect.result = "Result: 49" + +[[test.example-argument-picker]] +command = "calc" +expect.exit-code = 0 +expect.result = "Error: First number (number_a) was not provided." + +[[test.example-argument-picker]] +command = "calc 1" +expect.exit-code = 0 +expect.result = "Error: Operator was not provided." + +[[test.example-argument-picker]] +command = "calc 1 +" +expect.exit-code = 0 +expect.result = "Error: Second number (number_b) was not provided." + +[[test.example-argument-picker]] +command = "calc 4 / 3" +expect.exit-code = 0 +expect.result = "Result: 1.3333334" + +[[test.example-argument-picker]] +command = "calc 4 / 3 --round" +expect.exit-code = 0 +expect.result = "Result: 1" diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index adf900f..2509029 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -50,7 +50,7 @@ dispatch_tree = ["mingling_core/dispatch_tree", "mingling_macros/dispatch_tree"] repl = ["mingling_core/repl", "mingling_macros/repl"] comp = ["mingling_core/comp", "mingling_macros/comp"] parser = ["dep:size"] -picker = ["dep:arg-picker", "arg-picker/mingling_support"] +picker = ["mingling_core/picker", "dep:arg-picker", "arg-picker/mingling_support"] pathf = ["mingling_core/pathf", "mingling_macros/pathf"] structural_renderer = [ diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index c4f59c9..bdefb5a 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -124,6 +124,251 @@ /// } /// ``` pub mod example_argument_parse {} +/// Example Argument Picker +/// +/// > Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line. +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + 1 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 7 * 7 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 --round +/// ``` +/// +/// Output: +/// ```plaintext +/// Result: 2 +/// Result: 49 +/// Error: First number (number_a) was not provided. +/// Error: Operator was not provided. +/// Error: Second number (number_b) was not provided. +/// Result: 1.3333334 +/// Result: 1 +/// ``` +/// +/// Source code (./Cargo.toml) +/// ```toml +/// [package] +/// name = "example-argument-picker" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// # Enable `picker` features +/// features = ["picker", "extra_macros"] +/// +/// [workspace] +/// ``` +/// +/// Source code (./src/main.rs) +/// ```ignore +/// use mingling::{ +/// consts::REMAINS, +/// macros::route, +/// picker::{ +/// IntoPicker, PickerArgResult, SinglePickable, +/// parselib::{ParserStyle, UNIX_STYLE}, +/// value::Flag, +/// }, +/// prelude::*, +/// }; +/// +/// // --------- IMPORTANT --------- +/// // Use picker::BasicProgramSetup instead of the original BasicProgramSetup +/// // It uses arg-picker to rewrite the logic of the original BasicProgramSetup +/// use mingling::setup::picker::BasicProgramSetup; +/// +/// // --------- IMPORTANT --------- +/// +/// dispatcher!("calc", CMDCalculate => EntryCalculate); +/// +/// pack_err!(ErrorNumberANotProvided); +/// pack_err!(ErrorNumberBNotProvided); +/// pack_err!(ErrorNumberOperatorNotProvided); +/// pack_err!(ErrorDivisionByZero); +/// +/// pack!(StateAdd = (f32, f32)); +/// pack!(StateSubtract = (f32, f32)); +/// pack!(StateMultiply = (f32, f32)); +/// pack!(StateDivide = (f32, f32)); +/// +/// pack!(ResultNumber = f32); +/// +/// #[derive(Grouped)] +/// struct StateCalculate { +/// number_a: f32, +/// operator: Operator, +/// number_b: f32, +/// } +/// +/// #[derive(Debug, PartialEq, Eq)] +/// enum Operator { +/// Plus, +/// Dash, +/// Slash, +/// Star, +/// } +/// +/// // --------- IMPORTANT --------- +/// // Define SinglePickable for type Operator +/// // This allows the type to be picked as an argument +/// impl SinglePickable for Operator { +/// fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { +/// let Some(str) = str else { +/// return PickerArgResult::NotFound; +/// }; +/// let op = match str.chars().next() { +/// Some('+') => Operator::Plus, +/// Some('-') => Operator::Dash, +/// Some('*') => Operator::Star, +/// Some('/') => Operator::Slash, +/// _ => return PickerArgResult::NotFound, +/// }; +/// PickerArgResult::Parsed(op) +/// } +/// } +/// // --------- IMPORTANT --------- +/// +/// #[derive(Default, Clone)] +/// struct ResNumberDisplaySetting { +/// round: bool, +/// } +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // Use ParserStyle to manage the arg-picker theme +/// ParserStyle::set_global_style(&UNIX_STYLE); +/// +/// // Enable picker::BasicProgramSetup +/// program.with_setup(BasicProgramSetup); +/// +/// // --------- IMPORTANT --------- +/// // Pre-process global arguments before executing commands +/// let (round, args) = program +/// .take_args() +/// // Use arg![round: Flag] to indicate the `--round` | `-R` flag +/// // | +/// // vvvvvvvvvvvvvvvv +/// .pick(&arg![round: Flag, 'R']) +/// // Use REMAINS to extract remaining arguments +/// // | +/// // vvvvvvvv +/// .pick(&REMAINS) +/// // Since Flag and REMAINS will not fail to parse, +/// // we can safely unwrap here +/// .unwrap(); +/// program.replace_args(args.into()); +/// +/// program.with_resource(ResNumberDisplaySetting { round: *round }); +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatcher(CMDCalculate); +/// program.exec_and_exit(); +/// } +/// +/// #[chain] +/// fn handle_calc(args: EntryCalculate) -> Next { +/// // --------- IMPORTANT --------- +/// let (number_a, operator, number_b) = route!( +/// // Use the arg! macro to define a positional argument of type f32 +/// // | +/// // vvvvvvvvvv +/// args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain()) +/// .pick_or_route(&arg![Operator], || { +/// ErrorNumberOperatorNotProvided::default().to_chain() +/// }) // Returns a routable type when not found or fails to parse +/// // | +/// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +/// .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain()) +/// // Use `to_result` to parse arguments +/// // and convert to Result<(Tuple, ...), Route> type +/// .to_result() +/// ); +/// // --------- IMPORTANT --------- +/// +/// if operator == Operator::Slash && number_b == 0. { +/// return ErrorDivisionByZero::default().to_chain(); +/// } +/// +/// StateCalculate { +/// number_a, +/// operator, +/// number_b, +/// } +/// .to_chain() +/// } +/// +/// #[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(), +/// } +/// } +/// +/// #[chain] +/// fn handle_state_add(state_add: StateAdd) -> ResultNumber { +/// let (a, b) = state_add.inner; +/// ResultNumber::new(a + b) +/// } +/// +/// #[chain] +/// fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber { +/// let (a, b) = state_subtract.inner; +/// ResultNumber::new(a - b) +/// } +/// +/// #[chain] +/// fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber { +/// let (a, b) = state_multiply.inner; +/// ResultNumber::new(a * b) +/// } +/// +/// #[chain] +/// fn handle_state_divide(state_divide: StateDivide) -> ResultNumber { +/// let (a, b) = state_divide.inner; +/// ResultNumber::new(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 }; +/// format!("Result: {}", result) +/// } +/// +/// #[renderer] +/// fn render_error_division_by_zero(_: ErrorDivisionByZero) -> String { +/// "Error: Division by zero is not allowed!".to_string() +/// } +/// +/// #[renderer] +/// fn render_error_number_a_not_provided(_: ErrorNumberANotProvided) -> String { +/// "Error: First number (number_a) was not provided.".to_string() +/// } +/// +/// #[renderer] +/// fn render_error_number_b_not_provided(_: ErrorNumberBNotProvided) -> String { +/// "Error: Second number (number_b) was not provided.".to_string() +/// } +/// +/// #[renderer] +/// fn render_error_number_operator_not_provided(_: ErrorNumberOperatorNotProvided) -> String { +/// "Error: Operator was not provided.".to_string() +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_argument_picker {} /// Example Async Runtime Support /// /// > This example shows how to drive an async runtime using the `async` feature @@ -378,7 +623,7 @@ pub mod example_basic {} /// /// Source code (./src/main.rs) /// ```ignore -/// use mingling::{Groupped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup}; +/// use mingling::{Grouped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup}; /// use std::io::Write; /// /// fn main() { @@ -407,10 +652,10 @@ pub mod example_basic {} /// // Implement Clap Parser, and bind to Dispatcher /// // _______________________________ Default trait, provides fallback on parse failure /// // / ______________________ clap::Parser, parsing logic implemented by Clap -/// // | / ________ Implement mingling::Groupped +/// // | / ________ Implement mingling::Grouped /// // | | / to ensure Mingling can recognize the type -/// // vvvvvvv vvvvvvvvvvvv vvvvvvvv -/// #[derive(Default, clap::Parser, Groupped)] +/// // vvvvvvv vvvvvvvvvvvv vvvvvvv +/// #[derive(Default, clap::Parser, Grouped)] /// #[dispatcher_clap( /// "greet", CMDGreet, // Bind EntryGreet to "greet" command /// help = true, // Generate clap help for EntryGreet @@ -724,15 +969,15 @@ pub mod example_completion {} /// /// Source code (./src/main.rs) /// ```ignore -/// use mingling::{macros::route, parser::Pickable, prelude::*, Groupped}; +/// use mingling::{macros::route, parser::Pickable, prelude::*, Grouped}; /// use std::io::Write; /// /// // Define types that can be recognized by Mingling /// // ________________________ `Pickable` trait needs to implement Default -/// // / ________ The Groupped derive macro registers an ID for this type +/// // / ________ The Grouped derive macro registers an ID for this type /// // | / Mingling uses this ID to identify the type -/// // vvvvvvv vvvvvvvv -/// #[derive(Debug, Default, Clone, Groupped)] +/// // vvvvvvv vvvvvvv +/// #[derive(Debug, Default, Clone, Grouped)] /// pub struct Address { /// pub ip: [u8; 4], /// pub port: u16, @@ -963,7 +1208,7 @@ pub mod example_dispatch_tree {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{ -/// macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Groupped, ShellContext, +/// macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Grouped, ShellContext, /// Suggest, /// }; /// use std::io::Write; @@ -972,7 +1217,7 @@ pub mod example_dispatch_tree {} /// // ________ adds metadata to the enum, enabling it to: /// // / 1. Be used by the `suggest_enum!(Enum)` macro under the `comp` feature for autocompletion /// // vvvvvvv 2. Implement the `PickableEnum` trait -/// #[derive(Debug, Default, EnumTag, Groupped)] +/// #[derive(Debug, Default, EnumTag, Grouped)] /// pub enum ProgrammingLanguages { /// #[enum_desc("An efficient and flexible compiled language widely used for system programming")] /// C, @@ -1691,7 +1936,7 @@ pub mod example_lazy_resources {} /// /// Renderer for parse errors — using the outside `ParseIntError` type. /// /// /// /// The `ParseIntError` type is registered via `group!` above, so it implements -/// /// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function. +/// /// `Grouped<ThisProgram>` and can be used directly in a `#[renderer]` function. /// #[renderer] /// fn render_parse_error(err: ParseIntError) -> RenderResult { /// let mut render_result = RenderResult::new(); @@ -2436,7 +2681,7 @@ pub mod example_setup {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::prelude::*; -/// use mingling::{Groupped, StructuralData, parser::Picker, setup::StructuralRendererSetup}; +/// use mingling::{parser::Picker, setup::StructuralRendererSetup, Grouped, StructuralData}; /// use serde::Serialize; /// use std::io::Write; /// @@ -2453,12 +2698,12 @@ pub mod example_setup {} /// // --------- IMPORTANT --------- /// // For beautiful output structure, do not use `pack!` to wrap the types that need to be output. /// // Instead, manually implement -/// // __________________________________ Mark as structured data so it can be rendered -/// // / ____________________ Implement serde::Serialize -/// // | / _________ Implement mingling::Groupped -/// // | | / to ensure Mingling can recognize the type -/// // vvvvvvvvvvvv vvvvvvvvv vvvvvvvv -/// #[derive(StructuralData, Serialize, Groupped)] +/// // ____________________________________ Mark as structured data so it can be rendered +/// // / ____________________ Implement serde::Serialize +/// // | / _________ Implement mingling::Grouped +/// // | | / to ensure Mingling can recognize the type +/// // vvvvvvvvvvvv vvvvvvvvv vvvvvvv +/// #[derive(StructuralData, Serialize, Grouped)] /// struct Info { /// #[serde(rename = "member_name")] /// name: String, diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 4b3ced6..5cd53db 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -106,6 +106,10 @@ pub mod macros { /// `route! { /* ... */ }` - Used to generate a route that either returns a successful result or early returns an error. #[cfg(feature = "extra_macros")] pub use mingling_macros::route; + /// `#[routeify]` - An extension attribute macro that transforms `expr?` into `route!(expr)`. + /// Can be used standalone or as a chain/renderer extension: `#[chain(routeify, ...)]`. + #[cfg(feature = "extra_macros")] + pub use mingling_macros::routeify; /// `suggest! { "hello", "bye" }` - Used to generate suggestions #[cfg(feature = "comp")] pub use mingling_macros::suggest; @@ -118,9 +122,9 @@ pub mod macros { #[cfg(feature = "macros")] pub use mingling_macros::EnumTag; -/// derive macro Groupped +/// derive macro Grouped #[cfg(feature = "macros")] -pub use mingling_macros::Groupped; +pub use mingling_macros::Grouped; /// derive macro `StructuralData` — marks a type as supporting structured output #[cfg(feature = "structural_renderer")] @@ -171,9 +175,9 @@ pub mod res; /// use mingling::prelude::*; /// ``` pub mod prelude { - /// Re-export of the `Groupped` derive macro for grouping types. + /// Re-export of the `Grouped` derive macro for grouping types. #[cfg(feature = "core")] - pub use crate::Groupped; + pub use crate::Grouped; /// Re-export of the `RenderResult` struct for outputting rendering result #[cfg(feature = "core")] pub use crate::RenderResult; diff --git a/mingling/src/picker/entry_picker.rs b/mingling/src/picker/entry_picker.rs index 7a980c6..7364c50 100644 --- a/mingling/src/picker/entry_picker.rs +++ b/mingling/src/picker/entry_picker.rs @@ -1,8 +1,8 @@ -use mingling_core::{ChainProcess, Groupped, ProgramCollect}; +use mingling_core::{ChainProcess, Grouped, ProgramCollect}; use crate::{picker::Pickable, picker::Picker, picker::PickerArg, picker::PickerPattern1}; -/// Trait for converting Mingling entry types (types that implement `Groupped<R>` and `Into<Vec<String>>`) +/// Trait for converting Mingling entry types (types that implement `Grouped<R>` and `Into<Vec<String>>`) /// into [`Picker`] instances for a given route type `R`. /// /// This trait provides a bridge between entry definitions created with the `mingling` framework @@ -12,7 +12,7 @@ use crate::{picker::Pickable, picker::Picker, picker::PickerArg, picker::PickerP /// /// * `'a` — The lifetime of the picker and its references to argument definitions. /// * `This` — The program type used for dispatching runtime routes; must implement [`ProgramCollect`]. -/// * `Route` — The route type used for dispatching; must implement [`Groupped<This>`]. +/// * `Route` — The route type used for dispatching; must implement [`Grouped<This>`]. pub trait EntryPicker<'a, This> { /// Converts `self` into a [`Picker`] for the given route type `Route`. fn to_picker(self) -> Picker<'a, ChainProcess<This>>; @@ -117,7 +117,7 @@ pub trait EntryPicker<'a, This> { impl<'a, This, Bind> EntryPicker<'a, This> for Bind where This: ProgramCollect<Enum = This>, - Bind: Groupped<This> + Into<Vec<String>>, + Bind: Grouped<This> + Into<Vec<String>>, { fn to_picker(self) -> Picker<'a, ChainProcess<This>> { let args = self.into(); diff --git a/mingling_core/Cargo.toml b/mingling_core/Cargo.toml index 85afd55..14140c6 100644 --- a/mingling_core/Cargo.toml +++ b/mingling_core/Cargo.toml @@ -15,6 +15,7 @@ nightly = [] default = [] async = [] builds = [] +picker = [] dispatch_tree = [] structural_renderer = ["dep:serde"] diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs index 2680f43..e6b7406 100644 --- a/mingling_core/src/any.rs +++ b/mingling_core/src/any.rs @@ -1,12 +1,12 @@ use crate::error::ChainProcessError; -use crate::{Groupped, ProgramCollect}; +use crate::{Grouped, ProgramCollect}; #[doc(hidden)] pub mod group; /// Any type output /// -/// Accepts any type that implements `Send + Groupped<G>` +/// Accepts any type that implements `Send + Grouped<G>` /// After being passed into `AnyOutput`, it will be converted to `Box<dyn Any + Send + 'static>` /// /// Note: @@ -22,10 +22,10 @@ pub struct AnyOutput<G> { } impl<G> AnyOutput<G> { - /// Create an `AnyOutput` from a `Send + Groupped<G>` type + /// Create an `AnyOutput` from a `Send + Grouped<G>` type pub fn new<T>(value: T) -> Self where - T: Send + Groupped<G> + 'static, + T: Send + Grouped<G> + 'static, { Self { inner: Box::new(value), @@ -148,7 +148,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::Groupped; + use crate::Grouped; /// Mock enum for testing AnyOutput #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -175,7 +175,7 @@ mod tests { value: i32, } - impl Groupped<MockGroup> for AlphaData { + impl Grouped<MockGroup> for AlphaData { fn member_id() -> MockGroup { MockGroup::Alpha } @@ -187,7 +187,7 @@ mod tests { name: String, } - impl Groupped<MockGroup> for BetaData { + impl Grouped<MockGroup> for BetaData { fn member_id() -> MockGroup { MockGroup::Beta } @@ -198,7 +198,7 @@ mod tests { #[cfg_attr(feature = "structural_renderer", derive(serde::Serialize))] struct GammaData; - impl Groupped<MockGroup> for GammaData { + impl Grouped<MockGroup> for GammaData { fn member_id() -> MockGroup { MockGroup::Gamma } @@ -354,7 +354,7 @@ mod tests { x: i32, } - impl Groupped<MockGroup> for SerData { + impl Grouped<MockGroup> for SerData { fn member_id() -> MockGroup { MockGroup::Gamma } @@ -381,13 +381,13 @@ mod tests { b: String, } - impl Groupped<MockGroup> for SerA { + impl Grouped<MockGroup> for SerA { fn member_id() -> MockGroup { MockGroup::Alpha } } - impl Groupped<MockGroup> for SerB { + impl Grouped<MockGroup> for SerB { fn member_id() -> MockGroup { MockGroup::Beta } diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs index cb847d9..90ef9fc 100644 --- a/mingling_core/src/any/group.rs +++ b/mingling_core/src/any/group.rs @@ -1,11 +1,11 @@ -use crate::{AnyOutput, ChainProcess}; +use crate::{AnyOutput, ChainProcess, ProgramCollect, Routable}; /// Used to mark a type with a unique enum ID, assisting dynamic dispatch /// -/// **Note:** Unlike earlier versions, `Groupped` no longer requires `Serialize` +/// **Note:** Unlike earlier versions, `Grouped` no longer requires `Serialize` /// even when the `structural_renderer` feature is enabled. Structured output is /// controlled separately via the \[`StructuralData`\] trait. -pub trait Groupped<Group> +pub trait Grouped<Group> where Self: Sized + 'static, { @@ -32,3 +32,17 @@ where AnyOutput::new(self).route_renderer() } } + +impl<T, C> Routable<C> for T +where + C: ProgramCollect<Enum = C>, + T: Grouped<C> + Send, +{ + fn to_chain(self) -> ChainProcess<C> { + T::to_chain(self) + } + + fn to_render(self) -> ChainProcess<C> { + T::to_render(self) + } +} diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index b1fd617..9b9a5d4 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -21,3 +21,6 @@ pub mod node; #[doc(hidden)] pub mod renderer; + +#[doc(hidden)] +pub mod routable; diff --git a/mingling_core/src/asset/routable.rs b/mingling_core/src/asset/routable.rs new file mode 100644 index 0000000..24b7bb1 --- /dev/null +++ b/mingling_core/src/asset/routable.rs @@ -0,0 +1,22 @@ +use crate::ChainProcess; + +/// Provides routing capabilities for converting an item into a `ChainProcess` +/// directed to either the chain or render processing pipeline. +/// +/// This trait enables items to be dispatched to different processing routes +/// (chain or render) by wrapping them into an `AnyOutput` and routing them +/// through the appropriate pipeline. +pub trait Routable<Group> +where + Self: Sized + 'static, +{ + /// Converts the routable item into a `ChainProcess` directed to the chain route. + /// + /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline. + fn to_chain(self) -> ChainProcess<Group>; + + /// Converts the routable item into a `ChainProcess` directed to the render route. + /// + /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline. + fn to_render(self) -> ChainProcess<Group>; +} diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs index f6fecd1..55e9952 100644 --- a/mingling_core/src/comp.rs +++ b/mingling_core/src/comp.rs @@ -96,7 +96,7 @@ impl CompletionHelper { trace!("entry type: {}", any.member_id); let dispatcher_not_found = - <P::ErrorDispatcherNotFound as crate::Groupped<P>>::member_id(); + <P::ErrorDispatcherNotFound as crate::Grouped<P>>::member_id(); if dispatcher_not_found == any.member_id { debug!("dispatcher_not_found matched"); diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs index cfa4700..1fca325 100644 --- a/mingling_core/src/comp/shell_ctx.rs +++ b/mingling_core/src/comp/shell_ctx.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::collections::HashSet; use crate::{Flag, ShellFlag, Suggest}; @@ -115,6 +117,13 @@ impl ShellContext { /// // } /// } /// ``` + #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool { let flag = flag.into(); if self.filling_argument(&flag) { @@ -153,6 +162,13 @@ impl ShellContext { /// // } /// } /// ``` + #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool { for f in flag.into().iter() { if self.previous_word == **f { @@ -190,6 +206,12 @@ impl ShellContext { /// } /// ``` #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn typing_argument(&self) -> bool { #[cfg(target_os = "windows")] { @@ -208,6 +230,12 @@ impl ShellContext { /// when the user has already typed certain flags. The method processes both /// regular suggestion sets and file completion suggestions differently. #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest { let typed = Self::get_typed_arguments(self); match suggest { @@ -225,6 +253,12 @@ impl ShellContext { /// which typically represent command-line flags or options. It returns a vector /// containing these flag strings, converted to owned `String` values. #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn get_typed_arguments(&self) -> HashSet<String> { self.all_words .iter() diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs index a81de64..99afc54 100644 --- a/mingling_core/src/comp/suggest.rs +++ b/mingling_core/src/comp/suggest.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::collections::BTreeSet; use crate::ShellContext; @@ -30,6 +32,12 @@ impl Suggest { /// Filters out already typed flag arguments from suggestion results. #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn strip_typed_argument(self, ctx: &ShellContext) -> Self { ctx.strip_typed_argument(self) } diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 3c2cf9b..4996b19 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -36,6 +36,7 @@ pub use crate::asset::help::*; pub use crate::asset::lazy_resource::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; +pub use crate::asset::routable::*; /// All error types of `Mingling` pub mod error { diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs index d5aab4b..fe78979 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -4,7 +4,7 @@ use std::pin::Pin; #[cfg(feature = "dispatch_tree")] use crate::Dispatcher; -use crate::{AnyOutput, ChainProcess, Groupped, RenderResult}; +use crate::{AnyOutput, ChainProcess, Grouped, RenderResult}; #[cfg(feature = "structural_renderer")] use crate::{StructuralRendererSetting, error::StructuralRendererSerializeError}; @@ -21,9 +21,9 @@ pub use mock::*; pub trait ProgramCollect { /// Enum type representing internal IDs for the program type Enum; - type ErrorDispatcherNotFound: Groupped<Self::Enum>; - type ErrorRendererNotFound: Groupped<Self::Enum>; - type ResultEmpty: Groupped<Self::Enum>; + type ErrorDispatcherNotFound: Grouped<Self::Enum>; + type ErrorRendererNotFound: Grouped<Self::Enum>; + type ResultEmpty: Grouped<Self::Enum>; /// Use a prefix tree to quickly match arguments and dispatch to an Entry #[cfg(feature = "dispatch_tree")] diff --git a/mingling_core/src/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs index 9b2e7af..5847f10 100644 --- a/mingling_core/src/program/collection/mock.rs +++ b/mingling_core/src/program/collection/mock.rs @@ -4,7 +4,7 @@ use std::pin::Pin; #[cfg(feature = "dispatch_tree")] use crate::Dispatcher; -use crate::{AnyOutput, ChainProcess, Groupped, ProgramCollect, RenderResult}; +use crate::{AnyOutput, ChainProcess, Grouped, ProgramCollect, RenderResult}; #[cfg(feature = "structural_renderer")] use crate::{StructuralRendererSetting, error::StructuralRendererSerializeError}; @@ -23,7 +23,7 @@ pub enum MockProgramCollect { Bar, } -impl Groupped<MockProgramCollect> for MockProgramCollect { +impl Grouped<MockProgramCollect> for MockProgramCollect { fn member_id() -> MockProgramCollect { MockProgramCollect::Foo } diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs index 56d8e0e..db1691b 100644 --- a/mingling_core/src/program/hook.rs +++ b/mingling_core/src/program/hook.rs @@ -678,7 +678,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::Groupped; + use crate::Grouped; use std::sync::atomic::{AtomicBool, Ordering}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -694,7 +694,7 @@ mod tests { } } - impl Groupped<MockHookEnum> for MockHookEnum { + impl Grouped<MockHookEnum> for MockHookEnum { fn member_id() -> MockHookEnum { MockHookEnum::A } diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs new file mode 100644 index 0000000..e4cd826 --- /dev/null +++ b/mingling_macros/src/attr.rs @@ -0,0 +1,9 @@ +pub(crate) mod chain; +#[cfg(feature = "comp")] +pub(crate) mod completion; +#[cfg(feature = "clap")] +pub(crate) mod dispatcher_clap; +pub(crate) mod help; +#[cfg(feature = "extra_macros")] +pub(crate) mod program_setup; +pub(crate) mod renderer; diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/attr/chain.rs index 566e782..120e65d 100644 --- a/mingling_macros/src/chain.rs +++ b/mingling_macros/src/attr/chain.rs @@ -62,13 +62,13 @@ fn generate_proc_fn( quote! { #(#immut_resource_stmts)* #wrapped_body; - <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>> + <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } } else { quote! { #wrapped_body; - <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>> + <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } }; @@ -158,7 +158,7 @@ fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> { Ok(()) } -pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Reject non-empty attribute arguments; #[chain] must be bare if !attr.is_empty() { return syn::Error::new( @@ -263,7 +263,10 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Builds a match arm for chain mapping -pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_chain_arm( + struct_name: &Ident, + previous_type: &TypePath, +) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { #struct_name => #enum_variant, @@ -271,14 +274,14 @@ pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_ma } /// Builds a match arm for chain existence check -pub fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { Self::#enum_variant => true, } } -pub fn register_chain(input: TokenStream) -> TokenStream { +pub(crate) fn register_chain(input: TokenStream) -> TokenStream { // Parse the input as a comma-separated list of arguments let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/attr/completion.rs index ae01462..e917d7d 100644 --- a/mingling_macros/src/completion.rs +++ b/mingling_macros/src/attr/completion.rs @@ -5,7 +5,7 @@ use syn::spanned::Spanned; use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input}; #[cfg(feature = "comp")] -pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse the attribute arguments such as HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)] use crate::get_global_set; let previous_type_path: TypePath = if attr.is_empty() { diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 0945e31..6083a52 100644 --- a/mingling_macros/src/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -93,7 +93,7 @@ impl Parse for DispatcherClapInput { } #[cfg(feature = "clap")] -pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let attr_input = parse_macro_input!(attr as DispatcherClapInput); let input_struct = parse_macro_input!(item as ItemStruct); let struct_name = &input_struct.ident; @@ -144,7 +144,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream Some(quote! { #[allow(non_snake_case)] #[::mingling::macros::help] - pub fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult { + pub(crate) fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult { use std::io::Write; use clap::ColorChoice; @@ -189,7 +189,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream // Generate the dispatcher struct #[doc(hidden)] #[derive(Default)] - pub struct #dispatcher_struct; + pub(crate) struct #dispatcher_struct; impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct { fn node(&self) -> ::mingling::Node { diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/attr/help.rs index 6e0d12e..6defae4 100644 --- a/mingling_macros/src/help.rs +++ b/mingling_macros/src/attr/help.rs @@ -14,7 +14,7 @@ fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> } } -pub fn help_attr(item: TokenStream) -> TokenStream { +pub(crate) fn help_attr(item: TokenStream) -> TokenStream { // Parse the function item let input_fn = parse_macro_input!(item as ItemFn); @@ -160,7 +160,7 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2:: } } -pub fn register_help(input: TokenStream) -> TokenStream { +pub(crate) fn register_help(input: TokenStream) -> TokenStream { // Parse the input as a comma-separated list of arguments let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); diff --git a/mingling_macros/src/program_setup.rs b/mingling_macros/src/attr/program_setup.rs index 7fd9d16..dee5a1c 100644 --- a/mingling_macros/src/program_setup.rs +++ b/mingling_macros/src/attr/program_setup.rs @@ -47,7 +47,7 @@ fn extract_return_type(sig: &Signature) -> syn::Result<()> { } } -pub fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // #[program_setup] takes no arguments; always use the default program path let _ = attr; let program_path = crate::default_program_path(); diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/attr/renderer.rs index d124ec9..c7cbb0b 100644 --- a/mingling_macros/src/renderer.rs +++ b/mingling_macros/src/attr/renderer.rs @@ -15,7 +15,7 @@ fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> } #[allow(clippy::too_many_lines)] -pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // #[renderer] takes no arguments; always use the default program path let _ = attr; let program_path = crate::default_program_path(); @@ -130,7 +130,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Builds the renderer entry for the global renderers list -pub fn build_renderer_entry( +pub(crate) fn build_renderer_entry( struct_name: &syn::Ident, previous_type: &TypePath, ) -> proc_macro2::TokenStream { @@ -141,7 +141,7 @@ pub fn build_renderer_entry( } /// Builds the renderer existence check entry -pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { Self::#enum_variant => true, @@ -150,7 +150,9 @@ pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::Toke /// Builds the structural renderer entry #[cfg(feature = "structural_renderer")] -pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_structural_renderer_entry( + previous_type: &TypePath, +) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { Self::#enum_variant => { @@ -164,7 +166,7 @@ pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2: } } -pub fn register_renderer(input: TokenStream) -> TokenStream { +pub(crate) fn register_renderer(input: TokenStream) -> TokenStream { // Parse the input as a comma-separated list of arguments let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); diff --git a/mingling_macros/src/derive.rs b/mingling_macros/src/derive.rs new file mode 100644 index 0000000..ffa405b --- /dev/null +++ b/mingling_macros/src/derive.rs @@ -0,0 +1,2 @@ +pub(crate) mod enum_tag; +pub(crate) mod grouped; diff --git a/mingling_macros/src/enum_tag.rs b/mingling_macros/src/derive/enum_tag.rs index 6277b69..a7f71f0 100644 --- a/mingling_macros/src/enum_tag.rs +++ b/mingling_macros/src/derive/enum_tag.rs @@ -4,7 +4,7 @@ use syn::{ Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Variant, parse_macro_input, }; -pub fn derive_enum_tag(input: TokenStream) -> TokenStream { +pub(crate) fn derive_enum_tag(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); match derive_enum_tag_impl(input) { diff --git a/mingling_macros/src/groupped.rs b/mingling_macros/src/derive/grouped.rs index 8aee003..307aab6 100644 --- a/mingling_macros/src/groupped.rs +++ b/mingling_macros/src/derive/grouped.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{DeriveInput, Ident, parse_macro_input}; -pub fn derive_groupped(input: TokenStream) -> TokenStream { +pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream { // Parse the input struct/enum let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; @@ -12,11 +12,11 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream { let any_output_convert_impls = proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident)); - // Generate the Groupped trait implementation + // Generate the Grouped trait implementation let expanded = quote! { ::mingling::macros::register_type!(#struct_name); - impl ::mingling::Groupped<#group_ident> for #struct_name { + impl ::mingling::Grouped<#group_ident> for #struct_name { fn member_id() -> #group_ident { #group_ident::#struct_name } @@ -29,7 +29,7 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream { } #[cfg(feature = "structural_renderer")] -pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { +pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { // Parse the input struct/enum let input_parsed = parse_macro_input!(input as DeriveInput); let struct_name = input_parsed.ident.clone(); @@ -39,14 +39,14 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { let any_output_convert_impls = proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident)); - // Generate both Serialize and Groupped implementations + // Generate both Serialize and Grouped implementations let expanded = quote! { #[derive(serde::Serialize)] #input_parsed ::mingling::macros::register_type!(#struct_name); - impl ::mingling::Groupped<#group_ident> for #struct_name { + impl ::mingling::Grouped<#group_ident> for #struct_name { fn member_id() -> #group_ident { #group_ident::#struct_name } diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs new file mode 100644 index 0000000..aff20e2 --- /dev/null +++ b/mingling_macros/src/extensions.rs @@ -0,0 +1,116 @@ +//! Extension point mechanism for Mingling attribute macros. +//! +//! This module provides a way for attribute macros like `#[chain]`, `#[renderer]`, +//! `#[help]`, and `#[completion]` to accept extension identifiers that are +//! applied as outer attributes before the bare macro. + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Token}; + +/// Extension: `#[routeify]` — transforms `expr?` into `route!(expr)`. +#[cfg(feature = "extra_macros")] +pub(crate) mod routeify; + +/// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`. +pub(crate) struct Extensions { + pub(crate) exts: Vec<Ident>, +} + +impl Parse for Extensions { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut exts = Vec::new(); + while !input.is_empty() { + let ident: Ident = input.parse()?; + exts.push(ident); + if input.peek(Token![,]) { + let _ = input.parse::<Token![,]>(); + } + } + Ok(Extensions { exts }) + } +} + +/// Parsed extensions for `#[completion(EntryType, routeify, ...)]`. +#[cfg(feature = "comp")] +pub(crate) struct CompletionExt { + pub(crate) entry_type: proc_macro2::TokenStream, + pub(crate) exts: Vec<Ident>, +} + +#[cfg(feature = "comp")] +impl Parse for CompletionExt { + fn parse(input: ParseStream) -> syn::Result<Self> { + let entry_type: proc_macro2::TokenStream = input.parse()?; + let mut exts = Vec::new(); + while !input.is_empty() { + let _ = input.parse::<Token![,]>(); + if input.is_empty() { + break; + } + let ident: Ident = input.parse()?; + exts.push(ident); + } + Ok(CompletionExt { entry_type, exts }) + } +} + +/// Generates a re-dispatch token stream for attribute macros that take **no fixed arguments** +/// (chain, renderer, help). +pub(crate) fn try_redispatch_simple( + attr: TokenStream, + item: &TokenStream, + bare_attr_name: &str, +) -> Option<TokenStream> { + if attr.is_empty() { + return None; + } + + let exts: Extensions = syn::parse(attr).ok()?; + if exts.exts.is_empty() { + return None; + } + + let bare = Ident::new(bare_attr_name, proc_macro2::Span::call_site()); + let exts = &exts.exts; + let item = proc_macro2::TokenStream::from(item.clone()); + + Some( + quote! { + #(#[#exts])* + #[#bare] + #item + } + .into(), + ) +} + +/// Generates a re-dispatch token stream for `#[completion(EntryType, ...)]`. +#[cfg(feature = "comp")] +pub(crate) fn try_redispatch_completion( + attr: TokenStream, + item: &TokenStream, +) -> Option<TokenStream> { + if attr.is_empty() { + return None; + } + + let parsed: CompletionExt = syn::parse(attr).ok()?; + if parsed.exts.is_empty() { + return None; + } + + let entry_type = &parsed.entry_type; + let exts = &parsed.exts; + let item = proc_macro2::TokenStream::from(item.clone()); + + Some( + quote! { + #(#[#exts])* + #[::mingling::macros::completion(#entry_type)] + #item + } + .into(), + ) +} diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs new file mode 100644 index 0000000..f011fb9 --- /dev/null +++ b/mingling_macros/src/extensions/routeify.rs @@ -0,0 +1,39 @@ +//! The `#[routeify]` extension — transforms `expr?` into `route!(expr)`. +//! +//! Designed as an extension for the Mingling attribute macro system, intended +//! to be used with `#[chain(routeify)]` or standalone as `#[routeify]`. +//! +//! # How it works +//! +//! The macro parses the function AST and replaces every `Expr::Try` node with an +//! equivalent `route!(expr)` invocation. + +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::visit_mut::VisitMut; +use syn::{Expr, ItemFn, parse_macro_input}; + +struct RouteifyTransform; + +impl VisitMut for RouteifyTransform { + fn visit_expr_mut(&mut self, expr: &mut Expr) { + syn::visit_mut::visit_expr_mut(self, expr); + + if let Expr::Try(try_expr) = expr { + let inner = &*try_expr.expr; + let inner_tokens = inner.to_token_stream(); + + if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! { + ::mingling::macros::route!(#inner_tokens) + }) { + *expr = macro_expr; + } + } + } +} + +pub(crate) fn routeify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_fn = parse_macro_input!(item as ItemFn); + RouteifyTransform.visit_item_fn_mut(&mut input_fn); + input_fn.to_token_stream().into() +} diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs new file mode 100644 index 0000000..720b20a --- /dev/null +++ b/mingling_macros/src/func.rs @@ -0,0 +1,12 @@ +pub(crate) mod dispatcher; +#[cfg(feature = "extra_macros")] +pub(crate) mod entry; +pub(crate) mod gen_program; +#[cfg(feature = "extra_macros")] +pub(crate) mod group; +pub(crate) mod node; +pub(crate) mod pack; +#[cfg(feature = "extra_macros")] +pub(crate) mod pack_err; +#[cfg(feature = "comp")] +pub(crate) mod suggest; diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index 2a7c850..a61dd26 100644 --- a/mingling_macros/src/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -75,7 +75,7 @@ impl Parse for DispatcherChainInput { // are nearly identical and could benefit from refactoring into common helper functions. #[allow(clippy::too_many_lines)] -pub fn dispatcher(input: TokenStream) -> TokenStream { +pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { // Parse the input let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); @@ -134,8 +134,8 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { ::mingling::macros::node!(#command_name_str) } fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> { - use ::mingling::Groupped; - #pack::new(args).to_chain() + use ::mingling::Grouped; + ::mingling::Routable::to_chain(#pack::new(args)) } fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> { Box::new(#command_struct) @@ -209,7 +209,7 @@ impl Parse for RegisterDispatcherInput { } #[cfg(feature = "dispatch_tree")] -pub fn register_dispatcher(input: TokenStream) -> TokenStream { +pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { let RegisterDispatcherInput { node_name, dispatcher_type, @@ -243,7 +243,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { } #[cfg(not(feature = "dispatch_tree"))] -pub fn register_dispatcher(_input: TokenStream) -> TokenStream { +pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream { quote! {}.into() } diff --git a/mingling_macros/src/entry.rs b/mingling_macros/src/func/entry.rs index 2ac5d6b..35209e5 100644 --- a/mingling_macros/src/entry.rs +++ b/mingling_macros/src/func/entry.rs @@ -39,7 +39,7 @@ fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result<Vec<String>> { Ok(strings) } -pub fn entry(input: TokenStream) -> TokenStream { +pub(crate) fn entry(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as EntryInput); let strings = match &parsed { diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs new file mode 100644 index 0000000..7128a9a --- /dev/null +++ b/mingling_macros/src/func/gen_program.rs @@ -0,0 +1,602 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +use crate::CHAINS; +use crate::CHAINS_EXIST; +#[cfg(feature = "dispatch_tree")] +use crate::COMPILE_TIME_DISPATCHERS; +#[cfg(feature = "comp")] +use crate::COMPLETIONS; +use crate::HELP_REQUESTS; +use crate::PACKED_TYPES; +use crate::RENDERERS; +use crate::RENDERERS_EXIST; +#[cfg(feature = "structural_renderer")] +use crate::STRUCTURAL_RENDERERS; +use crate::attr::{chain, renderer}; +use crate::get_global_set; +#[cfg(feature = "dispatch_tree")] +use crate::systems::dispatch_tree_gen; + +#[cfg(feature = "async")] +const ASYNC_ENABLED: bool = true; +#[cfg(not(feature = "async"))] +const ASYNC_ENABLED: bool = false; + +/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. +fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { + let s = entry.to_string(); + let arrow_idx = s + .find("=>") + .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); + let struct_str = s[..arrow_idx].trim(); + let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); + let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); + let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); + (struct_ident, variant_ident) +} + +/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. +/// Always compiled; returns empty map when pathf feature is not enabled. +fn load_pathf_map() -> std::collections::HashMap<String, String> { + if !cfg!(feature = "pathf") { + return std::collections::HashMap::new(); + } + let out_dir = std::env::var("OUT_DIR").ok(); + let crate_name = std::env::var("CARGO_PKG_NAME").ok(); + match (out_dir, crate_name) { + (Some(dir), Some(name)) => { + let path = std::path::Path::new(&dir).join(&name).join("type_using.rs"); + match std::fs::read_to_string(&path) { + Ok(content) => content + .lines() + .filter_map(|line| { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("use ") { + let path = rest.strip_suffix(';').unwrap_or(rest); + if let Some((_mod, type_name)) = path.rsplit_once("::") { + return Some((type_name.to_string(), path.to_string())); + } + } + None + }) + .collect(), + Err(_) => std::collections::HashMap::new(), + } + } + _ => std::collections::HashMap::new(), + } +} + +/// Resolves a type name to its full path token stream using the pathf mapping. +pub(crate) fn resolve_type( + name: &str, + map: &std::collections::HashMap<String, String>, +) -> proc_macro2::TokenStream { + if let Some(full_path) = map.get(name) { + syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + }) + } else { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + } +} + +pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "comp")] + let comp_gen = quote! { + ::mingling::macros::program_comp_gen!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_gen = quote! {}; + + TokenStream::from(quote! { + /// Alias for the current program type `crate::ThisProgram` + pub type Next = ::mingling::ChainProcess<crate::ThisProgram>; + + impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram> + { + fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) + } + other => other, + } + } + + fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer)) + } + other => other, + } + } + } + + #comp_gen + ::mingling::macros::program_fallback_gen!(); + ::mingling::macros::program_final_gen!(); + }) +} + +#[cfg(feature = "comp")] +pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "async")] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub async fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); + crate::CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(not(feature = "async"))] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); + crate::CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(feature = "dispatch_tree")] + let internal_dispatcher_comp = quote! { + use __internal_completion_mod::__internal_dispatcher_comp; + }; + + #[cfg(not(feature = "dispatch_tree"))] + let internal_dispatcher_comp = quote! {}; + + let comp_dispatcher = quote! { + #[doc(hidden)] + mod __internal_completion_mod { + use ::mingling::Grouped; + ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); + ::mingling::macros::pack!( + CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) + ); + } + #internal_dispatcher_comp + use __internal_completion_mod::CompletionContext; + use __internal_completion_mod::CompletionSuggest; + pub use __internal_completion_mod::CMDCompletion; + + #fn_exec_comp + + ::mingling::macros::register_type!(CompletionContext); + + #[allow(unused)] + #[doc(hidden)] + #[::mingling::macros::renderer] + pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { + let result = ::mingling::RenderResult::default(); + let (ctx, suggest) = prev.inner; + ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest); + result + } + }; + + TokenStream::from(comp_dispatcher) +} + +pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream { + let type_ident = parse_macro_input!(input as syn::Ident); + let entry_str = type_ident.to_string(); + + get_global_set(&PACKED_TYPES) + .lock() + .unwrap() + .insert(entry_str); + + TokenStream::new() +} + +pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream { + chain::register_chain(input) +} + +pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream { + renderer::register_renderer(input) +} + +pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "structural_renderer")] + let pack_empty = quote! { + #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + #[cfg(not(feature = "structural_renderer"))] + let pack_empty = quote! { + #[derive(::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + let expanded = quote! { + ::mingling::macros::pack!(ErrorRendererNotFound = String); + ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); + #pack_empty + }; + TokenStream::from(expanded) +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { + let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); + + let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); + + let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); + let chains = get_global_set(&CHAINS).lock().unwrap().clone(); + let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); + let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); + + #[cfg(feature = "structural_renderer")] + let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clone(); + + #[cfg(feature = "comp")] + let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); + + let packed_types: Vec<proc_macro2::TokenStream> = packed_types + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let chain_tokens: Vec<proc_macro2::TokenStream> = chains + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let pathf_map: std::collections::HashMap<String, String> = if cfg!(feature = "pathf") { + load_pathf_map() + } else { + std::collections::HashMap::new() + }; + + let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") { + pathf_map + .values() + .map(|path| format!("use {};", path).parse().unwrap_or_default()) + .collect() + } else { + Vec::new() + }; + + #[cfg(feature = "structural_renderer")] + let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "structural_renderer")] + let structural_render = quote! { + fn structural_render( + any: ::mingling::AnyOutput<Self::Enum>, + setting: &::mingling::StructuralRendererSetting, + ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#structural_renderer_tokens)* + _ => { + // Non-structural types: render ResultEmpty (which implements + // StructuralData + Serialize) instead of producing nothing. + let mut r = ::mingling::RenderResult::default(); + ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; + Ok(r) + } + } + } + }; + + #[cfg(not(feature = "structural_renderer"))] + let structural_render = quote! {}; + + #[cfg(feature = "dispatch_tree")] + let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clone() + .iter() + .cloned() + .collect(); + + #[cfg(feature = "dispatch_tree")] + let dispatch_tree_nodes = { + let entries: Vec<(String, String, String)> = compile_time_dispatchers + .iter() + .filter_map(|entry| { + let parts: Vec<&str> = entry.split(':').collect(); + if parts.len() == 3 { + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + )) + } else { + None + } + }) + .collect(); + + let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); + let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); + + quote! { + #get_nodes_fn + #dispatch_trie_fn + } + }; + + #[cfg(not(feature = "dispatch_tree"))] + let dispatch_tree_nodes = quote! {}; + + #[cfg(feature = "comp")] + let completion_tokens: Vec<proc_macro2::TokenStream> = completions + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "comp")] + let comp = quote! { + fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#completion_tokens)* + _ => ::mingling::Suggest::FileCompletion, + } + } + }; + + #[cfg(not(feature = "comp"))] + let comp = quote! {}; + + // Build render function arms from stored entries + let render_fn = + if renderer_tokens.is_empty() { + quote! { + fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + ::mingling::RenderResult::default() + } + } + } else { + let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + <#resolved_struct as ::mingling::Renderer>::render(value) + } + } + }).collect(); + quote! { + fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + match any.member_id { + #(#render_arms)* + _ => ::mingling::RenderResult::default(), + } + } + } + }; + + // Build do_chain function (async and sync versions) + let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await }; + ::std::boxed::Box::pin(fut) + } + } + }).collect(); + + let chain_arms_sync: Vec<_> = chain_tokens + .iter() + .map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value) + } + } + }) + .collect(); + + let do_chain_fn = if chain_tokens.is_empty() { + quote! { + fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { + ::core::panic!("No chain found for type id") + } + } + } else if ASYNC_ENABLED { + quote! { + fn do_chain( + any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { + match any.member_id { + #(#chain_arms_async)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + } else { + quote! { + fn do_chain( + any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::mingling::ChainProcess<Self::Enum> { + match any.member_id { + #(#chain_arms_sync)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + }; + + let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let num_variants = packed_types.len(); + let repr_type = if u8::try_from(num_variants).is_ok() { + quote! { u8 } + } else if u16::try_from(num_variants).is_ok() { + quote! { u16 } + } else if u32::try_from(num_variants).is_ok() { + quote! { u32 } + } else { + quote! { u128 } + }; + + let expanded = quote! { + #[derive(Debug, PartialEq, Eq, Clone)] + #[repr(#repr_type)] + #[allow(nonstandard_style)] + pub enum #name { + #(#packed_types),* + } + + impl ::std::fmt::Display for #name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* + } + } + } + + impl ::mingling::ProgramCollect for #name { + type Enum = #name; + type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type ErrorRendererNotFound = ErrorRendererNotFound; + 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())) + } + fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) + } + fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ResultEmpty) + } + #render_fn + #do_chain_fn + fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#help_tokens)* + _ => ::mingling::RenderResult::default(), + } + } + fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { + match any.member_id { + #(#renderer_exist_tokens)* + _ => false + } + } + fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { + match any.member_id { + #(#chain_exist_tokens)* + _ => false + } + } + #dispatch_tree_nodes + #structural_render + #comp + } + + impl #name { + /// Creates a new `Program<#name>` instance with default configuration. + pub fn new() -> ::mingling::Program<#name> { + ::mingling::Program::new() + } + + /// Returns a static reference to the global `Program<#name>` singleton. + pub fn this() -> &'static ::mingling::Program<#name> { + &::mingling::this::<#name>() + } + } + }; + + // Clear all global registries to prevent stale state in Rust Analyzer + get_global_set(&PACKED_TYPES).lock().unwrap().clear(); + get_global_set(&CHAINS).lock().unwrap().clear(); + get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); + get_global_set(&RENDERERS).lock().unwrap().clear(); + get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); + get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); + #[cfg(feature = "comp")] + get_global_set(&COMPLETIONS).lock().unwrap().clear(); + #[cfg(feature = "dispatch_tree")] + get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clear(); + #[cfg(feature = "structural_renderer")] + get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clear(); + + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/func/group.rs index 59da9dd..b865913 100644 --- a/mingling_macros/src/group_impl.rs +++ b/mingling_macros/src/func/group.rs @@ -91,7 +91,7 @@ fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream { } } -pub fn group_macro(input: TokenStream) -> TokenStream { +pub(crate) fn group_macro(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as GroupInput); let is_aliased = matches!(input, GroupInput::Aliased { .. }); @@ -124,7 +124,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream { quote! {} }; - // Generate the module with the Groupped implementation + // Generate the module with the Grouped implementation let expanded = quote! { #alias_stmt #[allow(non_camel_case_types)] @@ -133,7 +133,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream { #type_use #alias_use - impl ::mingling::Groupped<__MinglingProgram> for #type_name { + impl ::mingling::Grouped<__MinglingProgram> for #type_name { fn member_id() -> __MinglingProgram { __MinglingProgram::#type_name } diff --git a/mingling_macros/src/node.rs b/mingling_macros/src/func/node.rs index b3a61c6..1b944a1 100644 --- a/mingling_macros/src/node.rs +++ b/mingling_macros/src/func/node.rs @@ -18,7 +18,7 @@ impl Parse for NodeInput { } } -pub fn node(input: TokenStream) -> TokenStream { +pub(crate) fn node(input: TokenStream) -> TokenStream { // Parse the input as a string literal let input_parsed = syn::parse_macro_input!(input as NodeInput); let path_str = input_parsed.path.value(); diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/func/pack.rs index 7f05232..a1a7e6b 100644 --- a/mingling_macros/src/pack.rs +++ b/mingling_macros/src/func/pack.rs @@ -25,7 +25,7 @@ impl Parse for PackInput { } #[allow(clippy::too_many_lines)] -pub fn pack(input: TokenStream) -> TokenStream { +pub(crate) fn pack(input: TokenStream) -> TokenStream { let pack_input = syn::parse_macro_input!(input as PackInput); let group_name = crate::default_program_path(); @@ -138,7 +138,7 @@ pub fn pack(input: TokenStream) -> TokenStream { } } - impl ::mingling::Groupped<#group_name> for #type_name { + impl ::mingling::Grouped<#group_name> for #type_name { fn member_id() -> #group_name { #group_name::#type_name } diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/func/pack_err.rs index ba7cf17..36e550a 100644 --- a/mingling_macros/src/pack_err.rs +++ b/mingling_macros/src/func/pack_err.rs @@ -31,7 +31,7 @@ impl syn::parse::Parse for PackErrInput { } #[allow(clippy::too_many_lines)] -pub fn pack_err(input: TokenStream) -> TokenStream { +pub(crate) fn pack_err(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as PackErrInput); match parsed { @@ -42,7 +42,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream { // Note: No longer derives Serialize under structural_renderer. // Use pack_err_structural for structured output support. let derive = quote! { - #[derive(::mingling::Groupped)] + #[derive(::mingling::Grouped)] }; let expanded = quote! { @@ -75,7 +75,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream { // Note: No longer derives Serialize under structural_renderer. // Use pack_err_structural for structured output support. let derive = quote! { - #[derive(::mingling::Groupped)] + #[derive(::mingling::Grouped)] }; let expanded = quote! { @@ -123,7 +123,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream { /// impl ::mingling::__private::StructuralData for ErrorNotFound {} /// ``` #[cfg(feature = "structural_renderer")] -pub fn pack_err_structural(input: TokenStream) -> TokenStream { +pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as PackErrInput); let type_name = match &parsed { @@ -150,7 +150,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { let snake_name = snake_case!(&name_str); let expanded = quote! { - #[derive(::mingling::Groupped, ::serde::Serialize)] + #[derive(::mingling::Grouped, ::serde::Serialize)] pub struct #type_name { /// The snake_case name of this error, automatically set at compile time. pub name: String, @@ -179,7 +179,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { let snake_name = snake_case!(&name_str); let expanded = quote! { - #[derive(::mingling::Groupped, ::serde::Serialize)] + #[derive(::mingling::Grouped, ::serde::Serialize)] pub struct #type_name { /// The snake_case name of this error, automatically set at compile time. pub name: String, diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/func/suggest.rs index d3ab446..0f2026f 100644 --- a/mingling_macros/src/suggest.rs +++ b/mingling_macros/src/func/suggest.rs @@ -35,7 +35,7 @@ impl Parse for SuggestItem { } #[cfg(feature = "comp")] -pub fn suggest(input: TokenStream) -> TokenStream { +pub(crate) fn suggest(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as SuggestInput); let mut items = Vec::new(); @@ -71,7 +71,7 @@ pub fn suggest(input: TokenStream) -> TokenStream { expanded.into() } -pub fn suggest_enum(input: TokenStream) -> TokenStream { +pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream { let enum_type = parse_macro_input!(input as syn::Type); let expanded = quote! {{ diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 9419f39..a85648f 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -13,7 +13,7 @@ //! ┌──────────────────────────────────────────────────────────────────┐ //! │ Phase 1: Declaration │ //! │ │ -//! │ dispatcher! pack! node! #[derive(Groupped)] │ +//! │ dispatcher! pack! node! #[derive(Grouped)] │ //! │ │ │ │ │ │ //! │ V V V V │ //! │ Declares Wraps a Builds Makes a type │ @@ -55,7 +55,7 @@ //! | `pack_err!` | Creates an error struct with automatic `name` field | //! | `pack_err_structural!` | Like `pack_err!` but also derives `StructuralData` for structured output | //! | `entry!` | Creates a packed entry from string literals | -//! | [`#[derive(Groupped)]`](derive@Groupped) | Makes a type recognizable by the framework's type registry | +//! | [`#[derive(Grouped)]`](derive@Grouped) | Makes a type recognizable by the framework's type registry | //! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) | //! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) | //! @@ -141,40 +141,48 @@ //! } //! ``` -use proc_macro::TokenStream; +#[cfg(feature = "extra_macros")] use quote::quote; + +#[cfg(feature = "extra_macros")] +use syn::parse_macro_input; + +use proc_macro::TokenStream; use std::collections::BTreeSet; use std::sync::Mutex; use std::sync::OnceLock; -use syn::parse_macro_input; -mod chain; +mod attr; +mod derive; +mod func; +mod systems; + +mod extensions; +mod utils; + +// Bring all sub-modules into scope at the old paths so that existing +// references (e.g. `chain::chain_attr`, `renderer::renderer_attr`) +// continue to work without any `use`-path changes. #[cfg(feature = "comp")] -mod completion; -#[cfg(feature = "dispatch_tree")] -mod dispatch_tree_gen; -mod dispatcher; +use attr::completion; #[cfg(feature = "clap")] -mod dispatcher_clap; +use attr::dispatcher_clap; #[cfg(feature = "extra_macros")] -mod entry; -mod enum_tag; +use attr::program_setup; +use attr::{chain, help, renderer}; +use derive::{enum_tag, grouped}; #[cfg(feature = "extra_macros")] -mod group_impl; -mod groupped; -mod help; -mod node; -mod pack; +use func::entry; #[cfg(feature = "extra_macros")] -mod pack_err; +pub(crate) use func::group as group_impl; #[cfg(feature = "extra_macros")] -mod program_setup; -mod renderer; -mod res_injection; -#[cfg(feature = "structural_renderer")] -mod structural_data; +use func::pack_err; #[cfg(feature = "comp")] -mod suggest; +use func::suggest; +use func::{dispatcher, node, pack}; +use systems::res_injection; +#[cfg(feature = "structural_renderer")] +pub(crate) use systems::structural_data; pub(crate) fn default_program_path() -> proc_macro2::TokenStream { quote::quote! { crate::ThisProgram } @@ -262,44 +270,6 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { entry.contains(&format!(":: {variant_name} =>")) } -/// Registers an outside-type as a member of a program group without modifying its definition. -/// -/// This macro allows you to use outside-types from external crates (like `std::io::Error`) -/// within the Mingling framework by generating a `Groupped` implementation and registering -/// the type's simple name as an enum variant. -/// -/// # Syntax -/// -/// ```rust,ignore -/// group!(std::io::Error); -/// group!(ParseIntError); -/// ``` -/// -/// The type is registered under the default program (`crate::ThisProgram`). -/// -/// # How it works -/// -/// The macro generates a module containing: -/// - A `use` import for the program path and the outside-type -/// - An `impl Groupped<Program>` for the outside-type -/// - A `register_type!` call with the type's simple name -/// -/// The type's simple name (e.g. `Error`) is used as the enum variant in the generated -/// program enum, just like `#[derive(Groupped)]` or `pack!`. -/// -/// # Example -/// -/// ```rust,ignore -/// use mingling::macros::group; -/// -/// // Register std::io::Error as a group member -/// group!(std::io::Error); -/// ``` -/// -/// After expansion, the type can be used in chains and renderers like any -/// `#[derive(Groupped)]` type. -/// -/// This macro is only available with the `extra_macros` feature. #[cfg(feature = "extra_macros")] #[proc_macro] pub fn group(input: TokenStream) -> TokenStream { @@ -402,7 +372,7 @@ pub fn node(input: TokenStream) -> TokenStream { /// - `AsRef<String>`, `AsMut<String>` /// - `Default` if `String: Default` /// - `Into<AnyOutput<ThisProgram>>`, `Into<ChainProcess<ThisProgram>>` -/// - Implements `Groupped<ThisProgram>` with `member_id()` returning the enum variant +/// - 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. @@ -438,7 +408,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream { /// 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 `Groupped` +/// 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 @@ -461,7 +431,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream { /// For `pack_err!(ErrorNotFound)`: /// /// ```rust,ignore -/// #[derive(::mingling::Groupped)] +/// #[derive(::mingling::Grouped)] /// pub struct ErrorNotFound { /// name: String, /// } @@ -478,7 +448,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream { /// For `pack_err!(ErrorNotDir = PathBuf)`: /// /// ```rust,ignore -/// #[derive(::mingling::Groupped)] +/// #[derive(::mingling::Grouped)] /// pub struct ErrorNotDir { /// name: String, /// info: PathBuf, @@ -521,20 +491,25 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { pack_err::pack_err_structural(input) } -/// Early-returns an error from a `Result`, converting the `Ok` branch to a -/// `ChainProcess`. +/// Early-returns the error from a `Result`, converting the `Ok` branch to the +/// next chain process value. /// /// This macro is equivalent to: /// ```rust,ignore /// match expr { /// Ok(r) => r, -/// Err(e) => return ::mingling::Groupped::to_chain(e), +/// Err(e) => return ::mingling::Routable::to_chain(e), /// } /// ``` /// -/// It is useful inside chain functions where you have a `Result<SomeType, SomeType>` -/// where both types implement `Groupped` and want to propagate the error case -/// as an early return via `Groupped::to_chain()`. +/// It is useful inside chain functions where you have a `Result<SuccessType, ErrorType>` +/// where both types implement `Routable` and you want to propagate the error case +/// as an early return via `Routable::to_chain()`. +/// +/// The key difference from a simple `?` operator is that `route!` converts **both** +/// the success and error types into the chain process — the `Ok` value is unwrapped +/// directly, while the `Err` value is converted via `Routable::to_chain()` and +/// returned early. /// /// # Example /// @@ -542,7 +517,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { /// use mingling::macros::{chain, route}; /// /// #[chain] -/// fn process(prev: SomeEntry) -> ChainProcess<ThisProgram> { +/// fn process(prev: SomeEntry) -> Next { /// let value = route!(current_dir().map_err(|e| ErrorEntry::new(e.to_string_lossy().to_string()))); /// // value is the PathBuf from current_dir() /// value.to_chain() @@ -555,7 +530,7 @@ pub fn route(input: TokenStream) -> TokenStream { let expanded = quote! { match #expr { Ok(r) => r, - Err(e) => return ::mingling::Groupped::to_chain(e), + Err(e) => return ::mingling::Routable::to_chain(e), } }; TokenStream::from(expanded) @@ -613,7 +588,7 @@ pub fn route(input: TokenStream) -> TokenStream { #[proc_macro] pub fn empty_result(_input: TokenStream) -> TokenStream { let expanded = quote! { - <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) + <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) }; TokenStream::from(expanded) } @@ -872,6 +847,11 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { /// - With the `async` feature, async functions are supported; without it, async functions are rejected. #[proc_macro_attribute] pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { + // Extension point: if attr contains extension identifiers like `routeify`, + // re-dispatch as #[ext1] #[ext2] #[chain] fn ... + if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "chain") { + return redispatch; + } chain::chain_attr(attr, item) } @@ -939,6 +919,9 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream { + if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "renderer") { + return redispatch; + } renderer::renderer_attr(attr, item) } @@ -993,6 +976,9 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(feature = "comp")] #[proc_macro_attribute] pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream { + if let Some(redispatch) = extensions::try_redispatch_completion(attr.clone(), &item) { + return redispatch; + } completion::completion_attr(attr, item) } @@ -1246,14 +1232,38 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { /// /// [`BasicProgramSetup`]: https://docs.rs/mingling/latest/mingling/setup/struct.BasicProgramSetup.html #[proc_macro_attribute] -pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream { + if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "help") { + return redispatch; + } help::help_attr(item) } -/// Derive macro for automatically implementing the `Groupped` trait on a struct. +/// Extension attribute macro that transforms `expr?` into `route!(expr)`. +/// +/// Designed for use with `#[chain(routeify, ...)]` to enable concise error +/// routing in chain functions using the `?` operator syntax. /// -/// The `#[derive(Groupped)]` macro: -/// 1. Implements `Groupped<crate::ThisProgram>`. +/// # Example +/// +/// ```rust,ignore +/// #[chain(routeify)] +/// fn handle_calc(args: EntryCalculate) -> Next { +/// let a = args.pick(&arg![f32]).to_result()?; +/// let op = args.pick(&arg![Operator]).to_result()?; +/// StateCalculate { number_a: a, operator: op, ... }.to_chain() +/// } +/// ``` +#[cfg(feature = "extra_macros")] +#[proc_macro_attribute] +pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream { + extensions::routeify::routeify_impl(attr, item) +} + +/// Derive macro for automatically implementing the `Grouped` trait on a struct. +/// +/// The `#[derive(Grouped)]` macro: +/// 1. Implements `Grouped<crate::ThisProgram>`. /// 2. Registers the type via `register_type!` so it's included in the program enum. /// 3. Generates `Into<AnyOutput<Group>>` and `Into<ChainProcess<Group>>` conversions. /// 4. Adds `to_chain()` and `to_render()` methods to the struct. @@ -1261,7 +1271,7 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { /// # Syntax /// /// ```rust,ignore -/// #[derive(Groupped)] +/// #[derive(Grouped)] /// struct MyStruct { /// // ... /// } @@ -1270,9 +1280,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { /// # Example /// /// ```rust,ignore -/// use mingling::macros::Groupped; +/// use mingling::macros::Grouped; /// -/// #[derive(Groupped)] +/// #[derive(Grouped)] /// struct Greeting { /// name: String, /// } @@ -1280,9 +1290,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { /// /// This is equivalent to using `pack!` but works with custom structs that /// have named fields. For simple wrappers, prefer `pack!`. -#[proc_macro_derive(Groupped, attributes(group))] -pub fn derive_groupped(input: TokenStream) -> TokenStream { - groupped::derive_groupped(input) +#[proc_macro_derive(Grouped, attributes(group))] +pub fn derive_grouped(input: TokenStream) -> TokenStream { + grouped::derive_grouped(input) } /// Derive macro for automatically implementing the `EnumTag` trait on an enum @@ -1351,18 +1361,18 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream { structural_data::derive_structural_data(input) } -/// Derive macro for implementing both `Groupped` and `serde::Serialize` on a struct. +/// Derive macro for implementing both `Grouped` and `serde::Serialize` on a struct. /// /// **This macro is only available with the `structural_renderer` feature.** /// -/// This is identical to `#[derive(Groupped)]` but also adds `#[derive(serde::Serialize)]` +/// This is identical to `#[derive(Grouped)]` but also adds `#[derive(serde::Serialize)]` /// to the struct, which is required for the structural renderer to serialize output /// to formats like JSON, YAML, TOML, or RON. /// /// # Syntax /// /// ```rust,ignore -/// #[derive(GrouppedSerialize)] +/// #[derive(GroupedSerialize)] /// struct Info { /// name: String, /// age: i32, @@ -1372,19 +1382,19 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream { /// # Example /// /// ```rust,ignore -/// use mingling::GrouppedSerialize; +/// use mingling::GroupedSerialize; /// use serde::Serialize; /// -/// #[derive(GrouppedSerialize)] +/// #[derive(GroupedSerialize)] /// struct Info { /// name: String, /// age: i32, /// } /// ``` #[cfg(feature = "structural_renderer")] -#[proc_macro_derive(GrouppedSerialize, attributes(group))] -pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { - groupped::derive_groupped_serialize(input) +#[proc_macro_derive(GroupedSerialize, attributes(group))] +pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { + grouped::derive_grouped_serialize(input) } /// Generates the program enum and all collected types, chains, and renderers. @@ -1436,119 +1446,20 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { /// gen_program!(); /// ``` #[proc_macro] -pub fn gen_program(_input: TokenStream) -> TokenStream { - #[cfg(feature = "comp")] - let comp_gen = quote! { - ::mingling::macros::program_comp_gen!(); - }; - - #[cfg(not(feature = "comp"))] - let comp_gen = quote! {}; - - TokenStream::from(quote! { - pub type Next = ::mingling::ChainProcess<crate::ThisProgram>; - - #comp_gen - ::mingling::macros::program_fallback_gen!(); - ::mingling::macros::program_final_gen!(); - }) +pub fn gen_program(input: TokenStream) -> TokenStream { + func::gen_program::gen_program_impl(input) } -/// Internal macro used by `gen_program!` to generate completion infrastructure. -/// -/// **This macro is only available with the `comp` feature.** -/// -/// This is an internal macro and should not be called directly by user code. -/// It generates a completion dispatcher, the `CompletionContext` type, and -/// the execution/render logic for shell completion. -/// -/// The generated module `__completion_gen` contains: -/// - A `__comp` dispatcher that routes completion requests -/// - A `__exec_completion` chain that processes `CompletionContext` into `CompletionSuggest` -/// - A `__render_completion` renderer that outputs completion suggestions -#[proc_macro] #[cfg(feature = "comp")] -pub fn program_comp_gen(_input: TokenStream) -> TokenStream { - #[cfg(feature = "async")] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub async fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Groupped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(not(feature = "async"))] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Groupped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(feature = "dispatch_tree")] - let internal_dispatcher_comp = quote! { - use __internal_completion_mod::__internal_dispatcher_comp; - }; - - #[cfg(not(feature = "dispatch_tree"))] - let internal_dispatcher_comp = quote! {}; - - let comp_dispatcher = quote! { - #[doc(hidden)] - mod __internal_completion_mod { - use ::mingling::Groupped; - ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); - ::mingling::macros::pack!( - CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) - ); - } - #internal_dispatcher_comp - use __internal_completion_mod::CompletionContext; - use __internal_completion_mod::CompletionSuggest; - pub use __internal_completion_mod::CMDCompletion; - - #fn_exec_comp - - ::mingling::macros::register_type!(CompletionContext); - - #[allow(unused)] - #[doc(hidden)] - #[::mingling::macros::renderer] - pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { - let result = ::mingling::RenderResult::default(); - let (ctx, suggest) = prev.inner; - ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest); - result - } - }; - - TokenStream::from(comp_dispatcher) +#[proc_macro] +pub fn program_comp_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_comp_gen_impl(input) } /// 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(Groupped)]`(`macro.derive_groupped.html`) +/// This macro is called internally by `pack!` and `#[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. @@ -1567,115 +1478,29 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { /// Panics if the global `PACKED_TYPES` mutex is poisoned. #[proc_macro] pub fn register_type(input: TokenStream) -> TokenStream { - let type_ident = parse_macro_input!(input as syn::Ident); - let entry_str = type_ident.to_string(); - - get_global_set(&PACKED_TYPES) - .lock() - .unwrap() - .insert(entry_str); - - TokenStream::new() + func::gen_program::register_type_impl(input) } -/// Registers a chain mapping from a previous type to a chain struct. -/// -/// This macro is called internally by `#[chain]`(macro.chain.html) and is -/// generally not needed in user code. It inserts entries into the global -/// `CHAINS` and `CHAINS_EXIST` registries. -/// -/// # Syntax -/// -/// ```rust,ignore -/// register_chain!(PreviousType, ChainStruct); -/// ``` -/// -/// The `PreviousType` is the input type of the chain step, and `ChainStruct` -/// is the generated struct that implements the `Chain` trait. #[proc_macro] pub fn register_chain(input: TokenStream) -> TokenStream { - chain::register_chain(input) + func::gen_program::register_chain_impl(input) } -/// Registers a renderer mapping from a type to a renderer struct. -/// -/// This macro is called internally by `#[renderer]`(macro.renderer.html) and is -/// generally not needed in user code. It inserts entries into the global -/// `RENDERERS`, `RENDERERS_EXIST` and (with `structural_renderer` feature) -/// `STRUCTURAL_RENDERERS` registries. -/// -/// # Syntax -/// -/// ```rust,ignore -/// register_renderer!(PreviousType, RendererStruct); -/// ``` -/// -/// The `PreviousType` is the input type of the renderer, and `RendererStruct` -/// is the generated struct that implements the `Renderer` trait. #[proc_macro] pub fn register_renderer(input: TokenStream) -> TokenStream { - renderer::register_renderer(input) + func::gen_program::register_renderer_impl(input) } -/// Internal macro used by `gen_program!` to generate fallback types. -/// -/// This macro generates the fallback wrapper types that are essential -/// for error handling in the Mingling pipeline: -/// -/// - **`ErrorRendererNotFound`** — Wraps a `String` (the name of the missing renderer). -/// Used when no matching renderer is found for a given output type. -/// - **`ErrorDispatcherNotFound`** — Wraps `Vec<String>` (the unrecognized command args). -/// Used when no matching dispatcher is found for user input. -/// - **`ResultEmpty`** — Wraps `()` (the unit type). -/// Used when the chain returns an empty result. -/// -/// Users can (and should) write `#[renderer]` functions for these types -/// to provide meaningful error messages. -/// -/// This macro is called automatically by `gen_program!` and should not -/// be called directly by user code. -/// -/// # Syntax -/// -/// ```rust,ignore -/// // Called internally by gen_program!: -/// program_fallback_gen!(); -/// ``` -/// -/// # Generated code equivalent -/// -/// ```rust,ignore -/// pack!(ErrorRendererNotFound = String); -/// pack!(ErrorDispatcherNotFound = Vec<String>); -/// pack!(ResultEmpty = ()); -/// ``` #[proc_macro] -pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { - #[cfg(feature = "structural_renderer")] - let pack_empty = quote! { - #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Groupped, Default)] - pub struct ResultEmpty; - }; - - #[cfg(not(feature = "structural_renderer"))] - let pack_empty = quote! { - #[derive(::mingling::Groupped, Default)] - pub struct ResultEmpty; - }; - - let expanded = quote! { - ::mingling::macros::pack!(ErrorRendererNotFound = String); - ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); - #pack_empty - }; - TokenStream::from(expanded) +pub fn program_fallback_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_fallback_gen_impl(input) } /// Internal macro used by `gen_program!` to generate the final program enum /// and its `ProgramCollect` implementation. /// /// This is the core code generation macro that: -/// 1. Collects all registered types (from `pack!`, `#[derive(Groupped)]`, etc.) and +/// 1. Collects all registered types (from `pack!`, `#[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 @@ -1720,434 +1545,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { /// pub fn new() -> Program<MyProgram> { Program::new() } /// } /// ``` -/// -/// # Panics -/// -// Feature detection: baked into the proc-macro binary at compile time -#[cfg(feature = "async")] -const ASYNC_ENABLED: bool = true; -#[cfg(not(feature = "async"))] -const ASYNC_ENABLED: bool = false; - -/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. -fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { - let s = entry.to_string(); - let arrow_idx = s - .find("=>") - .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); - let struct_str = s[..arrow_idx].trim(); - let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); - let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); - let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); - (struct_ident, variant_ident) -} - -/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. -/// Always compiled; returns empty map when pathf feature is not enabled. -fn load_pathf_map() -> std::collections::HashMap<String, String> { - if !cfg!(feature = "pathf") { - return std::collections::HashMap::new(); - } - let out_dir = std::env::var("OUT_DIR").ok(); - let crate_name = std::env::var("CARGO_PKG_NAME").ok(); - match (out_dir, crate_name) { - (Some(dir), Some(name)) => { - let path = std::path::Path::new(&dir).join(&name).join("type_using.rs"); - match std::fs::read_to_string(&path) { - Ok(content) => content - .lines() - .filter_map(|line| { - let line = line.trim(); - if let Some(rest) = line.strip_prefix("use ") { - let path = rest.strip_suffix(';').unwrap_or(rest); - if let Some((_mod, type_name)) = path.rsplit_once("::") { - return Some((type_name.to_string(), path.to_string())); - } - } - None - }) - .collect(), - Err(_) => std::collections::HashMap::new(), - } - } - _ => std::collections::HashMap::new(), - } -} - -/// Resolves a type name to its full path token stream using the pathf mapping. -pub(crate) fn resolve_type( - name: &str, - map: &std::collections::HashMap<String, String>, -) -> proc_macro2::TokenStream { - if let Some(full_path) = map.get(name) { - syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - }) - } else { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - } -} - -/// Panics if any of the global registries (`PACKED_TYPES`, `RENDERERS`, `CHAINS`, etc.) -/// are poisoned. #[proc_macro] -#[allow(clippy::too_many_lines)] -pub fn program_final_gen(_input: TokenStream) -> TokenStream { - let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); - - let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); - - let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); - let chains = get_global_set(&CHAINS).lock().unwrap().clone(); - let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); - let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); - - #[cfg(feature = "structural_renderer")] - let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clone(); - - #[cfg(feature = "comp")] - let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); - - let packed_types: Vec<proc_macro2::TokenStream> = packed_types - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let chain_tokens: Vec<proc_macro2::TokenStream> = chains - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let pathf_map: std::collections::HashMap<String, String> = if cfg!(feature = "pathf") { - load_pathf_map() - } else { - std::collections::HashMap::new() - }; - - let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") { - pathf_map - .values() - .map(|path| format!("use {};", path).parse().unwrap_or_default()) - .collect() - } else { - Vec::new() - }; - - #[cfg(feature = "structural_renderer")] - let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - #[cfg(feature = "structural_renderer")] - let structural_render = quote! { - fn structural_render( - any: ::mingling::AnyOutput<Self::Enum>, - setting: &::mingling::StructuralRendererSetting, - ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#structural_renderer_tokens)* - _ => { - // Non-structural types: render ResultEmpty (which implements - // StructuralData + Serialize) instead of producing nothing. - let mut r = ::mingling::RenderResult::default(); - ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; - Ok(r) - } - } - } - }; - - #[cfg(not(feature = "structural_renderer"))] - let structural_render = quote! {}; - - #[cfg(feature = "dispatch_tree")] - let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clone() - .iter() - .cloned() - .collect(); - - #[cfg(feature = "dispatch_tree")] - let dispatch_tree_nodes = { - let entries: Vec<(String, String, String)> = compile_time_dispatchers - .iter() - .filter_map(|entry| { - let parts: Vec<&str> = entry.split(':').collect(); - if parts.len() == 3 { - Some(( - parts[0].to_string(), - parts[1].to_string(), - parts[2].to_string(), - )) - } else { - None - } - }) - .collect(); - - let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); - let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); - - quote! { - #get_nodes_fn - #dispatch_trie_fn - } - }; - - #[cfg(not(feature = "dispatch_tree"))] - let dispatch_tree_nodes = quote! {}; - - #[cfg(feature = "comp")] - let completion_tokens: Vec<proc_macro2::TokenStream> = completions - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - #[cfg(feature = "comp")] - let comp = quote! { - fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#completion_tokens)* - _ => ::mingling::Suggest::FileCompletion, - } - } - }; - - #[cfg(not(feature = "comp"))] - let comp = quote! {}; - - // Build render function arms from stored entries - let render_fn = - if renderer_tokens.is_empty() { - quote! { - fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - ::mingling::RenderResult::default() - } - } - } else { - let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Renderer>::render(value) - } - } - }).collect(); - quote! { - fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - match any.member_id { - #(#render_arms)* - _ => ::mingling::RenderResult::default(), - } - } - } - }; - - // Build do_chain function (async and sync versions) - let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await }; - ::std::boxed::Box::pin(fut) - } - } - }).collect(); - - let chain_arms_sync: Vec<_> = chain_tokens - .iter() - .map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value) - } - } - }) - .collect(); - - let do_chain_fn = if chain_tokens.is_empty() { - quote! { - fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { - ::core::panic!("No chain found for type id") - } - } - } else if ASYNC_ENABLED { - quote! { - fn do_chain( - any: ::mingling::AnyOutput<Self::Enum>, - ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { - match any.member_id { - #(#chain_arms_async)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), - } - } - } - } else { - quote! { - fn do_chain( - any: ::mingling::AnyOutput<Self::Enum>, - ) -> ::mingling::ChainProcess<Self::Enum> { - match any.member_id { - #(#chain_arms_sync)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), - } - } - } - }; - - let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS) - .lock() - .unwrap() - .clone() - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let num_variants = packed_types.len(); - let repr_type = if u8::try_from(num_variants).is_ok() { - quote! { u8 } - } else if u16::try_from(num_variants).is_ok() { - quote! { u16 } - } else if u32::try_from(num_variants).is_ok() { - quote! { u32 } - } else { - quote! { u128 } - }; - - let expanded = quote! { - #[derive(Debug, PartialEq, Eq, Clone)] - #[repr(#repr_type)] - #[allow(nonstandard_style)] - pub enum #name { - #(#packed_types),* - } - - impl ::std::fmt::Display for #name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match self { - #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* - } - } - } - - impl ::mingling::ProgramCollect for #name { - type Enum = #name; - type ErrorDispatcherNotFound = ErrorDispatcherNotFound; - type ErrorRendererNotFound = ErrorRendererNotFound; - 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())) - } - fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) - } - fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ResultEmpty) - } - #render_fn - #do_chain_fn - fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#help_tokens)* - _ => ::mingling::RenderResult::default(), - } - } - fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { - #(#renderer_exist_tokens)* - _ => false - } - } - fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { - #(#chain_exist_tokens)* - _ => false - } - } - #dispatch_tree_nodes - #structural_render - #comp - } - - impl #name { - /// Creates a new `Program<#name>` instance with default configuration. - pub fn new() -> ::mingling::Program<#name> { - ::mingling::Program::new() - } - - /// Returns a static reference to the global `Program<#name>` singleton. - pub fn this() -> &'static ::mingling::Program<#name> { - &::mingling::this::<#name>() - } - } - }; - - // Clear all global registries to prevent stale state in Rust Analyzer - get_global_set(&PACKED_TYPES).lock().unwrap().clear(); - get_global_set(&CHAINS).lock().unwrap().clear(); - get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); - get_global_set(&RENDERERS).lock().unwrap().clear(); - get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); - get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); - #[cfg(feature = "comp")] - get_global_set(&COMPLETIONS).lock().unwrap().clear(); - #[cfg(feature = "dispatch_tree")] - get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clear(); - #[cfg(feature = "structural_renderer")] - get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clear(); - - TokenStream::from(expanded) +pub fn program_final_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_final_gen_impl(input) } /// Builds a `Suggest` instance with inline suggestion items. diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs new file mode 100644 index 0000000..53e58c5 --- /dev/null +++ b/mingling_macros/src/systems.rs @@ -0,0 +1,5 @@ +#[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/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs index b66e2f4..7383421 100644 --- a/mingling_macros/src/dispatch_tree_gen.rs +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -4,11 +4,11 @@ use just_fmt::snake_case; use proc_macro2::TokenStream; use quote::quote; -use crate::resolve_type; +use crate::func::gen_program::resolve_type; /// Generate the `get_nodes()` function body for a ProgramCollect impl. /// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths. -pub fn gen_get_nodes( +pub(crate) fn gen_get_nodes( entries: &[(String, String, String)], pathf_map: &HashMap<String, String>, ) -> TokenStream { @@ -40,7 +40,7 @@ pub fn gen_get_nodes( /// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match. /// /// If `pathf_map` is non-empty, resolves dispatcher types using full paths. -pub fn gen_dispatch_args_trie( +pub(crate) fn gen_dispatch_args_trie( entries: &[(String, String, String)], pathf_map: &HashMap<String, String>, ) -> TokenStream { diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/systems/res_injection.rs index 606b9a6..606b9a6 100644 --- a/mingling_macros/src/res_injection.rs +++ b/mingling_macros/src/systems/res_injection.rs diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/systems/structural_data.rs index 5350d7e..74bcf09 100644 --- a/mingling_macros/src/structural_data.rs +++ b/mingling_macros/src/systems/structural_data.rs @@ -176,7 +176,7 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { } } - impl ::mingling::Groupped<#program_path> for #type_name { + impl ::mingling::Grouped<#program_path> for #type_name { fn member_id() -> #program_path { #program_path::#type_name } @@ -297,7 +297,7 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream { #type_use #alias_use - impl ::mingling::Groupped<__MinglingProgram> for #type_name { + impl ::mingling::Grouped<__MinglingProgram> for #type_name { fn member_id() -> __MinglingProgram { __MinglingProgram::#type_name } diff --git a/mingling_macros/src/utils.rs b/mingling_macros/src/utils.rs new file mode 100644 index 0000000..49d0e7a --- /dev/null +++ b/mingling_macros/src/utils.rs @@ -0,0 +1 @@ +// Shared utilities for the macro crate. diff --git a/mingling_pathf/README.md b/mingling_pathf/README.md index 89d1811..9706f49 100644 --- a/mingling_pathf/README.md +++ b/mingling_pathf/README.md @@ -7,7 +7,7 @@ ## Overview -`mingling_pathf` provides the `pathf` feature for Mingling. It automatically analyzes the full module paths of all Mingling types in a crate at build-time (types defined via `pack!`, `#[derive(Groupped)]`, `#[chain]`, `#[renderer]`, etc.), and generates a mapping from type names to module paths for consumption by `gen_program!()` at compile-time. +`mingling_pathf` provides the `pathf` feature for Mingling. It automatically analyzes the full module paths of all Mingling types in a crate at build-time (types defined via `pack!`, `#[derive(Grouped)]`, `#[chain]`, `#[renderer]`, etc.), and generates a mapping from type names to module paths for consumption by `gen_program!()` at compile-time. When enabled, `gen_program!()` uses full module paths for type references in the generated dispatch code (e.g., `downcast::<myapp::sub::ResultMyName>()`), eliminating the need to `use` all types in the module where `gen_program!()` is called. This allows for a more flexible module organization without the constraint of centralized `use` statements. diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index 3765971..5bbc3b4 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -31,7 +31,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer { analyzer.add_pattern(BasicStructPattern); analyzer.add_pattern(PackPattern); analyzer.add_pattern(GroupPattern); - analyzer.add_pattern(GrouppedDerivePattern); + analyzer.add_pattern(GroupedDerivePattern); analyzer.add_pattern(ChainPattern); analyzer.add_pattern(RendererPattern); analyzer.add_pattern(HelpPattern); diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs index 9801e9b..9845b73 100644 --- a/mingling_pathf/src/patterns.rs +++ b/mingling_pathf/src/patterns.rs @@ -6,7 +6,7 @@ pub use completion::*; pub use dispatcher::*; pub use dispatcher_clap::*; pub use group::*; -pub use groupped_derive::*; +pub use grouped_derive::*; pub use help::*; pub use pack::*; pub use renderer::*; @@ -17,7 +17,7 @@ mod completion; mod dispatcher; mod dispatcher_clap; mod group; -mod groupped_derive; +mod grouped_derive; mod help; mod pack; mod renderer; diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/grouped_derive.rs index 91daaef..9522c1f 100644 --- a/mingling_pathf/src/patterns/groupped_derive.rs +++ b/mingling_pathf/src/patterns/grouped_derive.rs @@ -1,5 +1,5 @@ -//! The `GrouppedDerivePattern` matches structs, enums, and unions annotated with -//! `#[derive(Groupped)]` or `#[derive(GrouppedSerialize)]` (or any combination +//! The `GroupedDerivePattern` matches structs, enums, and unions annotated with +//! `#[derive(Grouped)]` or `#[derive(GroupedSerialize)]` (or any combination //! with other derives). It also recurses into `mod` items to find nested types. //! This is used to track grouped items for code generation or analysis. @@ -7,17 +7,17 @@ use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; -/// Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]`. +/// Matches `#[derive(Grouped)]` and `#[derive(GroupedSerialize)]`. /// /// Covers the forms: -/// - `#[derive(Groupped)] struct T { ... }` -/// - `#[derive(Groupped, Serialize, ...)] struct T { ... }` -/// - `#[derive(GrouppedSerialize)] struct T { ... }` -pub struct GrouppedDerivePattern; +/// - `#[derive(Grouped)] struct T { ... }` +/// - `#[derive(Grouped, Serialize, ...)] struct T { ... }` +/// - `#[derive(GroupedSerialize)] struct T { ... }` +pub struct GroupedDerivePattern; -impl AnalyzePattern for GrouppedDerivePattern { +impl AnalyzePattern for GroupedDerivePattern { fn contains(&self, content: &str) -> bool { - content.contains("Groupped") + content.contains("Grouped") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -29,19 +29,19 @@ impl AnalyzePattern for GrouppedDerivePattern { for item in &syntax.items { match item { - Item::Struct(s) if has_groupped_derive(&s.attrs) => { + Item::Struct(s) if has_grouped_derive(&s.attrs) => { items.push(AnalyzeItem { module: String::new(), item_name: s.ident.to_string(), }); } - Item::Enum(e) if has_groupped_derive(&e.attrs) => { + Item::Enum(e) if has_grouped_derive(&e.attrs) => { items.push(AnalyzeItem { module: String::new(), item_name: e.ident.to_string(), }); } - Item::Union(u) if has_groupped_derive(&u.attrs) => { + Item::Union(u) if has_grouped_derive(&u.attrs) => { items.push(AnalyzeItem { module: String::new(), item_name: u.ident.to_string(), @@ -51,13 +51,13 @@ impl AnalyzePattern for GrouppedDerivePattern { if let Some((_, nested)) = &item_mod.content { for n in nested { match n { - Item::Struct(s) if has_groupped_derive(&s.attrs) => { + Item::Struct(s) if has_grouped_derive(&s.attrs) => { items.push(AnalyzeItem { module: item_mod.ident.to_string(), item_name: s.ident.to_string(), }); } - Item::Enum(e) if has_groupped_derive(&e.attrs) => { + Item::Enum(e) if has_grouped_derive(&e.attrs) => { items.push(AnalyzeItem { module: item_mod.ident.to_string(), item_name: e.ident.to_string(), @@ -76,10 +76,10 @@ impl AnalyzePattern for GrouppedDerivePattern { } } -fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool { +fn has_grouped_derive(attrs: &[syn::Attribute]) -> bool { attrs.iter().any(|attr| { if attr.path().is_ident("derive") { - // Correctly parse comma-separated paths in #[derive(Groupped, Debug, ...)] + // Correctly parse comma-separated paths in #[derive(Grouped, Debug, ...)] attr.parse_args_with(|input: syn::parse::ParseStream| { let paths = syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated( @@ -87,7 +87,7 @@ fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool { )?; Ok(paths.iter().any(|p| { let name = p.segments.last().unwrap().ident.to_string(); - name == "Groupped" || name == "GrouppedSerialize" + name == "Grouped" || name == "GroupedSerialize" })) }) .unwrap_or(false) diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs index 824cbbf..7e7cbd5 100644 --- a/mingling_pathf/test/src/lib.rs +++ b/mingling_pathf/test/src/lib.rs @@ -222,11 +222,11 @@ fn test_group_analyze() { } #[test] -fn test_groupped_derive_analyze() { +fn test_grouped_derive_analyze() { let analyzer = mingling_pathf::pattern_analyzer::init(); let file = current_dir() .unwrap() - .join("src/test_files/test_groupped_derive.rs"); + .join("src/test_files/test_grouped_derive.rs"); let r = analyzer.analyze_file(file).unwrap(); let required: Vec<&str> = vec![ diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_grouped_derive.rs index 913587c..20055e9 100644 --- a/mingling_pathf/test/src/test_files/test_groupped_derive.rs +++ b/mingling_pathf/test/src/test_files/test_grouped_derive.rs @@ -1,42 +1,42 @@ -#[derive(Groupped)] +#[derive(Grouped)] struct Derived1 { value: String, } -#[derive(Groupped, Debug, Clone)] +#[derive(Grouped, Debug, Clone)] struct Derived2 { value: i32, } -#[derive(GrouppedSerialize)] +#[derive(GroupedSerialize)] struct Derived3 { value: bool, } -#[derive(Groupped)] +#[derive(Grouped)] enum EnumDerived1 { A, B, } -#[derive(GrouppedSerialize)] +#[derive(GroupedSerialize)] enum EnumDerived2 { X(String), Y(i32), } pub mod sub { - #[derive(Groupped)] + #[derive(Grouped)] struct Derived1 { value: String, } - #[derive(GrouppedSerialize)] + #[derive(GroupedSerialize)] struct Derived3 { value: bool, } - #[derive(Groupped)] + #[derive(Grouped)] enum EnumDerived1 { A, } diff --git a/mling/src/cli.rs b/mling/src/cli.rs index 45a49d0..69e4fe9 100644 --- a/mling/src/cli.rs +++ b/mling/src/cli.rs @@ -11,7 +11,7 @@ use crate::{ }; use colored::Colorize; use mingling::{ - Groupped, Program, RenderResult, + Grouped, Program, RenderResult, hook::ProgramHook, macros::{chain, help, pack, program_setup, renderer}, res::ResExitCode, diff --git a/mling/src/proj_mgr/generator.rs b/mling/src/proj_mgr/generator.rs index 4f965d9..f0dbdd7 100644 --- a/mling/src/proj_mgr/generator.rs +++ b/mling/src/proj_mgr/generator.rs @@ -1,7 +1,7 @@ use std::path::{self, PathBuf}; use mingling::{ - Groupped, RenderResult, + Grouped, RenderResult, macros::{chain, pack, renderer, route}, }; use std::io::Write as _; diff --git a/mling/src/proj_mgr/show_binaries.rs b/mling/src/proj_mgr/show_binaries.rs index 9d5caf0..4fb5c28 100644 --- a/mling/src/proj_mgr/show_binaries.rs +++ b/mling/src/proj_mgr/show_binaries.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use colored::Colorize; use mingling::{ - Groupped, RenderResult, + Grouped, RenderResult, macros::{chain, pack, renderer}, }; use serde::Serialize; @@ -17,7 +17,7 @@ use crate::{ res::ResManifestPath, }; -#[derive(Serialize, Groupped)] +#[derive(Serialize, Grouped)] pub struct ResultBinaries { pub binaries: Vec<DataBinary>, } diff --git a/mling/src/proj_mgr/show_directories.rs b/mling/src/proj_mgr/show_directories.rs index 7d7c074..5c5f448 100644 --- a/mling/src/proj_mgr/show_directories.rs +++ b/mling/src/proj_mgr/show_directories.rs @@ -1,6 +1,6 @@ use colored::Colorize; use mingling::{ - Groupped, RenderResult, + Grouped, RenderResult, macros::{chain, pack, renderer}, }; use serde::Serialize; @@ -12,12 +12,12 @@ use crate::{ res::ResManifestPath, }; -#[derive(Serialize, Groupped)] +#[derive(Serialize, Grouped)] pub struct ResultWorkspaceDirectory { pub path: String, } -#[derive(Serialize, Groupped)] +#[derive(Serialize, Grouped)] pub struct ResultTargetDirectory { pub path: String, } |
