diff options
62 files changed, 1861 insertions, 1056 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4b737..997eac0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,13 @@ 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`. #### Features: @@ -184,6 +190,33 @@ None These methods complement the existing read-only `get_args(&self)` method, providing full control over argument mutation and ownership. +9. **[`macros:chain`]** Relaxed the `#[chain]` return type validation. Previously, `#[chain]` functions were restricted to returning `Next`, `ChainProcess<ThisProgram>`, `()`, or omitting the return type. Now, any return type is accepted, and the generated `proc` function performs an explicit `Into<ChainProcess<ThisProgram>>` conversion using a fully-qualified turbofish based on the user-declared return type. + + This means `#[chain]` functions can now return any pack type directly, without needing an explicit `.into()` call in the function body: + + ```rust + // Before — required explicit .into() + #[chain] + fn handle_greet(args: EntryGreet) -> Next { + let name = /* ... */; + ResultGreeting::new(name).into() + } + + // After — return any pack type directly + #[chain] + fn handle_greet(args: EntryGreet) -> ResultGreeting { + let name = /* ... */; + ResultGreeting::new(name) + } + ``` + + 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(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. + #### **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. @@ -236,6 +269,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 new file mode 100644 index 0000000..e08d17b --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,775 @@ +# Writing with Mingling + +## The Big Picture + +Mingling organizes your CLI program into three distinct phases: + +``` +Input → [Dispatcher] → Entry → [Chain(s)] → Result → [Renderer] → Output +``` + +**Step 1: Input** — The user's raw arguments flow in. +**Step 2: Dispatch** — The **Dispatcher** receives them and wraps them into an **Entry** type. +**Step 3: Chain** — The Entry is handed to a **Chain** function for processing. +**Step 4: Render** — The **Renderer** receives the result and writes it to the terminal. + +> [!NOTE] +> A Chain can produce a **State** type, passed to the next Chain for further processing, +> +> or it can produce a **Result** type, handed to the Renderer for output. + +Every step in this pipeline is a **pure function** annotated with an attribute macro, and the framework recognizes them as long as they meet the requirements. + +--- + +## 1. Defining Commands — `dispatcher!` + +The entry point for every subcommand is the `dispatcher!` macro. It generates two structs for you: a **Dispatcher** (used to register the command with the program) and an **Entry** (a wrapper around `Vec<String>` that holds the raw arguments). + +```rust +use mingling::prelude::*; + +// command.name Dispatcher EntryType +// │ │ │ +dispatcher!("greet", CMDGreet => EntryGreet); + +// Nested subcommand: `remote add` +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +``` + +Then in `main()`, register the dispatcher with the program: + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +Mingling also supports an abbreviated form (with the `extra_macros` feature): + +```rust +// Features: ["extra_macros"] + +// Auto-generates CMDGreet / EntryGreet from "greet" +dispatcher!("greet"); +``` + +--- + +## 2. The Chain — "#[chain]" — Where Logic Lives + +The `#[chain]` attribute turns a plain function into an execution step. Think of it as "the logic that transforms one typed value into another." + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +pack!(ResultGreeting = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let greeting = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(greeting).into() +} +``` + +Key points: + +- The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`. +- You chain results by calling `.to_chain()` on any `pack!`-ed type. +- You can have **multiple chain functions** for the same command, each transforming the data further. +- With the `async` feature, chain functions can be `async fn`. + +--- + +## 3. The Renderer — "#[renderer]" — How Output Works + +The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`. + +```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + +pack!(ResultGreeting = String); + +#[renderer] +fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result +} +``` + +Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline. + +You can write renderers for **any type** in your program, including error types: + +```rust +use mingling::prelude::*; +use std::io::Write; + +#[renderer] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Command not found: [{}]", err.join(" ")).ok(); + result +} +``` + +--- + +## 4. Parsing Arguments — The Picker + +Mingling provides a **Picker** for zero-cost argument extraction. You use `pick()` or `pick_or()` on an entry to extract typed values, then `unpack()` to get the final tuple. + +```rust +// Features: ["parser"] + +use mingling::parser::Picker; + +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let (name, count) = Picker::new(args.inner) + .pick::<String>(()) // positional: first string + .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default + .unpack(); + ResultGreeting::new(format!("{} x{}", name, count)).into() +} +``` + +With the `parser` feature, the `AsPicker` trait provides a shorthand directly on entries: + +```rust +// Features: ["parser"] + +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); + +#[chain] +fn handle(args: EntryGreet) -> Next { + let (name, count) = args + .pick::<Option<String>>(()) + .pick_or::<u8>(["-r", "--repeat"], 1) + .unpack(); + ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)).into() +} +``` + +For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants from strings: + +```rust +// Features: ["parser", "extra_macros"] + +use mingling::{EnumTag, Grouped}; +use mingling::parser::PickableEnum; + +dispatcher!("lang.select", CMDLang => EntryLang); + +#[derive(Debug, Default, EnumTag, Grouped)] +pub enum Language { + #[default] + Rust, + #[enum_rename("C++")] + CPlusPlus, +} + +impl PickableEnum for Language {} + +#[chain] +fn handle(args: EntryLang) -> Next { + let lang: Language = args.pick(()).unpack(); + lang.into() +} +``` + +--- + +## 5. The Help System — "#[help]" + +Help is just another attribute macro. When the user passes `--help` or `-h`, the program skips the normal chain/render pipeline and routes directly to your `#[help]` function. + +Enable it by adding `BasicProgramSetup`: + +```rust +use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; +use std::io::Write; + +dispatcher!("greet", CMDGreet => EntryGreet); + +#[help] +fn help_greet(_prev: EntryGreet) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Usage: greet <NAME>").ok(); + writeln!(result, "Greets the user with the given name.").ok(); + result +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); // enables --help / -h + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +The flow is: + +- User types `greet --help` +- `BasicProgramSetup` sets `program.user_context.help = true` +- The dispatcher sees this flag and routes to the `#[help]` function instead of the `#[chain]` + +--- + +## 6. Completion — "#[completion]" — Dynamic Shell Completions + +With the `comp` feature, Mingling provides a fully dynamic completion system. You write a function that returns `Suggest` based on the current shell context, and Mingling generates the completion scripts for bash, zsh, fish, and pwsh. + +```rust +// Features: ["comp", "extra_macros"] + +use mingling::{macros::suggest, ShellContext, Suggest}; + +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = (u8, String)); + +#[completion(EntryGreet)] +fn complete_greet(ctx: &ShellContext) -> Suggest { + // Suggest positional arguments + if ctx.previous_word == "greet" { + return suggest! { + "Alice": "Likes to receive messages", + "Bob": "Likes to pass messages", + "World" + }; + } + + // Suggest flag arguments + if ctx.typing_argument() { + return suggest! { + "-r": "Number of repetitions", + "--repeat": "Number of repetitions", + } + .strip_typed_argument(ctx); + } + + suggest!() // no suggestions +} +``` + +You also need to register the built-in completion dispatcher: + +```rust +// Features: ["comp"] + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(crate::CMDCompletion); + program.exec_and_exit(); +} +``` + +In your `build.rs`, generate the shell scripts: + +```rust +// BUILD TIME +// Features: ["comp", "builds"] +mingling::build::build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap(); +``` + +For enum-based completions, use `suggest_enum!`: + +```rust +// Features: ["comp", "extra_macros"] + +use mingling::{ShellContext, Suggest}; +use mingling::macros::suggest_enum; +use mingling::EnumTag; + +dispatcher!("lang.select", CMDLang => EntryLang); + +#[derive(EnumTag)] +pub enum ProgrammingLanguages { + Rust, + Python, + JavaScript, +} + +#[completion(EntryLang)] +fn complete_lang(_: &ShellContext) -> Suggest { + suggest_enum!(ProgrammingLanguages) +} +``` + +--- + +## 7. Error Handling + +Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`: + +```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + +dispatcher!("hello", CMDHello => EntryHello); +pack!(ResultName = String); +pack!(ErrorNoNameProvided = ()); +pack!(ErrorNameTooLong = u16); + +#[chain] +fn handle(args: EntryHello) -> Next { + let Some(name) = args.inner.first().cloned() else { + return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer + }; + + if name.len() > 10 { + return ErrorNameTooLong::new(name.len() as u16).to_render(); + } + + ResultName::new(name).to_render() // ← success path +} + +#[renderer] +fn render_no_name(_: ErrorNoNameProvided) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "No name provided").ok(); + result +} + +#[renderer] +fn render_too_long(len: ErrorNameTooLong) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Name too long: {} > 10", *len).ok(); + result +} +``` + +Two built-in fallback types are always available: + +- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input +- `ErrorRendererNotFound` — rendered when no renderer is found for a result type + +--- + +## 8. Resource Injection + +Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`. + +```rust +// Features: ["parser", "extra_macros"] + +use std::path::PathBuf; + +dispatcher!("current", CMDCurrent => EntryCurrent); +dispatcher!("cd", CMDCd => EntryCd); + +#[derive(Default, Clone)] +struct ResCurrentDir { + current_dir: PathBuf, +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResCurrentDir { + current_dir: std::env::current_dir().unwrap(), + }); + program.with_dispatcher(CMDCurrent); + program.with_dispatcher(CMDCd); + program.exec_and_exit(); +} + +// Read-only access (shared reference): +#[chain] +fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next { + println!("Current: {}", current_dir.current_dir.display()); + empty_result!() +} + +// Mutable access: +#[chain] +fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next { + let path: String = prev.pick(()).unpack(); + current_dir.current_dir = current_dir.current_dir.join(path); + empty_result!() +} +``` + +Resources can also be injected into `#[renderer]`: + +```rust +use mingling::prelude::*; +use std::io::Write; + +dispatcher!("current", CMDCurrent => EntryCurrent); + +#[derive(Default, Clone)] +struct ResCurrentDir { + current_dir: std::path::PathBuf, +} + +#[renderer] +fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok(); + result +} +``` + +--- + +## 9. Dispatch Tree — Compile-Time Command Trie + +As your program grows to dozens or hundreds of subcommands, linear dispatcher lookup becomes slow. Enable the `dispatch_tree` feature to convert the command structure into a **prefix tree (Trie)** at compile time. + +```rust +// Features: ["dispatch_tree"] + +dispatcher!("cmd1", CMD1 => Entry1); +dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1); +dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2); +dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1); +dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2); +// ... dozens more + +fn main() { + let program = ThisProgram::new(); + // No more with_dispatcher calls — it's all compile-time! + program.exec_and_exit(); +} +``` + +With `dispatch_tree` enabled: + +- Dispatchers are auto-collected at compile time +- `Program` no longer stores a dispatcher list +- `program.with_dispatcher(...)` is not compiled +- Lookup is **O(n)** where _n_ is input length, not number of commands + +--- + +## 10. Clap Binding — Using Clap's Parser + +If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It generates a dispatcher from a `clap::Parser` struct. + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" + +use mingling::macros::dispatcher_clap; +use mingling::prelude::*; +use std::io::Write; + +#[derive(Default, clap::Parser, Grouped)] +#[dispatcher_clap( + "greet", CMDGreet, + help = true, // auto-generate #[help] from clap + error = ErrorGreetParsed, // capture parse errors as a renderable type +)] +pub struct EntryGreet { + #[clap(default_value = "World")] + name: String, + + #[arg(short, long, default_value_t = 1)] + repeat: i32, +} + +#[renderer] +fn render_greet(greet: EntryGreet) -> RenderResult { + let mut result = RenderResult::new(); + write!(result, "Hello, ").ok(); + for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); } + writeln!(result, "!").ok(); + result +} + +#[renderer] +fn render_parse_error(err: ErrorGreetParsed) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{}", *err).ok(); + result +} +``` + +You can control how clap help is displayed: + +```rust +// Features: ["clap"] + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.stdout_setting.clap_help_print_behaviour = + mingling::ClapHelpPrintBehaviour::WriteToRenderResult; + // or: PrintDirectly — writes clap help straight to stdout + program.exec_and_exit(); +} +``` + +--- + +## 11. REPL Mode + +With the `repl` feature, turn your CLI into an interactive shell with one method call: + +```rust +// Features: ["repl"] + +fn main() { + ThisProgram::new().exec_repl(); +} +``` + +Mingling provides built-in REPL setups: + +```rust +// Features: ["repl", "extra_macros"] + +use mingling::{ + res::ResREPL, + setup::{BasicREPLReadlineSetup, BasicREPLOutputSetup, BasicREPLPromptSetup}, +}; + +dispatcher!("cd", CMDCd => EntryCd); +dispatcher!("exit", CMDExit => EntryExit); + +fn main() { + let mut program = ThisProgram::new(); + + program.with_dispatcher(CMDCd); + program.with_dispatcher(CMDExit); + + // Enable line reading from stdin + program.with_setup(BasicREPLReadlineSetup); + + // Enable output flushing after each render + program.with_setup(BasicREPLOutputSetup); + + // Custom prompt + program.with_setup(BasicREPLPromptSetup::func(|| "> ".to_string())); + + program.exec_repl(); // ← interactive loop +} + +// Exit the REPL via the ResREPL resource: +#[chain] +fn handle_exit(_prev: EntryExit, repl: &mut ResREPL) { + repl.exit = true; +} +``` + +--- + +## 12. Hooks — Observing the Pipeline + +Mingling provides a `ProgramHook` system for observing every stage of the execution pipeline. Useful for debugging, logging, or telemetry. + +```rust +use mingling::{ + hook::{ProgramControlUnit, ProgramHook}, +}; + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + + program.with_hook( + ProgramHook::<ThisProgram>::empty() + .on_begin::<_, ()>(|_| println!("[DEBUG] Program is begin")) + .on_pre_dispatch(|info| println!("[DEBUG] Pre dispatch: {}", info.arguments.join(" "))) + .on_post_dispatch(|info| println!("[DEBUG] Post dispatch: {}", info.entry)) + .on_pre_chain(|info| { + println!("[DEBUG] Pre chain: {}", info.input); + }) + .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) + .on_finish(|_| { + println!("[DEBUG] Loop end"); + ProgramControlUnit::OverrideExitCode(0) // Override exit code + }) + .on_pre_render(|info| println!("[DEBUG] Pre render: {}", info.input)) + .on_post_render(|_| println!("[DEBUG] Post render")), + ); + + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +--- + +## 13. Structural Renderer — Structured Output (JSON/YAML) + +With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text. + +```rust +// Features: ["structural_renderer", "parser"] +// Dependencies: +// serde = "1" + +use mingling::{prelude::*, setup::StructuralRendererSetup}; +use mingling::Grouped; +use mingling::StructuralData; +use serde::Serialize; +use std::io::Write; + +dispatcher!("render", CMDRender => EntryRender); + +#[derive(Default, StructuralData, Serialize, Grouped)] +struct ResultInfo { + name: String, + age: i32, +} + +#[chain] +fn render_info(args: EntryRender) -> Next { + let (name, age) = args.pick::<String>(()).pick::<i32>(()).unpack(); + ResultInfo { name, age }.to_chain() +} + +#[renderer] +fn render_info_result(info: ResultInfo) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{} is {} years old", info.name, info.age).ok(); + result +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(StructuralRendererSetup); // enables --json / --yaml + program.with_dispatcher(CMDRender); + let _ = program.exec(); +} +``` + +Then users can do: + +```bash +$ myapp render Bob 22 +Bob is 22 years old + +$ myapp render Bob 22 --json +{"name":"Bob","age":22} + +$ myapp render Bob 22 --yaml +name: Bob +age: 22 +``` + +--- + +## 14. Async Support + +Enable the `async` feature to use `async fn` inside `#[chain]`: + +```rust +// Features: ["async", "parser"] +// Dependencies: +// tokio = { version = "1", features = ["full"] } + +use std::io::Write; +use std::time::Duration; + +dispatcher!("download", CMDDownload => EntryDownload); +pack!(ResultDownloaded = String); + +#[chain] +pub async fn handle_download(args: EntryDownload) -> Next { + let file = args.pick(()).unpack(); + download_file(file).await.into() +} + +async fn download_file(name: String) -> ResultDownloaded { + tokio::time::sleep(Duration::from_secs(1)).await; + ResultDownloaded::new(name) +} + +#[renderer] +fn render_downloaded(result: ResultDownloaded) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "\"{}\" downloaded.", *result).ok(); + r +} +``` + +> [!NOTE] +> +> `#[renderer]` functions cannot be async. When `async` is enabled, `program.exec_and_exit().await` returns a Future. + +--- + +## 15. Wrapping Up — `gen_program!()` + +At the very end of your crate root (main.rs / lib.rs), call `gen_program!()` to generate the `ThisProgram` struct, the `Next` type alias, and all internal plumbing. + +```rust +use mingling::macros::gen_program; + +gen_program!(); +``` + +It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations. + +--- + +## Putting It All Together + +Here's a complete, runnable program: + +```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +pack!(ResultGreeting = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let greeting = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(greeting).into() +} + +#[renderer] +fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result +} + +gen_program!(); +``` + +```bash +$ myapp greet +Hello, World! + +$ myapp greet Alice +Hello, Alice! +``` @@ -1,6 +1,6 @@ <p align="center"> <a href="https://github.com/mingling-rs/mingling"> - <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon2.png" width="50%"> + <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon2.png" width="30%"> </a> </p> <h1 align="center">Mìng Lìng - 命令</h1> @@ -21,852 +21,100 @@ <img src="https://img.shields.io/github/actions/workflow/status/mingling-rs/mingling/ci.yml"> </p> -> [!WARNING] -> -> **Note**: Mingling is still under active development, and its API may change. Feel free to try it out and give us feedback! -> **Hint**: This note will be removed in version `0.5.0` +## What is Mingling? -<h1 align="center"> - What is Mingling? -</h1> +[`Mingling`](https://github.com/mingling-rs/mingling) is a **state-driven and data-driven** CLI workflow orchestration framework built in Rust. -[`Mingling`](https://github.com/mingling-rs/mingling) is a **proc-macro and type-system based** Rust CLI framework, suitable for developing complex command-line programs with numerous subcommands. +💡 Its name comes from the Chinese pinyin **"Mìng Lìng"**, which means **"command"**. -> Its name comes from the Chinese Pinyin **"Mìng Lìng"**, meaning **"Command"**. +## WARNING -### Mingling's Core Capabilities +Mingling is currently usable at a basic level, but it is still under active development, so many APIs are not yet mature. Any changes to the public API will be documented in detail in the [Changelog](https://github.com/mingling-rs/mingling/blob/main/CHANGELOG.md). -1. **Separation of Concerns, Clear Logic**: Mingling decouples logic by responsibility, helping you organize your CLI program more clearly. - See example: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-basic/src/main.rs) -2. **"All Logic is Functions"**: Execution logic, rendering logic, completion logic, help logic — everything is a function. Just attach the corresponding attribute macro to bind them to your program. -3. **Fully Dynamic Completion System**: With the `comp` feature, you can flexibly implement dynamic completion logic for any subcommand. - See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-completion/src/main.rs) -4. **Lightning-Fast Subcommand Dispatch**: With the `dispatch_tree` feature, Mingling hardens the subcommand structure into a prefix tree at **compile time**, enabling blazing-fast subcommand lookup. - See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-dispatch-tree/src/main.rs) -5. **Lightweight Dependencies, On-Demand Importing**: Minimal core dependencies keep builds fast; enhanced features are imported on demand through fine-grained feature flags. -6. **Structured Output**: Enabling the `structural_renderer` feature adds support for flags like `--json` and `--yaml`, providing structured output capabilities. - See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-structural-renderer/src/main.rs) +Additionally, the project is currently developed by me alone ([Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)). If you are interested in this project, I warmly welcome your [contributions](https://github.com/mingling-rs/mingling/blob/main/CONTRIBUTING.md). You can reach me directly via [Github Issue](https://github.com/mingling-rs/mingling/issues). ---- +## About Mingling's Design -**💡 To learn more, check out the following links:** - -- 📖 [Mainpage](https://mingling-rs.github.io/mingling/) -- 📖 [Examples](https://mingling-rs.github.io/mingling/docs/examples.html) -- 📖 [docs.rs](https://docs.rs/mingling/latest/mingling/) -- 📖 [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/) Or [帮助文档](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) -- 🔍 [Devdoc](https://mingling-rs.github.io/mingling/docs/dev/) - -<h1 align="center"> - Getting Started -</h1> - -Add Mingling to your `Cargo.toml`: - -```toml -[dependencies.mingling] -version = "0.3.0" -features = [] -``` - -Or use the github version - -```toml -[dependencies.mingling] -git = "https://github.com/mingling-rs/mingling.git" -tag = "unreleased" -features = [] -``` - -Or use the [template project](https://github.com/mingling-rs/mingling-template): - -```bash -cargo generate --git mingling-rs/mingling-template -``` - -<h1 align="center"> - Writing with Mingling -</h1> - -### The Big Picture - -Mingling organizes your CLI program into three distinct phases: - -``` -User Input → [Dispatcher] → Entry → [Chain(s)] → Result → [Renderer] → Output -``` - -**Step1: Input** — The user's raw arguments flow in. -**Step2: Dispatch** — A **Dispatcher** picks them up and wraps them into an **Entry** type. -**Step3: Chain** — The entry is handed off to a **Chain** function, which processes it. -**Step4: Render** — A **Renderer** takes that result and writes it to the terminal. - -> [!NOTE] -> A Chain can produce a **State** type to be passed to the next Chain for further processing, -> -> or it can produce a **Result** type to be handed off to a Renderer. - -Everything in this pipeline is a **plain Rust function** with an attribute macro on top. - -You never need to manually implement traits or construct boilerplate. - ---- - -### 1. Defining Commands — `dispatcher!` - -The entry point for every subcommand is the `dispatcher!` macro. It generates two structs for you: a **Dispatcher** (used to register the command with the program) and an **Entry** (a wrapper around `Vec<String>` that holds the raw arguments). - -```rust -use mingling::prelude::*; - -// "command.name" Dispatcher EntryType -// │ │ │ -dispatcher!("greet", CMDGreet => EntryGreet); - -// Nested subcommand: `remote add` -dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); -``` - -Then in `main()`, register the dispatcher with the program: - -```rust -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} -``` - -Mingling also supports an abbreviated form (with the `extra_macros` feature): - -```rust -// Features: ["extra_macros"] - -// Auto-generates CMDGreet / EntryGreet from "greet" -dispatcher!("greet"); -``` - ---- - -### 2. The Chain — "#[chain]" — Where Logic Lives - -The `#[chain]` attribute turns a plain function into an execution step. Think of it as "the logic that transforms one typed value into another." - -```rust -dispatcher!("greet", CMDGreet => EntryGreet); - -pack!(ResultGreeting = String); - -#[chain] -fn handle_greet(args: EntryGreet) -> Next { - let greeting = args - .inner - .first() - .cloned() - .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting).into() -} -``` - -Key points: - -- The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`. -- You chain results by calling `.to_chain()` on any `pack!`-ed type. -- You can have **multiple chain functions** for the same command, each transforming the data further. -- With the `async` feature, chain functions can be `async fn`. - ---- - -### 3. The Renderer — "#[renderer]" — How Output Works - -The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`. - -```rust -use mingling::macros::pack; -use mingling::prelude::*; -use std::io::Write; - -pack!(ResultGreeting = String); - -#[renderer] -fn render_greeting(greeting: ResultGreeting) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); - result -} -``` - -Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline. - -You can write renderers for **any type** in your program, including error types: - -```rust -use mingling::prelude::*; -use std::io::Write; - -#[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Command not found: [{}]", err.join(" ")).ok(); - result -} -``` - ---- - -### 4. Parsing Arguments — The Picker - -Mingling provides a **Picker** for zero-cost argument extraction. You use `pick()` or `pick_or()` on an entry to extract typed values, then `unpack()` to get the final tuple. - -```rust -// Features: ["parser"] - -use mingling::parser::Picker; - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultGreeting = String); - -#[chain] -fn handle_greet(args: EntryGreet) -> Next { - let (name, count) = Picker::new(args.inner) - .pick::<String>(()) // positional: first string - .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default - .unpack(); - ResultGreeting::new(format!("{} x{}", name, count)).into() -} -``` - -With the `parser` feature, the `AsPicker` trait provides a shorthand directly on entries: - -```rust -// Features: ["parser"] - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultGreeting = String); - -#[chain] -fn handle(args: EntryGreet) -> Next { - let (name, count) = args - .pick::<Option<String>>(()) - .pick_or::<u8>(["-r", "--repeat"], 1) - .unpack(); - ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)).into() -} -``` - -For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants from strings: +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 -// Features: ["parser", "extra_macros"] - -use mingling::{EnumTag, Groupped}; -use mingling::parser::PickableEnum; - -dispatcher!("lang.select", CMDLang => EntryLang); - -#[derive(Debug, Default, EnumTag, Groupped)] -pub enum Language { - #[default] - Rust, - #[enum_rename("C++")] - CPlusPlus, -} - -impl PickableEnum for Language {} - -#[chain] -fn handle(args: EntryLang) -> Next { - let lang: Language = args.pick(()).unpack(); - lang.into() -} -``` - ---- - -### 5. The Help System — "#[help]" - -Help is just another attribute macro. When the user passes `--help` or `-h`, the program skips the normal chain/render pipeline and routes directly to your `#[help]` function. - -Enable it by adding `BasicProgramSetup`: - -```rust -use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; -use std::io::Write; - -dispatcher!("greet", CMDGreet => EntryGreet); - -#[help] -fn help_greet(_prev: EntryGreet) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Usage: greet <NAME>").ok(); - writeln!(result, "Greets the user with the given name.").ok(); - result -} - -fn main() { - let mut program = ThisProgram::new(); - program.with_setup(BasicProgramSetup); // enables --help / -h - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} - -gen_program!(); -``` - -The flow is: - -- User types `greet --help` -- `BasicProgramSetup` sets `program.user_context.help = true` -- The dispatcher sees this flag and routes to the `#[help]` function instead of the `#[chain]` - ---- - -### 6. Completion — "#[completion]" — Dynamic Shell Completions - -With the `comp` feature, Mingling provides a fully dynamic completion system. You write a function that returns `Suggest` based on the current shell context, and Mingling generates the completion scripts for bash, zsh, fish, and pwsh. - -```rust -// Features: ["comp", "extra_macros"] - -use mingling::{macros::suggest, ShellContext, Suggest}; - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultName = (u8, String)); - -#[completion(EntryGreet)] -fn complete_greet(ctx: &ShellContext) -> Suggest { - // Suggest positional arguments - if ctx.previous_word == "greet" { - return suggest! { - "Alice": "Likes to receive messages", - "Bob": "Likes to pass messages", - "World" - }; - } - - // Suggest flag arguments - if ctx.typing_argument() { - return suggest! { - "-r": "Number of repetitions", - "--repeat": "Number of repetitions", - } - .strip_typed_argument(ctx); - } - - suggest!() // no suggestions -} -``` - -You also need to register the built-in completion dispatcher: - -```rust -// Features: ["comp"] - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(crate::CMDCompletion); - program.exec_and_exit(); -} -``` - -In your `build.rs`, generate the shell scripts: - -```rust -// BUILD TIME -// Features: ["comp", "builds"] -mingling::build::build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap(); -``` - -For enum-based completions, use `suggest_enum!`: - -```rust -// Features: ["comp", "extra_macros"] - -use mingling::{ShellContext, Suggest}; -use mingling::macros::suggest_enum; -use mingling::EnumTag; - -dispatcher!("lang.select", CMDLang => EntryLang); - -#[derive(EnumTag)] -pub enum ProgrammingLanguages { - Rust, - Python, - JavaScript, -} - -#[completion(EntryLang)] -fn complete_lang(_: &ShellContext) -> Suggest { - suggest_enum!(ProgrammingLanguages) -} -``` - ---- - -### 7. Error Handling - -Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`: - -```rust -use mingling::macros::pack; -use mingling::prelude::*; -use std::io::Write; - -dispatcher!("hello", CMDHello => EntryHello); -pack!(ResultName = String); -pack!(ErrorNoNameProvided = ()); -pack!(ErrorNameTooLong = u16); - -#[chain] -fn handle(args: EntryHello) -> Next { - let Some(name) = args.inner.first().cloned() else { - return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer - }; - - if name.len() > 10 { - return ErrorNameTooLong::new(name.len() as u16).to_render(); - } - - ResultName::new(name).to_render() // ← success path -} - -#[renderer] -fn render_no_name(_: ErrorNoNameProvided) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "No name provided").ok(); - result -} - -#[renderer] -fn render_too_long(len: ErrorNameTooLong) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Name too long: {} > 10", *len).ok(); - result -} -``` - -Two built-in fallback types are always available: - -- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input -- `ErrorRendererNotFound` — rendered when no renderer is found for a result type - ---- - -### 8. Resource Injection - -Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`. - -```rust -// Features: ["parser", "extra_macros"] - -use std::path::PathBuf; - dispatcher!("current", CMDCurrent => EntryCurrent); -dispatcher!("cd", CMDCd => EntryCd); +pack!(StateNext = ()); -#[derive(Default, Clone)] -struct ResCurrentDir { - current_dir: PathBuf, -} - -fn main() { - let mut program = ThisProgram::new(); - program.with_resource(ResCurrentDir { - current_dir: std::env::current_dir().unwrap(), - }); - program.with_dispatcher(CMDCurrent); - program.with_dispatcher(CMDCd); - program.exec_and_exit(); -} - -// Read-only access (shared reference): #[chain] -fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next { - println!("Current: {}", current_dir.current_dir.display()); - empty_result!() -} - -// Mutable access: -#[chain] -fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next { - let path: String = prev.pick(()).unpack(); - current_dir.current_dir = current_dir.current_dir.join(path); - empty_result!() +fn handle_current(_: EntryCurrent) -> StateNext { + // 1. The first phase outputs the StateNext value + StateNext::default() // ^^^^^^^^^ +} // | + // | + // 2. The second phase takes StateNext as input +#[chain] // | +fn handle_state_next(_: StateNext) { + todo!() } ``` -Resources can also be injected into `#[renderer]`: +See? `handle_current` and `handle_state_next` have no direct connection! -```rust -use mingling::prelude::*; -use std::io::Write; - -dispatcher!("current", CMDCurrent => EntryCurrent); - -#[derive(Default, Clone)] -struct ResCurrentDir { - current_dir: std::path::PathBuf, -} - -#[renderer] -fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok(); - result -} -``` - ---- - -### 9. Dispatch Tree — Compile-Time Command Trie - -As your program grows to dozens or hundreds of subcommands, linear dispatcher lookup becomes slow. Enable the `dispatch_tree` feature to convert the command structure into a **prefix tree (Trie)** at compile time. - -```rust -// Features: ["dispatch_tree"] - -dispatcher!("cmd1", CMD1 => Entry1); -dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1); -dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2); -dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1); -dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2); -// ... dozens more - -fn main() { - let program = ThisProgram::new(); - // No more with_dispatcher calls — it's all compile-time! - program.exec_and_exit(); -} -``` - -With `dispatch_tree` enabled: - -- Dispatchers are auto-collected at compile time -- `Program` no longer stores a dispatcher list -- `program.with_dispatcher(...)` is not compiled -- Lookup is **O(n)** where _n_ is input length, not number of commands - ---- - -### 10. Clap Binding — Using Clap's Parser - -If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It generates a dispatcher from a `clap::Parser` struct. - -```rust -// Features: ["clap"] -// Dependencies: -// clap = "4" - -use mingling::macros::dispatcher_clap; -use mingling::prelude::*; -use std::io::Write; - -#[derive(Default, clap::Parser, Groupped)] -#[dispatcher_clap( - "greet", CMDGreet, - help = true, // auto-generate #[help] from clap - error = ErrorGreetParsed, // capture parse errors as a renderable type -)] -pub struct EntryGreet { - #[clap(default_value = "World")] - name: String, - - #[arg(short, long, default_value_t = 1)] - repeat: i32, -} - -#[renderer] -fn render_greet(greet: EntryGreet) -> RenderResult { - let mut result = RenderResult::new(); - write!(result, "Hello, ").ok(); - for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); } - writeln!(result, "!").ok(); - result -} - -#[renderer] -fn render_parse_error(err: ErrorGreetParsed) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "{}", *err).ok(); - result -} -``` - -You can control how clap help is displayed: - -```rust -// Features: ["clap"] - -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(CMDGreet); - program.stdout_setting.clap_help_print_behaviour = - mingling::ClapHelpPrintBehaviour::WriteToRenderResult; - // or: PrintDirectly — writes clap help straight to stdout - program.exec_and_exit(); -} -``` - ---- - -### 11. REPL Mode - -With the `repl` feature, turn your CLI into an interactive shell with one method call: - -```rust -// Features: ["repl"] - -fn main() { - ThisProgram::new().exec_repl(); -} -``` +They are bridged by `StateNext` and automatically linked by the framework. -Mingling provides built-in REPL setups: +You can use this approach to separate computation from result rendering, like this: ```rust -// Features: ["repl", "extra_macros"] +// Features: ["picker"] +dispatcher!("calc", CMDCalculate => EntryCalculate); +pack!(StateSumNumbers = Vec<i32>); +pack!(ResultNumber = i32); -use mingling::{ - res::ResREPL, - setup::{BasicREPLReadlineSetup, BasicREPLOutputSetup, BasicREPLPromptSetup}, -}; - -dispatcher!("cd", CMDCd => EntryCd); -dispatcher!("exit", CMDExit => EntryExit); - -fn main() { - let mut program = ThisProgram::new(); - - program.with_dispatcher(CMDCd); - program.with_dispatcher(CMDExit); - - // Enable line reading from stdin - program.with_setup(BasicREPLReadlineSetup); - - // Enable output flushing after each render - program.with_setup(BasicREPLOutputSetup); - - // Custom prompt - program.with_setup(BasicREPLPromptSetup::func(|| "> ".to_string())); - - program.exec_repl(); // ← interactive loop -} - -// Exit the REPL via the ResREPL resource: +// Entry: parse arguments and pass state to the calculation step #[chain] -fn handle_exit(_prev: EntryExit, repl: &mut ResREPL) { - repl.exit = true; -} -``` - ---- - -### 12. Hooks — Observing the Pipeline - -Mingling provides a `ProgramHook` system for observing every stage of the execution pipeline. Useful for debugging, logging, or telemetry. - -```rust -use mingling::{ - hook::{ProgramControlUnit, ProgramHook}, -}; - -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - - program.with_hook( - ProgramHook::<ThisProgram>::empty() - .on_begin::<_, ()>(|_| println!("[DEBUG] Program is begin")) - .on_pre_dispatch(|info| println!("[DEBUG] Pre dispatch: {}", info.arguments.join(" "))) - .on_post_dispatch(|info| println!("[DEBUG] Post dispatch: {}", info.entry)) - .on_pre_chain(|info| { - println!("[DEBUG] Pre chain: {}", info.input); - }) - .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) - .on_finish(|_| { - println!("[DEBUG] Loop end"); - ProgramControlUnit::OverrideExitCode(0) // Override exit code - }) - .on_pre_render(|info| println!("[DEBUG] Pre render: {}", info.input)) - .on_post_render(|_| println!("[DEBUG] Post render")), - ); - - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} -``` - ---- - -### 13. Structural Renderer — Structured Output (JSON/YAML) - -With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text. - -```rust -// Features: ["structural_renderer", "parser"] -// Dependencies: -// serde = "1" - -use mingling::{prelude::*, setup::StructuralRendererSetup}; -use mingling::Groupped; -use mingling::StructuralData; -use serde::Serialize; -use std::io::Write; - -dispatcher!("render", CMDRender => EntryRender); - -#[derive(Default, StructuralData, Serialize, Groupped)] -struct ResultInfo { - name: String, - age: i32, +fn handle_calc(args: EntryCalculate) -> StateSumNumbers { + let numbers = args.pick(&arg![Vec<i32>]).unwrap(); + StateSumNumbers::new(numbers) } +// Calculate: pass the result to the rendering step #[chain] -fn render_info(args: EntryRender) -> Next { - let (name, age) = args.pick::<String>(()).pick::<i32>(()).unpack(); - ResultInfo { name, age }.to_chain() +fn handle_state_sum_numbers(sum: StateSumNumbers) -> ResultNumber { + let numbers = sum.inner; + let total: i32 = numbers.iter().sum(); + ResultNumber::new(total) } +// Renderer: return the render result and let the framework handle output #[renderer] -fn render_info_result(info: ResultInfo) -> RenderResult { +fn render_number(number: ResultNumber) -> RenderResult { let mut result = RenderResult::new(); - writeln!(result, "{} is {} years old", info.name, info.age).ok(); + writeln!(result, "Number: {}", *number).ok(); result } - -fn main() { - let mut program = ThisProgram::new(); - program.with_setup(StructuralRendererSetup); // enables --json / --yaml - program.with_dispatcher(CMDRender); - let _ = program.exec(); -} ``` -Then users can do: +Although this may make your program slightly more verbose, each step is a **pure function**, making it extremely easy to test! -```bash -$ myapp render Bob 22 -Bob is 22 years old +## Getting Started -$ myapp render Bob 22 --json -{"name":"Bob","age":22} +Add Mingling to your `Cargo.toml`: -$ myapp render Bob 22 --yaml -name: Bob -age: 22 +```toml +[dependencies.mingling] +version = "0.3.0" +features = [] ``` ---- - -### 14. Async Support - -Enable the `async` feature to use `async fn` inside `#[chain]`: - -```rust -// Features: ["async", "parser"] -// Dependencies: -// tokio = { version = "1", features = ["full"] } - -use std::io::Write; -use std::time::Duration; - -dispatcher!("download", CMDDownload => EntryDownload); -pack!(ResultDownloaded = String); - -#[chain] -pub async fn handle_download(args: EntryDownload) -> Next { - let file = args.pick(()).unpack(); - download_file(file).await.into() -} - -async fn download_file(name: String) -> ResultDownloaded { - tokio::time::sleep(Duration::from_secs(1)).await; - ResultDownloaded::new(name) -} +Or use the github version -#[renderer] -fn render_downloaded(result: ResultDownloaded) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "\"{}\" downloaded.", *result).ok(); - r -} +```toml +[dependencies.mingling] +git = "https://github.com/mingling-rs/mingling.git" +tag = "unreleased" +features = [] ``` > [!NOTE] -> -> `#[renderer]` functions cannot be async. When `async` is enabled, `program.exec_and_exit().await` returns a Future. - ---- - -### 15. Wrapping Up — `gen_program!()` - -At the very end of your crate root (main.rs / lib.rs), call `gen_program!()` to generate the `ThisProgram` struct, the `Next` type alias, and all internal plumbing. - -```rust -use mingling::macros::gen_program; - -gen_program!(); -``` - -It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations. - ---- - -### Putting It All Together - -Here's a complete, runnable program: - -```rust -use mingling::macros::pack; -use mingling::prelude::*; -use std::io::Write; +> To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING_STARTED.md) -dispatcher!("greet", CMDGreet => EntryGreet); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(CMDGreet); - program.exec_and_exit(); -} - -pack!(ResultGreeting = String); - -#[chain] -fn handle_greet(args: EntryGreet) -> Next { - let greeting = args - .inner - .first() - .cloned() - .unwrap_or_else(|| "World".to_string()); - ResultGreeting::new(greeting).into() -} - -#[renderer] -fn render_greeting(greeting: ResultGreeting) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); - result -} - -gen_program!(); -``` - -```bash -$ myapp greet -Hello, World! - -$ myapp greet Alice -Hello, Alice! -``` - -<h1 align="center"> - 🗺️ Roadmap 🗺️ -</h1> +## Roadmap - [x] Milestone.1 "MVP" 🎉 - [x] [[0.1.4](https://docs.rs/mingling/0.1.4/mingling/)] [`core`] [`structural_renderer`] **Mingling** can render data into serializable formats via `--json` and `--yaml` flags @@ -880,19 +128,17 @@ Hello, Alice! - [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" - - [ ] ... + - [ ] ... -<h1 align="center"> - 🚫 Unplanned Features 🚫 -</h1> +## Unplanned Features While Mingling has several common CLI features that are **NOT PLANNED** to be directly included in the framework. This is because the Rust ecosystem already has excellent and mature crates to handle these issues, and Mingling's design is intended to be used in combination with them. @@ -902,10 +148,19 @@ This is because the Rust ecosystem already has excellent and mature crates to ha - **Progress Bars**: To display progress indicators, the [`indicatif`](https://crates.io/crates/indicatif) crate is the standard choice. - **TUI**: To build full-screen interactive terminal applications, it is recommended to use a framework like [`ratatui`](https://crates.io/crates/ratatui) (formerly `tui-rs`). -<h1 align="center"> - 📄 Open Source License 📄 -</h1> +## License This project is licensed under the MIT License. See [LICENSE-MIT](LICENSE-MIT) or [LICENSE-APACHE](LICENSE-APACHE) file for details. + +## Learn More + +**To learn more, check out the following links:** + +- 📦 Repo - [Github](https://github.com/mingling-rs/mingling) | [Gitee](https://gitee.com/mingling-rs/mingling) | [Origin](https://catilgrass.cn/mingling.git) +- 🚪 Mainpage - [Github](https://mingling-rs.github.io/mingling/) | [crates.io](https://crates.io/crates/mingling) +- 💡 Examples - [Github](https://mingling-rs.github.io/mingling/docs/examples.html) +- 📖 Help Doc - [EN](https://mingling-rs.github.io/mingling/docs/doc.html#/) | [中文](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) +- 📖 API Doc - [docs.rs](https://docs.rs/mingling/latest/mingling/) | [latest](https://mingling-rs.github.io/mingling/docs/api-docs/mingling/) +- 📖 Dev Doc - [Github](https://mingling-rs.github.io/mingling/docs/dev/) diff --git a/arg_picker/Cargo.toml b/arg_picker/Cargo.toml index 913719e..d87a844 100644 --- a/arg_picker/Cargo.toml +++ b/arg_picker/Cargo.toml @@ -8,6 +8,9 @@ authors = ["Weicao-CatilGrass"] readme = "README.md" description = "A lightweight, type-safe CLI argument parser" +[features] +mingling_support = ["arg-picker-macros/mingling_support"] + [dependencies] arg-picker-macros.workspace = true just_fmt.workspace = true 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..6fe4418 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) 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 6945b67..adf900f 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"] +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 499390d..f4bc9dc 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -118,9 +118,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 +171,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; @@ -218,7 +218,10 @@ pub mod prelude { pub use crate::parser::AsPicker; #[cfg(feature = "picker")] - pub use arg_picker::prelude::*; + pub use arg_picker::prelude::arg; + + #[cfg(feature = "picker")] + pub use crate::picker::EntryPicker; /// Used to enable the `writeln!` macro for `RenderResult` #[cfg(feature = "core")] 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/src/setups/picker/basic.rs b/mingling/src/setups/picker/basic.rs index b8cc237..c9f82b3 100644 --- a/mingling/src/setups/picker/basic.rs +++ b/mingling/src/setups/picker/basic.rs @@ -43,7 +43,10 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - pick_global_flag(program, self.flag); + let help = pick_global_flag(program, self.flag); + if help { + program.user_context.help = true; + } } } @@ -72,7 +75,10 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - pick_global_flag(program, self.flag); + let help = pick_global_flag(program, self.flag); + if help { + program.stdout_setting.quiet = true; + } } } @@ -101,7 +107,10 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - pick_global_flag(program, self.flag); + let help = pick_global_flag(program, self.flag); + if help { + program.user_context.confirm = true; + } } } 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/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/chain.rs b/mingling_macros/src/chain.rs index 5044227..ef31854 100644 --- a/mingling_macros/src/chain.rs +++ b/mingling_macros/src/chain.rs @@ -20,41 +20,16 @@ fn is_unit_return_type(sig: &Signature) -> bool { } } -/// Validates that the return type is `Next`, `ChainProcess<ThisProgram>`, `()`, or omitted. +/// Validates that the return type is acceptable. +/// Accepts `()`, `Next`, `ChainProcess<...>`, or any type that can +/// be converted to `ChainProcess` via `.into()` (i.e. any pack type). fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> { // `()` or omitted is always valid if is_unit_return_type(sig) { return Ok(()); } - match &sig.output { - ReturnType::Type(_, ty) => match &**ty { - Type::Path(type_path) => { - let last_segment = type_path.path.segments.last().unwrap(); - let ident_str = last_segment.ident.to_string(); - if ident_str == "Next" || ident_str == "ChainProcess" { - return Ok(()); - } - Err(syn::Error::new( - ty.span(), - "Chain function must return `Next`, `ChainProcess<ThisProgram>`, `()`, or omit the return type", - ) - .to_compile_error()) - } - _ => Err(syn::Error::new( - ty.span(), - "Chain function must return `Next`, `ChainProcess<ThisProgram>`, `()`, or omit the return type", - ) - .to_compile_error()), - }, - ReturnType::Default => { - Err(syn::Error::new( - sig.span(), - "Chain function must specify a return type (must be `Next`, `ChainProcess<ThisProgram>`, or `()`)", - ) - .to_compile_error()) - } - } + Ok(()) } /// Builds the `proc` function implementation inside the generated `Chain` impl. @@ -71,6 +46,7 @@ fn generate_proc_fn( fn_body_stmts: &[syn::Stmt], is_async_fn: bool, is_unit_return: bool, + origin_return_type: &proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type); let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); @@ -86,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) } }; @@ -106,11 +82,16 @@ fn generate_proc_fn( } else { quote! { #wrapped_body } }; - // Use a let-binding with explicit type annotation so that the inner - // body's `.into()` calls resolve correctly (same as the original function). + // Convert the body result to `ChainProcess` using the user-declared + // return type as the source type for a fully-qualified `Into` call. + // This works for both: + // - `-> Next` / `-> ChainProcess`: identity `From<T> for T` + // - `-> PackType`: `Into<ChainProcess>` from pack!/derive quote! { - let __chain_result: ::mingling::ChainProcess<#program_type> = { #body }; - __chain_result + let __chain_result = { #body }; + <#origin_return_type as ::std::convert::Into< + ::mingling::ChainProcess<#program_type> + >>::into(__chain_result) } }; @@ -234,6 +215,12 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Always use the default crate-defined program path let program_type = crate::default_program_path(); + // Extract the user's return type for the explicit Into turbofish + let origin_return_type = match &input_fn.sig.output { + ReturnType::Type(_, ty) => quote! { #ty }, + ReturnType::Default => quote! { () }, + }; + // Generate the `proc` function for the Chain impl let proc_fn = generate_proc_fn( has_resources, @@ -247,6 +234,7 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(not(feature = "async"))] false, is_unit_return, + &origin_return_type, ); // Preserve the original function untouched, with dead_code allowed diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/dispatcher.rs index 2a7c850..3698ede 100644 --- a/mingling_macros/src/dispatcher.rs +++ b/mingling_macros/src/dispatcher.rs @@ -134,7 +134,7 @@ 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; + use ::mingling::Grouped; #pack::new(args).to_chain() } fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> { diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/group_impl.rs index 59da9dd..cac2734 100644 --- a/mingling_macros/src/group_impl.rs +++ b/mingling_macros/src/group_impl.rs @@ -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/groupped.rs b/mingling_macros/src/grouped.rs index 8aee003..9014c37 100644 --- a/mingling_macros/src/groupped.rs +++ b/mingling_macros/src/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 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/lib.rs b/mingling_macros/src/lib.rs index 9419f39..ea33577 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) | //! @@ -161,7 +161,7 @@ mod entry; mod enum_tag; #[cfg(feature = "extra_macros")] mod group_impl; -mod groupped; +mod grouped; mod help; mod node; mod pack; @@ -265,7 +265,7 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { /// 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 +/// within the Mingling framework by generating a `Grouped` implementation and registering /// the type's simple name as an enum variant. /// /// # Syntax @@ -281,11 +281,11 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { /// /// 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 +/// - An `impl Grouped<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!`. +/// program enum, just like `#[derive(Grouped)]` or `pack!`. /// /// # Example /// @@ -297,7 +297,7 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { /// ``` /// /// After expansion, the type can be used in chains and renderers like any -/// `#[derive(Groupped)]` type. +/// `#[derive(Grouped)]` type. /// /// This macro is only available with the `extra_macros` feature. #[cfg(feature = "extra_macros")] @@ -402,7 +402,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 +438,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 +461,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 +478,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 +521,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 +547,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 +560,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 +618,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) } @@ -1250,10 +1255,10 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { help::help_attr(item) } -/// Derive macro for automatically implementing the `Groupped` trait on a struct. +/// Derive macro for automatically implementing the `Grouped` trait on a struct. /// -/// The `#[derive(Groupped)]` macro: -/// 1. Implements `Groupped<crate::ThisProgram>`. +/// 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 +1266,7 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { /// # Syntax /// /// ```rust,ignore -/// #[derive(Groupped)] +/// #[derive(Grouped)] /// struct MyStruct { /// // ... /// } @@ -1270,9 +1275,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 +1285,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 +1356,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 +1377,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. @@ -1446,8 +1451,30 @@ pub fn gen_program(_input: TokenStream) -> TokenStream { 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!(); @@ -1474,7 +1501,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { #[doc(hidden)] #[::mingling::macros::chain] pub async fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Groupped; + use ::mingling::Grouped; let read_ctx = ::mingling::ShellContext::try_from(prev.inner); match read_ctx { @@ -1492,7 +1519,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { #[doc(hidden)] #[::mingling::macros::chain] pub fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Groupped; + use ::mingling::Grouped; let read_ctx = ::mingling::ShellContext::try_from(prev.inner); match read_ctx { @@ -1516,7 +1543,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { let comp_dispatcher = quote! { #[doc(hidden)] mod __internal_completion_mod { - use ::mingling::Groupped; + use ::mingling::Grouped; ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); ::mingling::macros::pack!( CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) @@ -1548,7 +1575,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { /// Registers a type into the global packed types registry for inclusion in /// the program enum generated by `gen_program!`. /// -/// This macro is called internally by `pack!` and `#[derive(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. @@ -1653,13 +1680,13 @@ pub fn register_renderer(input: TokenStream) -> TokenStream { pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { #[cfg(feature = "structural_renderer")] let pack_empty = quote! { - #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Groupped, Default)] + #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] pub struct ResultEmpty; }; #[cfg(not(feature = "structural_renderer"))] let pack_empty = quote! { - #[derive(::mingling::Groupped, Default)] + #[derive(::mingling::Grouped, Default)] pub struct ResultEmpty; }; @@ -1675,7 +1702,7 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { /// and its `ProgramCollect` implementation. /// /// This is the core code generation macro that: -/// 1. Collects all registered types (from `pack!`, `#[derive(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 diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/pack.rs index 7f05232..0af1b4c 100644 --- a/mingling_macros/src/pack.rs +++ b/mingling_macros/src/pack.rs @@ -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/pack_err.rs index ba7cf17..a747aa9 100644 --- a/mingling_macros/src/pack_err.rs +++ b/mingling_macros/src/pack_err.rs @@ -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! { @@ -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/structural_data.rs b/mingling_macros/src/structural_data.rs index 5350d7e..74bcf09 100644 --- a/mingling_macros/src/structural_data.rs +++ b/mingling_macros/src/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_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, } diff --git a/verified-docs.toml b/verified-docs.toml index d66967f..f3b1f85 100644 --- a/verified-docs.toml +++ b/verified-docs.toml @@ -3,5 +3,6 @@ [verified] readme = "./README.md" +getting_started = "./GETTING_STARTED.md" documents_en_us = "./docs/pages/**" documents_zh_cn = "./docs/_zh_CN/pages/**" |
