aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md63
-rw-r--r--arg_picker/src/picker/parse.rs49
-rw-r--r--arg_picker/src/picker/result.rs49
-rw-r--r--docs/example-pages/examples.json16
-rw-r--r--examples/example-command-macro/Cargo.lock234
-rw-r--r--examples/example-command-macro/Cargo.toml16
-rw-r--r--examples/example-command-macro/page.toml10
-rw-r--r--examples/example-command-macro/src/main.rs66
-rw-r--r--examples/test-examples.toml15
-rw-r--r--mingling/src/example_docs.rs90
-rw-r--r--mingling/src/lib.rs6
-rw-r--r--mingling_core/src/renderer/render_result.rs31
-rw-r--r--mingling_macros/src/attr.rs2
-rw-r--r--mingling_macros/src/attr/command.rs365
-rw-r--r--mingling_macros/src/func/dispatcher.rs26
-rw-r--r--mingling_macros/src/func/gen_program.rs107
-rw-r--r--mingling_macros/src/func/program_comp_gen.rs4
-rw-r--r--mingling_macros/src/func/program_final_gen.rs122
-rw-r--r--mingling_macros/src/lib.rs87
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs43
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs15
-rw-r--r--mingling_pathf/src/patterns.rs2
-rw-r--r--mingling_pathf/src/patterns/command.rs81
-rw-r--r--mingling_pathf/src/type_mapping_builder.rs17
24 files changed, 1329 insertions, 187 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd2c0a3..5fe8be1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -60,7 +60,16 @@ None
#### Optimizations:
-None
+1. **[`pathf`]** Added `is_module` field to `AnalyzeItem` and a new constructor `AnalyzeItem::local_module(module, item_name)` which sets `is_module: true`. The `type_mapping_builder` now tracks whether an item is a module: when generating `type_using.rs`, module items produce `use path::to::module::*;` (glob import) instead of the standard `use path::to::TypeName;` direct import. Non-module items continue to use direct imports as before. The internal data structure changed from `Vec<(String, String)>` to `Vec<(String, String, bool)>` to carry the `is_module` flag through the pipeline.
+
+2. **[`macros:gen_program`]** Wrapped all code generated by `gen_program!()` inside a `__this_program_impl` module and re-exported it with `pub use __this_program_impl::*;`. This isolates the generated internal items (type aliases, trait implementations, and pathf-generated `use` statements) from the call site's module namespace, preventing name collisions and keeping generated machinery out of the caller's direct scope.
+
+ - The `Next` type alias, `Routable` impl for `ChainProcess<ThisProgram>`, and the `program_fallback_gen!()` / `program_final_gen!()` expansions are now all inside `pub mod __this_program_impl { ... }`, then re-exported publicly.
+ - Pathf integration: when the `pathf` feature is enabled, the `type_using.rs` file (generated by the build script) is loaded at compile time via `load_pathf_uses()` and emitted as `use ...;` statements **inside** the `__this_program_impl` module. Previously, pathf uses were injected via `include!()` inside the `ProgramCollect` impl block in `program_final_gen`; now they are loaded by `gen_program` itself and placed at the top of the hidden module. A `compile_error!` hint is emitted if the pathf file is missing or empty.
+ - When `pathf` is **disabled**, `__this_program_impl` emits `use super::*;` to bring the caller's parent scope types into the generated module, preserving existing behavior for projects that don't use pathf.
+ - Completion generation: removed `crate::` prefix from `CompletionSuggest` references in `program_comp_gen.rs`, since the generated code now lives inside `__this_program_impl` and no longer has a direct `crate` path to the user's crate root. The prefix became unnecessary because `CompletionSuggest` is expected to be in scope (e.g., via pathf glob re-exports or the `use super::*;` fallback).
+
+ _No behavioral change for downstream code — all public items are re-exported with the same names. The `__this_program_impl` module is `#[doc(hidden)]` and not part of the public API._
#### Features:
@@ -79,6 +88,58 @@ None
Each type implements `SinglePickable`, performing filesystem validation at parse time and returning `NotFound` when the precondition is not met.
+2. **[`picker:parsing`]** Added convenience methods to the internal `repeat!`-generated tuple implementations for `PickArgParsed<T1, T2, ...>` structs in `arg_picker::picker::parse`:
+
+ - **`unwrap_or_default(self)`** — Returns the parsed values, using `Default::default()` for any missing required arguments. Panics if a route was selected.
+ - **`unwrap_or_else<F>(self, op: F)`** — Returns the parsed values, using the provided closure to generate default values for any missing required arguments. Panics if a route was selected.
+ - **`expect(self, msg: &str)`** — Returns the parsed values, or panics with the given message if a route was selected. Requires `Route: std::fmt::Debug`.
+
+ These methods provide ergonomic alternatives to `to_result()` + `unwrap()` / `unwrap_or_default()` / `unwrap_or_else()` / `expect()` chaining, reducing boilerplate when working with `PickArgParsed` tuples directly.
+
+3. **[`macros:command`]** Added the `#[command]` attribute macro (feature-gated behind `extra_macros`) that converts a plain function with a `Vec<String>` parameter into a fully wired Mingling command. The macro:
+
+ - Calls `dispatcher!("command_name")` to register the dispatcher entry.
+ - Generates a `#[chain]` wrapper that bridges the entry type (`Entry{Pascal}`) to the original function.
+ - Preserves the original function unchanged (including attributes, extensions, visibility, and asyncness).
+
+ **Syntax variants:**
+
+ ```rust,ignore
+ // Simple form — auto-derives names from function name
+ #[command]
+ fn greet(args: Vec<String>) -> Next { /* ... */ }
+ // → dispatcher!("greet"), CMDGreet, EntryGreet
+
+ // Explicit node path
+ #[command(node = "hello.world")]
+ fn greet(args: Vec<String>) -> Next { /* ... */ }
+ // → dispatcher!("hello.world", CMDGreet => EntryGreet)
+
+ // Explicit name/entry overrides
+ #[command(name = MyDispatcher, entry = MyEntry)]
+ fn greet(args: Vec<String>) -> Next { /* ... */ }
+ // → dispatcher!("greet", MyDispatcher => MyEntry)
+ ```
+
+ **Extension attributes** (e.g. `buffer`, `routeify`) passed as bare paths in `#[command(...)]` are applied as `#[ext]` attributes **on the original function**, not on the generated chain wrapper. The chain wrapper always uses bare `#[::mingling::macros::chain]`.
+
+ **Resource injection:** Parameters after the first are treated as resource injections and passed through to the generated `#[chain]` wrapper unchanged.
+
+ **Hidden module:** Each `#[command]` generates a `#[doc(hidden)]` module `__command_{fn_name}_module` that re-exports all generated types (`CMD*`, `Entry*`, chain struct, dispatcher static) for pathf / external access.
+
+ Internally, the implementation:
+ - Parses `#[command(...)]` arguments via `CommandArgs` supporting `node`, `name`, `entry` keys and extension paths.
+ - Validates function constraints (no `self`, at least one parameter).
+ - Handles async functions (rejected without the `async` feature).
+ - Resolves default names via `just_fmt::dot_case!` / `just_fmt::pascal_case!`.
+ - Builds a chain wrapper that calls the original function with `entry.into()` for the first argument.
+
+4. **[`pathf:patterns`]** Added `CommandPattern` to the `pathf` pattern analyzer, matching functions annotated with `#[command]`. The pattern tracks the generated hidden module (`__command_{fn}_module`) and marks it as a local module item via `AnalyzeItem::local_module()`. The build system generates a glob re-export `use path::__command_{fn}_module::*;` to bring all generated types (`Entry*`, `CMD*`, chain struct, dispatcher static) into scope.
+
+5. **[`macros:dispatcher`]** Added a `From<pack_Type> for crate::Entry` implementation inside the `dispatcher!()` macro expansion. When the `dispatcher!()` macro generates the entry pack type (via `pack!(#pack = Vec<String>)`), it now also generates `impl From<#pack> for crate::Entry { fn from(value: #pack) -> Self { crate::Entry::new(value.inner) } }`. This allows pack types generated by `dispatcher!()` to be directly converted into `crate::Entry`, enabling ergonomic integration with program-level entry handling.
+
+6. **[`macros:gen_program`]** Added a `pack!(Entry = Vec<String>)` invocation inside the `__this_program_impl` module generated by `gen_program!()`. This creates a `Entry` pack type (aliasing a `Vec<String>` container) directly in the generated module, providing a default entry point type for the program that can be used by `dispatcher!()`-generated types and other chain infrastructure without requiring the user to define a separate entry pack type manually.
+
#### **BREAKING CHANGES** (API CHANGES):
None
diff --git a/arg_picker/src/picker/parse.rs b/arg_picker/src/picker/parse.rs
index 9db5bd9..4a959e3 100644
--- a/arg_picker/src/picker/parse.rs
+++ b/arg_picker/src/picker/parse.rs
@@ -33,6 +33,55 @@ internal_repeat!(1..=32 => {
((p.v$.expect(concat!("missing required argument at position ", $)),+))
}
+ /// Returns the parsed values, using the default values for any missing
+ /// required arguments, or panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ ///
+ /// # Type Constraints
+ ///
+ /// All types in the tuple must implement [`Default`].
+ pub fn unwrap_or_default(self) -> ((T$,+))
+ where
+ (
+ T$: Default,
+ +) {
+ let p = self.parse();
+ ((p.v$.unwrap_or_default(),+))
+ }
+
+ /// Returns the parsed values, using the provided closure to generate
+ /// default values for any missing required arguments, or panicking if
+ /// a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ pub fn unwrap_or_else<F>(self, op: F) -> ((T$,+))
+ where
+ F: FnOnce(Route) -> ((T$,+)),
+ {
+ let r = self.to_result();
+ r.unwrap_or_else(op)
+ }
+
+ /// Returns the parsed values, or panics with the given message if
+ /// a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected, with the provided message.
+ ///
+ /// # Type Constraints
+ ///
+ /// `Route` must implement [`std::fmt::Debug`] so that the error
+ /// message can include the route value.
+ pub fn expect(self, msg: &str) -> ((T$,+)) where Route: std::fmt::Debug {
+ self.to_result().expect(msg)
+ }
+
/// Returns the individual option values without checking the route.
pub fn unpack(self) -> ((Option<T$>,+)) {
let p = self.parse();
diff --git a/arg_picker/src/picker/result.rs b/arg_picker/src/picker/result.rs
index 2099825..f13f52e 100644
--- a/arg_picker/src/picker/result.rs
+++ b/arg_picker/src/picker/result.rs
@@ -32,6 +32,55 @@ internal_repeat!(1..=32 => {
((self.v$.expect(concat!("missing required argument at position ", $)),+))
}
+ /// Returns the parsed values, using the default values for any missing
+ /// required arguments, or panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ ///
+ /// # Type Constraints
+ ///
+ /// All types in the tuple must implement [`Default`].
+ pub fn unwrap_or_default(self) -> ((T$,+))
+ where
+ (
+ T$: Default,
+ +) {
+ let p = self;
+ ((p.v$.unwrap_or_default(),+))
+ }
+
+ /// Returns the parsed values, using the provided closure to generate
+ /// default values for any missing required arguments, or panicking if
+ /// a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ pub fn unwrap_or_else<F>(self, op: F) -> ((T$,+))
+ where
+ F: FnOnce(Route) -> ((T$,+)),
+ {
+ let r = self.to_result();
+ r.unwrap_or_else(op)
+ }
+
+ /// Returns the parsed values, or panics with the given message if
+ /// a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected, with the provided message.
+ ///
+ /// # Type Constraints
+ ///
+ /// `Route` must implement [`std::fmt::Debug`] so that the error
+ /// message can include the route value.
+ pub fn expect(self, msg: &str) -> ((T$,+)) where Route: std::fmt::Debug {
+ self.to_result().expect(msg)
+ }
+
/// Returns the individual option values without checking the route.
pub fn unpack(self) -> ((Option<T$>,+)) {
((self.v$,+))
diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json
index 2a07365..349248e 100644
--- a/docs/example-pages/examples.json
+++ b/docs/example-pages/examples.json
@@ -95,6 +95,22 @@
]
},
{
+ "id": "example-command-macro",
+ "name": "Command Macro",
+ "icon": "🚀",
+ "category": "advanced",
+ "desc": "Introduced how to use the `#[command]` macro to generate commands with minimal boilerplate\n",
+ "tags": [
+ "#[command]",
+ "dispatcher!",
+ "#[chain]"
+ ],
+ "files": [
+ "src/main.rs",
+ "Cargo.toml"
+ ]
+ },
+ {
"id": "example-completion",
"name": "Completion",
"icon": "🔄",
diff --git a/examples/example-command-macro/Cargo.lock b/examples/example-command-macro/Cargo.lock
new file mode 100644
index 0000000..025b322
--- /dev/null
+++ b/examples/example-command-macro/Cargo.lock
@@ -0,0 +1,234 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arg-picker"
+version = "0.1.1"
+dependencies = [
+ "arg-picker-macros",
+ "just_fmt",
+]
+
+[[package]]
+name = "arg-picker-macros"
+version = "0.1.1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "example-command-macro"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "might_be_async"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca871cea620b07bd2e6da0c883891a25bead698c43a9ab64b0fd663a7a78d5f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "toml",
+]
+
+[[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",
+ "might_be_async",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
diff --git a/examples/example-command-macro/Cargo.toml b/examples/example-command-macro/Cargo.toml
new file mode 100644
index 0000000..b73c2c6
--- /dev/null
+++ b/examples/example-command-macro/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "example-command-macro"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies.mingling]
+path = "../../mingling"
+features = [
+ # Use `extra_macros` to introduce the `#[command]` macro
+ "extra_macros",
+
+ # Use `picker` to parse arguments
+ "picker",
+]
+
+[workspace]
diff --git a/examples/example-command-macro/page.toml b/examples/example-command-macro/page.toml
new file mode 100644
index 0000000..fe36d7a
--- /dev/null
+++ b/examples/example-command-macro/page.toml
@@ -0,0 +1,10 @@
+[example]
+id = "example-command-macro"
+name = "Command Macro"
+icon = "🚀"
+category = "advanced"
+desc = """
+Introduced how to use the `#[command]` macro to generate commands with minimal boilerplate
+"""
+tags = ["#[command]", "dispatcher!", "#[chain]"]
+files = ["src/main.rs", "Cargo.toml"]
diff --git a/examples/example-command-macro/src/main.rs b/examples/example-command-macro/src/main.rs
new file mode 100644
index 0000000..2bf932a
--- /dev/null
+++ b/examples/example-command-macro/src/main.rs
@@ -0,0 +1,66 @@
+//! Example Command Macro
+//!
+//! > Introduced how to use the `#[command]` macro to generate commands with minimal boilerplate
+//!
+//! Run:
+//! ```base
+//! cargo run --manifest-path examples/example-command-macro/Cargo.toml --quiet -- hello world
+//! cargo run --manifest-path examples/example-command-macro/Cargo.toml --quiet -- greet-someone Alice
+//! cargo run --manifest-path examples/example-command-macro/Cargo.toml --quiet -- goodbye
+//! ```
+//!
+//! Output:
+//! ```plaintext
+//! Hello, World
+//! Hello, Alice
+//! Goodbye!
+//! ```
+
+use mingling::{macros::buffer, picker::IntoPicker, prelude::*};
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // Import the dispatchers generated by the `#[command]` macro
+ program.with_dispatcher(CMDHelloWorld);
+ program.with_dispatcher(CMDGreetSomeone);
+ program.with_dispatcher(CMDGoodBye);
+
+ program.exec_and_exit();
+}
+
+pack!(ResultGreeting = String);
+pack!(ResultGoodbye = ());
+
+// --------- IMPORTANT ---------
+// Auto-generates dispatcher!("hello.world", CMDHelloWorld => EntryHelloWorld);
+#[command]
+fn hello_world() -> ResultGreeting {
+ ResultGreeting::new("World".to_string())
+}
+
+// Auto-generates dispatcher!("hello-world", CMDGreetSomeone => EntryGreetSomeone);
+#[command(node = "greet-someone")]
+fn greet_someone(args: Vec<String>) -> ResultGreeting {
+ let name = args.pick_or(&arg![String], || "World".to_string()).unwrap();
+ ResultGreeting::new(name)
+}
+
+// Auto-generates dispatcher!("goodbye", CMDGoodBye => EntryGoodBye);
+#[command(name = CMDGoodBye, entry = EntryGoodBye)]
+fn goodbye() -> ResultGoodbye {
+ ResultGoodbye::default()
+}
+// --------- IMPORTANT ---------
+
+#[renderer(buffer)]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}", *result);
+}
+
+#[renderer(buffer)]
+fn render_goodbye(_: ResultGoodbye) {
+ r_println!("Goodbye!");
+}
+
+gen_program!();
diff --git a/examples/test-examples.toml b/examples/test-examples.toml
index e450919..b74c9a5 100644
--- a/examples/test-examples.toml
+++ b/examples/test-examples.toml
@@ -312,3 +312,18 @@ expect.result = "Result: 1.3333334"
command = "calc 4 / 3 --round"
expect.exit-code = 0
expect.result = "Result: 1"
+
+[[test.example-command-macro]]
+command = "hello world"
+expect.exit-code = 0
+expect.result = "Hello, World"
+
+[[test.example-command-macro]]
+command = "greet-someone Alice"
+expect.exit-code = 0
+expect.result = "Hello, Alice"
+
+[[test.example-command-macro]]
+command = "goodbye"
+expect.exit-code = 0
+expect.result = "Goodbye!"
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index 35cf024..3e9bd26 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -768,6 +768,96 @@ pub mod example_clap_binding {}
/// gen_program!();
/// ```
pub mod example_combine_pathf_dispatch_tree {}
+/// Example Command Macro
+///
+/// > Introduced how to use the `#[command]` macro to generate commands with minimal boilerplate
+///
+/// Run:
+/// ```base
+/// cargo run --manifest-path examples/example-command-macro/Cargo.toml --quiet -- hello world
+/// cargo run --manifest-path examples/example-command-macro/Cargo.toml --quiet -- greet-someone Alice
+/// cargo run --manifest-path examples/example-command-macro/Cargo.toml --quiet -- goodbye
+/// ```
+///
+/// Output:
+/// ```plaintext
+/// Hello, World
+/// Hello, Alice
+/// Goodbye!
+/// ```
+///
+/// Source code (./Cargo.toml)
+/// ```toml
+/// [package]
+/// name = "example-command-macro"
+/// version = "0.1.0"
+/// edition = "2024"
+///
+/// [dependencies.mingling]
+/// path = "../../mingling"
+/// features = [
+/// # Use `extra_macros` to introduce the `#[command]` macro
+/// "extra_macros",
+///
+/// # Use `picker` to parse arguments
+/// "picker",
+/// ]
+///
+/// [workspace]
+/// ```
+///
+/// Source code (./src/main.rs)
+/// ```ignore
+/// use mingling::{macros::buffer, picker::IntoPicker, prelude::*};
+///
+/// fn main() {
+/// let mut program = ThisProgram::new();
+///
+/// // Import the dispatchers generated by the `#[command]` macro
+/// program.with_dispatcher(CMDHelloWorld);
+/// program.with_dispatcher(CMDGreetSomeone);
+/// program.with_dispatcher(CMDGoodBye);
+///
+/// program.exec_and_exit();
+/// }
+///
+/// pack!(ResultGreeting = String);
+/// pack!(ResultGoodbye = ());
+///
+/// // --------- IMPORTANT ---------
+/// // Auto-generates dispatcher!("hello.world", CMDHelloWorld => EntryHelloWorld);
+/// #[command]
+/// fn hello_world() -> ResultGreeting {
+/// ResultGreeting::new("World".to_string())
+/// }
+///
+/// // Auto-generates dispatcher!("hello-world", CMDGreetSomeone => EntryGreetSomeone);
+/// #[command(node = "greet-someone")]
+/// fn greet_someone(args: Vec<String>) -> ResultGreeting {
+/// let name = args.pick_or(&arg![String], || "World".to_string()).unwrap();
+/// ResultGreeting::new(name)
+/// }
+///
+/// // Auto-generates dispatcher!("goodbye", CMDGoodBye => EntryGoodBye);
+/// #[command(name = CMDGoodBye, entry = EntryGoodBye)]
+/// fn goodbye() -> ResultGoodbye {
+/// ResultGoodbye::default()
+/// }
+/// // --------- IMPORTANT ---------
+///
+/// #[renderer(buffer)]
+/// fn render_greeting(result: ResultGreeting) {
+/// r_println!("Hello, {}", *result);
+/// }
+///
+/// #[renderer(buffer)]
+/// fn render_goodbye(_: ResultGoodbye) {
+/// r_println!("Goodbye!");
+/// }
+///
+/// gen_program!();
+/// ```
+pub mod example_command_macro {}
/// Example Completion
///
/// > This example demonstrates how to use **Mingling** to create fully dynamic command-line completions
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 19f2188..d7c8e8f 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -52,6 +52,9 @@ pub mod macros {
pub use mingling_macros::buffer;
/// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method
pub use mingling_macros::chain;
+ /// `#[command]` - Declares a command from a plain function with `Vec<String>` entry
+ #[cfg(feature = "extra_macros")]
+ pub use mingling_macros::command;
/// `#[completion(EntryType)]` - Used to generate completion entry
#[cfg(feature = "comp")]
pub use mingling_macros::completion;
@@ -223,6 +226,9 @@ pub mod prelude {
/// Re-export of the `chain` macro for defining a chain of commands.
#[cfg(feature = "macros")]
pub use crate::macros::chain;
+ /// Re-export of the `#[command]` macro for declaring commands from plain functions.
+ #[cfg(all(feature = "extra_macros", feature = "macros"))]
+ pub use crate::macros::command;
/// Re-export of the `dispatcher` macro for routing commands.
#[cfg(feature = "macros")]
pub use crate::macros::dispatcher;
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index 8757376..fc3f2b1 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,6 +1,7 @@
use std::{
fmt::{Display, Formatter},
io::Write,
+ process::{ExitCode, exit},
};
use crate::RenderResultMode::{Stderr, Stdout};
@@ -99,6 +100,18 @@ macro_rules! impl_from_int {
impl_from_int!(i32, i16, i8, u32, u16, u8, usize);
+impl From<RenderResult> for ExitCode {
+ fn from(value: RenderResult) -> Self {
+ ExitCode::from(value.exit_code as u8)
+ }
+}
+
+impl From<&RenderResult> for ExitCode {
+ fn from(value: &RenderResult) -> Self {
+ ExitCode::from(value.exit_code as u8)
+ }
+}
+
impl From<&String> for RenderResult {
fn from(value: &String) -> Self {
string_to_render_result(value, Stdout)
@@ -483,6 +496,24 @@ impl RenderResult {
exit_code: self.exit_code,
}
}
+
+ /// Exits the process with the exit code stored in this `RenderResult`.
+ ///
+ /// This method calls `std::process::exit()` with the `exit_code` value,
+ /// terminating the current process immediately.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::new();
+ /// result.exit_code = 42;
+ /// // result.exit_process(); // would exit with code 42
+ /// ```
+ pub fn exit_process(&self) {
+ exit(self.exit_code)
+ }
}
#[inline(always)]
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs
index ed7f58b..8c17878 100644
--- a/mingling_macros/src/attr.rs
+++ b/mingling_macros/src/attr.rs
@@ -1,4 +1,6 @@
pub(crate) mod chain;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod command;
#[cfg(feature = "comp")]
pub(crate) mod completion;
#[cfg(feature = "clap")]
diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs
new file mode 100644
index 0000000..35c5c30
--- /dev/null
+++ b/mingling_macros/src/attr/command.rs
@@ -0,0 +1,365 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::parse_macro_input;
+use syn::spanned::Spanned;
+use syn::token::Comma;
+use syn::{FnArg, Ident, ItemFn, LitStr, PatType, Token, Type};
+
+/// Parsed arguments for `#[command(...)]`.
+///
+/// Supports:
+/// - `node = "dot.separated.path"` — explicit command path
+/// - `name = CMDName` — explicit CMD struct name
+/// - `entry = EntryName` — explicit Entry struct name
+/// - bare paths like `routeify`, `::mingling::macros::routeify` — extension attrs for the original fn
+struct CommandArgs {
+ node: Option<LitStr>,
+ name: Option<Ident>,
+ entry: Option<Ident>,
+ exts: Vec<syn::Path>,
+}
+
+impl Parse for CommandArgs {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut node = None;
+ let mut name = None;
+ let mut entry = None;
+ let mut exts = Vec::new();
+
+ while !input.is_empty() {
+ if input.peek(Ident) && input.peek2(Token![=]) {
+ // key = value pair
+ let key: Ident = input.parse()?;
+ input.parse::<Token![=]>()?;
+
+ if key == "node" {
+ if node.is_some() {
+ return Err(input.error("duplicate `node` argument"));
+ }
+ node = Some(input.parse()?);
+ } else if key == "name" {
+ if name.is_some() {
+ return Err(input.error("duplicate `name` argument"));
+ }
+ name = Some(input.parse()?);
+ } else if key == "entry" {
+ if entry.is_some() {
+ return Err(input.error("duplicate `entry` argument"));
+ }
+ entry = Some(input.parse()?);
+ } else {
+ return Err(input.error(format!(
+ "unknown key `{}`; expected `node`, `name`, or `entry`",
+ key
+ )));
+ }
+ } else {
+ // Extension path (e.g. `routeify` or `::mingling::macros::routeify`)
+ let ext: syn::Path = input.parse()?;
+ exts.push(ext);
+ }
+
+ // Skip optional trailing comma
+ if input.peek(Comma) {
+ let _ = input.parse::<Comma>();
+ }
+ }
+
+ Ok(CommandArgs {
+ node,
+ name,
+ entry,
+ exts,
+ })
+ }
+}
+
+/// Returns the default node path as a dot-separated string, derived from the function name.
+///
+/// Example: `greet_someone` -> `"greet.someone"`, `greet` -> `"greet"`
+fn default_node_from_fn(fn_name: &Ident) -> String {
+ just_fmt::dot_case!(fn_name.to_string())
+}
+
+/// Checks basic function constraints for `#[command]`.
+/// Returns `Err(compile_error token stream)` on failure.
+fn validate_function(f: &ItemFn) -> Result<(), TokenStream2> {
+ if f.sig
+ .inputs
+ .iter()
+ .any(|arg| matches!(arg, FnArg::Receiver(_)))
+ {
+ return Err(syn::Error::new(
+ f.sig.span(),
+ "#[command] function cannot have a `self` parameter",
+ )
+ .to_compile_error());
+ }
+
+ Ok(())
+}
+
+/// Returns `(wrapper_async_token, await_call)` for the chain wrapper.
+///
+/// When the original function is async and the `async` feature is enabled,
+/// the wrapper needs to be `async` and call `.await` on the original function.
+/// Without the feature, async functions are rejected.
+fn handle_async(f: &ItemFn) -> Result<(TokenStream2, TokenStream2), TokenStream2> {
+ let is_async = f.sig.asyncness.is_some();
+
+ #[cfg(not(feature = "async"))]
+ if is_async {
+ return Err(syn::Error::new(
+ f.sig.span(),
+ "#[command] function cannot be async when the `async` feature is disabled",
+ )
+ .to_compile_error());
+ }
+
+ let wrapper_async = is_async.then_some(quote! { async }).unwrap_or_default();
+ let await_call = is_async.then_some(quote! { .await }).unwrap_or_default();
+ Ok((wrapper_async, await_call))
+}
+
+/// All resolved identifiers derived from `#[command]` arguments + function name.
+struct ResolvedNames {
+ /// `node_str` as a string literal token
+ node_lit: LitStr,
+ /// Whether the user supplied any explicit override (node/name/entry)
+ has_overrides: bool,
+ /// CMD struct name (e.g. `CMDGreet`)
+ cmd_name: Ident,
+ /// Entry struct name (e.g. `EntryGreet`)
+ entry_type: Ident,
+ /// Chain wrapper function name (e.g. `__command_chain_greet`)
+ chain_fn_name: Ident,
+}
+
+/// Resolves `node`, `cmd_name`, `entry_type`, and `chain_fn_name` from
+/// the attribute args and the original function name.
+fn resolve_names(fn_name: &Ident, args: &CommandArgs) -> ResolvedNames {
+ let fn_name_str = fn_name.to_string();
+
+ let node_str = match &args.node {
+ Some(lit) => lit.value(),
+ None => default_node_from_fn(fn_name),
+ };
+ let node_lit = syn::LitStr::new(&node_str, fn_name.span());
+
+ let has_overrides = args.node.is_some() || args.name.is_some() || args.entry.is_some();
+
+ let cmd_name = args.name.clone().unwrap_or_else(|| {
+ let pascal = just_fmt::pascal_case!(&node_str);
+ Ident::new(&format!("CMD{pascal}"), fn_name.span())
+ });
+
+ let entry_type = args.entry.clone().unwrap_or_else(|| {
+ let pascal = just_fmt::pascal_case!(&node_str);
+ Ident::new(&format!("Entry{pascal}"), fn_name.span())
+ });
+
+ let chain_fn_name = Ident::new(&format!("__command_chain_{}", fn_name_str), fn_name.span());
+
+ ResolvedNames {
+ node_lit,
+ has_overrides,
+ cmd_name,
+ entry_type,
+ chain_fn_name,
+ }
+}
+
+/// Converts extension paths into `#[ext]` attribute token streams.
+fn build_ext_attrs(exts: &[syn::Path]) -> Vec<TokenStream2> {
+ exts.iter().map(|ext| quote! { #[#ext] }).collect()
+}
+
+/// Returns `true` if the function has a first non-reference (owned) parameter that
+/// serves as the "args" input.
+fn has_args_param(sig: &syn::Signature) -> bool {
+ sig.inputs.first().map_or(false, |arg| {
+ if let FnArg::Typed(pat_type) = arg {
+ !matches!(&*pat_type.ty, Type::Reference(_))
+ } else {
+ false
+ }
+ })
+}
+
+/// Builds the wrapper function's parameter list.
+///
+/// - If the function has an "args" param (first non-reference): replaces its type
+/// with the entry type (e.g. `Vec<String>` → `EntryGreet`).
+/// - If not (no params, or first param is a reference): inserts a new entry param
+/// at the front to satisfy `#[chain]`'s requirement for an owned first parameter.
+fn build_wrapper_params(
+ sig: &syn::Signature,
+ entry_type: &Ident,
+) -> syn::punctuated::Punctuated<FnArg, syn::token::Comma> {
+ if has_args_param(sig) {
+ // First param is owned (args) -> replace its type with entry type
+ let mut params = sig.inputs.clone();
+ if let Some(FnArg::Typed(first)) = params.first_mut() {
+ first.ty = Box::new(Type::Path(syn::TypePath {
+ qself: None,
+ path: syn::Path::from(entry_type.clone()),
+ }));
+ }
+ params
+ } else {
+ // No args param -> insert an entry param at the front, keep rest as-is
+ let mut params = syn::punctuated::Punctuated::new();
+ let entry_param: FnArg = syn::parse_quote! { _args: #entry_type };
+ params.push(entry_param);
+ for arg in sig.inputs.iter() {
+ params.push(arg.clone());
+ }
+ params
+ }
+}
+
+/// Builds call arguments for the original function call inside the wrapper.
+///
+/// - If the function has an "args" param (first non-reference): first arg gets
+/// `.into()` (converts `EntryGreet` → `Vec<String>`), rest pass through as-is.
+/// - If not (no args): all params are resources — pass none, return empty.
+fn build_call_args(sig: &syn::Signature) -> Vec<TokenStream2> {
+ let has_args = has_args_param(sig);
+ sig.inputs
+ .iter()
+ .enumerate()
+ .map(|(i, arg)| {
+ let pat = match arg {
+ FnArg::Typed(PatType { pat, .. }) => quote! { #pat },
+ FnArg::Receiver(_) => unreachable!(),
+ };
+ if has_args && i == 0 {
+ quote! { #pat.into() }
+ } else {
+ pat
+ }
+ })
+ .collect()
+}
+
+/// Generates the `dispatcher!(...)` call.
+///
+/// - No overrides → abbreviated form: `dispatcher!("node")`
+/// - Any override → explicit form: `dispatcher!("node", CMDName => EntryName)`
+fn build_dispatcher_invoke(names: &ResolvedNames) -> TokenStream2 {
+ let node_lit = &names.node_lit;
+ if names.has_overrides {
+ let cmd_name = &names.cmd_name;
+ let entry_type = &names.entry_type;
+ quote! { ::mingling::macros::dispatcher!(#node_lit, #cmd_name => #entry_type); }
+ } else {
+ quote! { ::mingling::macros::dispatcher!(#node_lit); }
+ }
+}
+
+pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // validation
+ if let Err(err) = validate_function(&input_fn) {
+ return err.into();
+ }
+
+ // parse attribute
+ let args: CommandArgs = if attr.is_empty() {
+ CommandArgs {
+ node: None,
+ name: None,
+ entry: None,
+ exts: Vec::new(),
+ }
+ } else {
+ parse_macro_input!(attr as CommandArgs)
+ };
+
+ // async handling
+ let (wrapper_async, await_call) = match handle_async(&input_fn) {
+ Ok(pair) => pair,
+ Err(err) => return err.into(),
+ };
+
+ // resolve node / name / entry
+ let fn_name = &input_fn.sig.ident;
+ let names = resolve_names(fn_name, &args);
+ let chain_fn_name = &names.chain_fn_name;
+
+ // build extension attributes (applied to the ORIGINAL function)
+ let ext_attrs = build_ext_attrs(&args.exts);
+
+ // build wrapper
+ let wrapper_params = build_wrapper_params(&input_fn.sig, &names.entry_type);
+ let call_args = build_call_args(&input_fn.sig);
+
+ // build dispatcher invocation
+ let dispatcher_invoke = build_dispatcher_invoke(&names);
+
+ // preserve original function
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("command"));
+
+ let vis = &input_fn.vis;
+ let asyncness = input_fn.sig.asyncness;
+ let generics = &input_fn.sig.generics;
+ let orig_params = &input_fn.sig.inputs;
+ let orig_return = &input_fn.sig.output;
+ let fn_body = &input_fn.block;
+
+ // compute names for the re‑export module and internal structs
+ let fn_name_s = fn_name.to_string();
+ let mod_name = Ident::new(&format!("__command_{}_module", &fn_name_s), fn_name.span());
+ let wrapper_full = format!("__command_chain_{}", &fn_name_s);
+ let snaked_wrapper = just_fmt::snake_case!(wrapper_full);
+ let chain_internal = Ident::new(
+ &format!("__internal_chain_{}", snaked_wrapper),
+ fn_name.span(),
+ );
+
+ // dispatcher internal static (only exists with dispatch_tree feature)
+ #[cfg(feature = "dispatch_tree")]
+ let snaked_node = just_fmt::snake_case!(names.node_lit.value());
+ #[cfg(feature = "dispatch_tree")]
+ let dispatcher_internal = {
+ let ident = Ident::new(
+ &format!("__internal_dispatcher_{}", snaked_node),
+ fn_name.span(),
+ );
+ quote! { #vis use super::#ident; }
+ };
+ #[cfg(not(feature = "dispatch_tree"))]
+ let dispatcher_internal = quote! {};
+
+ let cmd_name = &names.cmd_name;
+ let entry_type = &names.entry_type;
+
+ // assemble output
+ let expanded = quote! {
+ #dispatcher_invoke
+
+ #[::mingling::macros::chain]
+ #vis #wrapper_async fn #chain_fn_name #generics(#wrapper_params) -> crate::Next {
+ #fn_name(#(#call_args),*)#await_call.into()
+ }
+
+ #(#fn_attrs)*
+ #(#ext_attrs)*
+ #vis #asyncness fn #fn_name #generics(#orig_params) #orig_return #fn_body
+
+ // hidden module gathering all generated types for pathf / external access
+ #[doc(hidden)]
+ #vis mod #mod_name {
+ #vis use super::#cmd_name;
+ #vis use super::#entry_type;
+ #vis use super::#chain_internal;
+ #dispatcher_internal
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs
index 4e7c42f..ef02618 100644
--- a/mingling_macros/src/func/dispatcher.rs
+++ b/mingling_macros/src/func/dispatcher.rs
@@ -99,7 +99,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
command_name,
} => {
let command_name_str = command_name.value();
- let pascal = dotted_to_pascal_case(&command_name_str);
+ let pascal = just_fmt::pascal_case!(&command_name_str);
let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span());
let pack = Ident::new(&format!("Entry{pascal}"), command_name.span());
(command_name, command_struct, pack, cmd_attrs, Vec::new())
@@ -121,6 +121,12 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec<String>);
+ impl From<#pack> for crate::Entry {
+ fn from(value: #pack) -> Self {
+ crate::Entry::new(value.inner)
+ }
+ }
+
#comp_entry
#dispatch_tree_entry
@@ -178,21 +184,3 @@ fn get_dispatch_tree_entry(
) -> TokenStream2 {
quote! {}
}
-
-/// Converts a dotted command name (e.g. "remote.add") to `PascalCase` (e.g. "`RemoteAdd`").
-///
-/// Each segment is split by `.`, the first character of each segment is uppercased,
-/// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax
-/// (when `Command => Entry` is omitted) to auto-derive struct names.
-#[cfg(feature = "extra_macros")]
-pub(crate) fn dotted_to_pascal_case(s: &str) -> String {
- s.split('.')
- .map(|segment| {
- let mut chars = segment.chars();
- match chars.next() {
- None => String::new(),
- Some(c) => c.to_uppercase().to_string() + chars.as_str(),
- }
- })
- .collect()
-}
diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs
index c5358bc..d50041e 100644
--- a/mingling_macros/src/func/gen_program.rs
+++ b/mingling_macros/src/func/gen_program.rs
@@ -15,33 +15,100 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
#[cfg(not(feature = "comp"))]
let comp_gen = quote! {};
+ // When pathf is enabled, load the type_using.rs generated by the build script
+ // and emit its use statements so types from submodules are in scope.
+ #[cfg(feature = "pathf")]
+ let pathf_uses: Vec<proc_macro2::TokenStream> = {
+ let uses = load_pathf_uses();
+ if uses.is_empty() {
+ // The file might not exist yet — emit a clear hint
+ let hint: proc_macro2::TokenStream = syn::parse_quote! {
+ compile_error!(
+ "pathf: `{}` not found or empty.\n\
+ Make sure `build.rs` calls `mingling::build::analyze_and_build_type_mapping().unwrap();`\n\
+ with features [\"build\", \"pathf\"] enabled."
+ );
+ };
+ vec![hint]
+ } else {
+ uses
+ }
+ };
+ #[cfg(not(feature = "pathf"))]
+ let pathf_uses: Vec<proc_macro2::TokenStream> = Vec::new();
+
+ #[cfg(feature = "pathf")]
+ let super_use = quote! {};
+
+ #[cfg(not(feature = "pathf"))]
+ let super_use = quote! {
+ use super::*;
+ };
+
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))
+ pub use __this_program_impl::*;
+
+ #[doc(hidden)]
+ pub mod __this_program_impl {
+ #super_use
+ #(#pathf_uses)*
+
+ /// Alias for the current program type `ThisProgram`
+ pub type Next = ::mingling::ChainProcess<ThisProgram>;
+
+ ::mingling::macros::pack!(Entry = Vec<String>);
+
+ impl ::mingling::Routable<ThisProgram> for ::mingling::ChainProcess<ThisProgram>
+ {
+ fn to_chain(self) -> ::mingling::ChainProcess<ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
+ }
+ other => other,
}
- other => other,
}
- }
- fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
- match self {
- ::mingling::ChainProcess::Ok((any, _)) => {
- ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ fn to_render(self) -> ::mingling::ChainProcess<ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
}
- other => other,
}
}
- }
- #comp_gen
- ::mingling::macros::program_fallback_gen!();
- ::mingling::macros::program_final_gen!();
+ #comp_gen
+ ::mingling::macros::program_fallback_gen!();
+ ::mingling::macros::program_final_gen!();
+ }
})
}
+
+/// Loads `type_using.rs` generated by the pathf build script and returns each
+/// `use ...;` line as a token stream, ready to be emitted in the generated output.
+#[cfg(feature = "pathf")]
+fn load_pathf_uses() -> Vec<proc_macro2::TokenStream> {
+ let out_dir = match std::env::var("OUT_DIR") {
+ Ok(d) => d,
+ Err(_) => return Vec::new(),
+ };
+ let crate_name = match std::env::var("CARGO_PKG_NAME") {
+ Ok(n) => n,
+ Err(_) => return Vec::new(),
+ };
+ let path = std::path::Path::new(&out_dir)
+ .join(&crate_name)
+ .join("type_using.rs");
+ let content = match std::fs::read_to_string(&path) {
+ Ok(c) => c,
+ Err(_) => return Vec::new(),
+ };
+ content
+ .lines()
+ .map(|line| line.trim().to_string())
+ .filter(|line| !line.is_empty())
+ .filter_map(|line| line.parse::<proc_macro2::TokenStream>().ok())
+ .collect()
+}
diff --git a/mingling_macros/src/func/program_comp_gen.rs b/mingling_macros/src/func/program_comp_gen.rs
index b6b2546..2fbb0e0 100644
--- a/mingling_macros/src/func/program_comp_gen.rs
+++ b/mingling_macros/src/func/program_comp_gen.rs
@@ -14,7 +14,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream {
match read_ctx {
Ok(ctx) => {
let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
+ ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest::new((ctx, suggest)))
}
Err(_) => std::process::exit(1),
}
@@ -32,7 +32,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream {
match read_ctx {
Ok(ctx) => {
let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
+ ::mingling::Routable::<crate::ThisProgram>::to_render(CompletionSuggest::new((ctx, suggest)))
}
Err(_) => std::process::exit(1),
}
diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs
index d3af3b3..0eed1db 100644
--- a/mingling_macros/src/func/program_final_gen.rs
+++ b/mingling_macros/src/func/program_final_gen.rs
@@ -1,5 +1,3 @@
-use std::collections::HashMap;
-
use proc_macro::TokenStream;
use quote::quote;
@@ -37,48 +35,12 @@ fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, pr
(struct_ident, variant_ident)
}
-/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`.
-fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> {
- if !cfg!(feature = "pathf") {
- return None;
- }
- let out_dir = std::env::var("OUT_DIR").ok()?;
- let crate_name = std::env::var("CARGO_PKG_NAME").ok()?;
- let path = std::path::Path::new(&out_dir)
- .join(&crate_name)
- .join("type_using.rs");
- let content = std::fs::read_to_string(&path).ok()?;
- Some(
- content
- .lines()
- .filter_map(|line| {
- let line = line.trim();
- if let Some(rest) = line.strip_prefix("use ") {
- let path = rest.strip_suffix(';').unwrap_or(rest);
- if let Some((_mod, type_name)) = path.rsplit_once("::") {
- return Some((type_name.to_string(), path.to_string()));
- }
- }
- None
- })
- .collect(),
- )
-}
-
-/// Resolves a type name to its full path token stream using the pathf mapping.
-pub(crate) fn resolve_type(
- name: &str,
- map: &std::collections::HashMap<String, String>,
-) -> proc_macro2::TokenStream {
- if let Some(full_path) = map.get(name) {
- syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- })
- } else {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- }
+/// Helper: convert a string ident into a token stream for the generated code.
+/// Types are expected to be in scope (e.g. via pathf glob re-exports), so bare
+/// idents suffice.
+fn ident_tokens(name: &str) -> proc_macro2::TokenStream {
+ let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
+ quote! { #ident }
}
#[allow(clippy::too_many_lines)]
@@ -126,45 +88,6 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
.map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
.collect();
- let pathf_map: Option<HashMap<String, String>> = load_pathf_map();
-
- #[cfg(feature = "pathf")]
- let pathf_hint: proc_macro2::TokenStream = if pathf_map.is_none() {
- quote! {
- compile_error!(
-"Cannot load type mapping computed by `pathf`.
-If not yet configured, execute `mingling::build::analyze_and_build_type_mapping()` in your `build.rs`.
-
-fn main() {
- // Require features: [\"builds\", \"pathf\"]
- mingling::build::analyze_and_build_type_mapping().unwrap();
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\__ Write to `build.rs`
-
-}
- ");
- }
- } else {
- quote! {}
- };
-
- #[cfg(not(feature = "pathf"))]
- let pathf_hint: proc_macro2::TokenStream = quote! {};
-
- let pathf_map: HashMap<String, String> = if cfg!(feature = "pathf") {
- pathf_map.unwrap_or_default()
- } else {
- HashMap::new()
- };
-
- let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") {
- pathf_map
- .values()
- .map(|path| format!("use {};", path).parse().unwrap_or_default())
- .collect()
- } else {
- Vec::new()
- };
-
#[cfg(feature = "structural_renderer")]
let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
.iter()
@@ -177,13 +100,9 @@ fn main() {
any: ::mingling::AnyOutput<Self::Enum>,
setting: &::mingling::StructuralRendererSetting,
) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
- #[allow(unused_imports)]
- #(#pathf_uses)*
match any.member_id() {
#(#structural_renderer_tokens)*
_ => {
- // Non-structural types: render ResultEmpty (which implements
- // StructuralData + Serialize) instead of producing nothing.
let mut r = ::mingling::RenderResult::default();
::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
Ok(r)
@@ -222,8 +141,8 @@ fn main() {
})
.collect();
- let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map);
- let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map);
+ let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries);
+ let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries);
quote! {
#get_nodes_fn
@@ -243,8 +162,6 @@ fn main() {
#[cfg(feature = "comp")]
let comp = quote! {
fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
- #[allow(unused_imports)]
- #(#pathf_uses)*
match any.member_id() {
#(#completion_tokens)*
_ => ::mingling::Suggest::FileCompletion,
@@ -266,12 +183,10 @@ fn main() {
} else {
let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ let downcast_ty = ident_tokens(&variant_ident.to_string());
+ let resolved_struct = ident_tokens(&struct_ident.to_string());
quote! {
Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
<#resolved_struct as ::mingling::Renderer>::render(value)
}
@@ -290,12 +205,10 @@ fn main() {
// Build do_chain function (async and sync versions)
let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ let downcast_ty = ident_tokens(&variant_ident.to_string());
+ let resolved_struct = ident_tokens(&struct_ident.to_string());
quote! {
Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
::std::boxed::Box::pin(fut)
@@ -307,12 +220,10 @@ fn main() {
.iter()
.map(|entry| {
let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ let downcast_ty = ident_tokens(&variant_ident.to_string());
+ let resolved_struct = ident_tokens(&struct_ident.to_string());
quote! {
Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
<#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
}
@@ -370,8 +281,6 @@ fn main() {
};
let expanded = quote! {
- #pathf_hint
-
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[repr(#repr_type)]
#[allow(nonstandard_style)]
@@ -392,6 +301,7 @@ fn main() {
type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
type ErrorRendererNotFound = ErrorRendererNotFound;
type ResultEmpty = ResultEmpty;
+
fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
}
@@ -404,8 +314,6 @@ fn main() {
#render_fn
#do_chain_fn
fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- #[allow(unused_imports)]
- #(#pathf_uses)*
match any.member_id() {
#(#help_tokens)*
_ => ::mingling::RenderResult::default(),
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 27563a4..ae874af 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -1100,6 +1100,93 @@ pub fn program_setup(attr: TokenStream, item: TokenStream) -> TokenStream {
program_setup::setup_attr(attr, item)
}
+/// Declares a command from a plain function.
+///
+/// **This macro is only available with the `extra_macros` feature.**
+///
+/// The `#[command]` attribute converts a function taking `Vec<String>` into a
+/// Mingling command by:
+/// 1. Calling `dispatcher!("command_name")` to register the dispatcher entry.
+/// 2. Generating a `#[chain]` wrapper that bridges the entry type (`Entry{Pascal}`)
+/// to the original function.
+/// 3. Preserving the original function unchanged (including attributes, extensions,
+/// visibility, and asyncness).
+///
+/// # Syntax
+///
+/// ## Simple form (auto-derives names from function name)
+///
+/// ```rust,ignore
+/// #[command]
+/// fn greet(args: Vec<String>) -> Next {
+/// // ...
+/// }
+/// ```
+///
+/// This deduces:
+/// - Command path: `"greet"` (via `dot_case` of function name)
+/// - Dispatcher struct: `CMDGreet`
+/// - Entry struct: `EntryGreet`
+/// - Dispatches via `dispatcher!("greet")`
+///
+/// ## Explicit attributes
+///
+/// ```rust,ignore
+/// #[command(node = "hello.world")]
+/// fn greet(args: Vec<String>) -> Next {
+/// // ...
+/// }
+/// // → dispatcher!("hello.world", CMDGreet => EntryGreet)
+/// ```
+///
+/// ```rust,ignore
+/// #[command(name = MyDispatcher, entry = MyEntry)]
+/// fn greet(args: Vec<String>) -> Next {
+/// // ...
+/// }
+/// // → dispatcher!("greet", MyDispatcher => MyEntry)
+/// ```
+///
+/// ## Extension attributes
+///
+/// Extra bare paths (e.g. `buffer`, `routeify`, `::mingling::macros::routeify`)
+/// are emitted as `#[ext]` attributes **on the original function**, not on the
+/// chain wrapper. The chain wrapper always uses bare `#[::mingling::macros::chain]`.
+///
+/// ```rust,ignore
+/// #[command(buffer)]
+/// fn greet(args: Vec<String>) {
+/// r_println!("Hello!");
+/// }
+/// ```
+///
+/// # Resource injection
+///
+/// Parameters after the first are treated as resource injections and passed
+/// through to the generated `#[chain]` wrapper unchanged (as reference params):
+///
+/// ```rust,ignore
+/// #[command]
+/// fn greet(args: Vec<String>, ec: &mut ResExitCode) -> Next {
+/// ec.exit_code = 0;
+/// // ...
+/// }
+/// ```
+///
+/// The generated chain wrapper calls the original function with `entry.into()`
+/// for the first argument and passes all subsequent arguments directly.
+///
+/// # Requirements
+///
+/// - The function must have at least one parameter (the `Vec<String>` entry argument).
+/// - The function must not have a `self` parameter.
+/// - Visibility (`pub` etc.) and `async` are preserved on the original function.
+#[cfg(feature = "extra_macros")]
+#[proc_macro_attribute]
+pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
+ attr::command::command_attr(attr, item)
+}
+
/// Declares a `Dispatcher` that uses `clap::Parser` for argument parsing.
///
/// **This macro is only available with the `clap` feature.**
diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs
index 9eeb637..fe44a49 100644
--- a/mingling_macros/src/systems/dispatch_tree_gen.rs
+++ b/mingling_macros/src/systems/dispatch_tree_gen.rs
@@ -1,27 +1,22 @@
-use std::collections::{BTreeMap, HashMap};
+use std::collections::BTreeMap;
use just_fmt::snake_case;
use proc_macro2::TokenStream;
use quote::quote;
-use crate::func::program_final_gen::resolve_type;
-
/// Generate the `get_nodes()` function body for a ProgramCollect impl.
-/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths.
-pub(crate) fn gen_get_nodes(
- entries: &[(String, String, String)],
- pathf_map: &HashMap<String, String>,
-) -> TokenStream {
+pub(crate) fn gen_get_nodes(entries: &[(String, String, String)]) -> TokenStream {
let mut node_entries = Vec::new();
for (node_name, _disp_type, _entry_name) in entries {
let static_name_str = format!("__internal_dispatcher_{}", snake_case!(node_name));
- let resolved = resolve_type(&static_name_str, pathf_map);
+ let static_ident =
+ proc_macro2::Ident::new(&static_name_str, proc_macro2::Span::call_site());
let node_display_name = node_name.replace('.', " ");
let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site());
node_entries.push(quote! {
- (#node_display_lit.to_string(), & #resolved)
+ (#node_display_lit.to_string(), &#static_ident)
});
}
@@ -38,20 +33,13 @@ pub(crate) fn gen_get_nodes(
///
/// Builds a hardcoded match tree: at each depth, group nodes by character.
/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match.
-///
-/// If `pathf_map` is non-empty, resolves dispatcher types using full paths.
-pub(crate) fn gen_dispatch_args_trie(
- entries: &[(String, String, String)],
- pathf_map: &HashMap<String, String>,
-) -> TokenStream {
- // Prepare (display_name, disp_type) pairs.
- // display_name = node_name.replace('.', " ")
+pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> TokenStream {
let nodes: Vec<(String, String)> = entries
.iter()
.map(|(name, disp, _)| (name.replace('.', " "), disp.clone()))
.collect();
- let dispatch_body = build_dispatch_body(&nodes, 0, pathf_map);
+ let dispatch_body = build_dispatch_body(&nodes, 0);
quote! {
fn dispatch_args_trie(
@@ -70,19 +58,13 @@ pub(crate) fn gen_dispatch_args_trie(
///
/// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far.
/// `depth`: The character index currently being matched.
-/// `pathf_map`: optional mapping from type name to full path for resolving dispatchers.
-fn build_dispatch_body(
- nodes: &[(String, String)],
- depth: usize,
- pathf_map: &HashMap<String, String>,
-) -> TokenStream {
+fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream {
if nodes.is_empty() {
return quote! {
return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
};
}
- // Group by character at `depth`
let mut groups: BTreeMap<char, Vec<(String, String)>> = BTreeMap::new();
let mut exact_nodes: Vec<(String, String)> = Vec::new();
@@ -97,18 +79,17 @@ fn build_dispatch_body(
}
}
- // Build a dispatch arm for a single node via `starts_with`
let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream {
let name_space = format!("{} ", name);
let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site());
- let disp_resolved = resolve_type(disp_type, pathf_map);
+ let disp_ident = proc_macro2::Ident::new(disp_type, proc_macro2::Span::call_site());
let prefix_word_count = name.split_whitespace().count();
quote! {
if raw_str.starts_with(#name_lit) {
let prefix_len = #prefix_word_count;
let trimmed_args: Vec<String> = raw.iter().skip(prefix_len).cloned().collect();
- let __cp = <#disp_resolved as ::mingling::Dispatcher<Self::Enum>>::begin(
- &#disp_resolved::default(),
+ let __cp = <#disp_ident as ::mingling::Dispatcher<Self::Enum>>::begin(
+ &#disp_ident::default(),
trimmed_args,
);
return match __cp {
@@ -136,7 +117,7 @@ fn build_dispatch_body(
}
});
} else {
- let sub_body = build_dispatch_body(sub_nodes, depth + 1, pathf_map);
+ let sub_body = build_dispatch_body(sub_nodes, depth + 1);
arms.push(quote! {
Some(#ch_char) => {
#sub_body
diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs
index 5b25c15..79b1c5a 100644
--- a/mingling_pathf/src/pattern_analyzer.rs
+++ b/mingling_pathf/src/pattern_analyzer.rs
@@ -33,6 +33,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer {
analyzer.add_pattern(GroupPattern);
analyzer.add_pattern(GroupedDerivePattern);
analyzer.add_pattern(ChainPattern);
+ analyzer.add_pattern(CommandPattern);
analyzer.add_pattern(RendererPattern);
analyzer.add_pattern(HelpPattern);
analyzer.add_pattern(CompletionPattern);
@@ -50,6 +51,8 @@ pub struct AnalyzeItem {
pub item_name: String,
/// Whether the item is from an external crate (resolved via `use`), bypassing the file's own module path.
pub is_foreign: bool,
+ /// When `true`, this item is a module whose contents should be glob-imported (`::*`).
+ pub is_module: bool,
}
impl AnalyzeItem {
@@ -59,6 +62,17 @@ impl AnalyzeItem {
module,
item_name,
is_foreign: false,
+ is_module: false,
+ }
+ }
+
+ /// Creates a local module item — generates a `use path::item_name::*;` glob import.
+ pub fn local_module(module: String, item_name: String) -> Self {
+ Self {
+ module,
+ item_name,
+ is_foreign: false,
+ is_module: true,
}
}
@@ -68,6 +82,7 @@ impl AnalyzeItem {
module,
item_name,
is_foreign: true,
+ is_module: false,
}
}
}
diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs
index 9845b73..964fe7c 100644
--- a/mingling_pathf/src/patterns.rs
+++ b/mingling_pathf/src/patterns.rs
@@ -2,6 +2,7 @@
pub use basic_struct::*;
pub use chain::*;
+pub use command::*;
pub use completion::*;
pub use dispatcher::*;
pub use dispatcher_clap::*;
@@ -13,6 +14,7 @@ pub use renderer::*;
mod basic_struct;
mod chain;
+mod command;
mod completion;
mod dispatcher;
mod dispatcher_clap;
diff --git a/mingling_pathf/src/patterns/command.rs b/mingling_pathf/src/patterns/command.rs
new file mode 100644
index 0000000..fac70af
--- /dev/null
+++ b/mingling_pathf/src/patterns/command.rs
@@ -0,0 +1,81 @@
+//! The `CommandPattern` matches functions annotated with `#[command]` and
+//! extracts the generated hidden module name (`__command_<fn>_module`).
+//!
+//! Supported forms:
+//! ```rust,ignore
+//! #[command]
+//! fn greet(args: Vec<String>) -> Next { ... }
+//!
+//! #[command(node = "foo.foo-bar")]
+//! fn greet(args: Vec<String>) -> Next { ... }
+//!
+//! #[command(name = CMDGreet, entry = EntryGreet)]
+//! fn greet(args: Vec<String>) -> Next { ... }
+//!
+//! #[command(node = "greet", name = CMDGreet, entry = EntryGreet, buffer)]
+//! fn greet(args: Vec<String>) { ... }
+//! ```
+
+use syn::Item;
+
+use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
+
+/// Match `#[command]` functions.
+///
+/// The `#[command]` macro generates a hidden `__command_<fn>_module` that
+/// re-exports all internal types (`Entry*`, `CMD*`, chain struct, dispatcher
+/// static). This pattern tracks that module; the build system generates a
+/// glob re-export `use path::__command_<fn>_module::*;` to bring everything
+/// into scope.
+pub struct CommandPattern;
+
+impl AnalyzePattern for CommandPattern {
+ fn contains(&self, content: &str) -> bool {
+ content.contains("command")
+ }
+
+ fn analyze(&self, content: &str) -> Vec<AnalyzeItem> {
+ let Ok(syntax) = syn::parse_file(content) else {
+ return Vec::new();
+ };
+
+ let mut items = Vec::new();
+ for item in &syntax.items {
+ collect_from_item(item, "", &mut items);
+ }
+ items
+ }
+}
+
+fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem>) {
+ match item {
+ Item::Fn(f) if has_command_attr(&f.attrs) => {
+ let fn_name = f.sig.ident.to_string();
+ let mod_name = format!("__command_{}_module", &fn_name);
+ items.push(AnalyzeItem::local_module(current_mod.to_string(), mod_name));
+ }
+ Item::Mod(item_mod) => {
+ if let Some((_, nested)) = &item_mod.content {
+ let mod_name = &item_mod.ident.to_string();
+ let nested_mod = if current_mod.is_empty() {
+ mod_name.clone()
+ } else {
+ format!("{current_mod}::{mod_name}")
+ };
+ for n in nested {
+ collect_from_item(n, &nested_mod, items);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+fn has_command_attr(attrs: &[syn::Attribute]) -> bool {
+ attrs.iter().any(|a| {
+ a.path()
+ .segments
+ .last()
+ .is_some_and(|s| s.ident == "command")
+ })
+}
diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs
index f6f57a5..fb59b43 100644
--- a/mingling_pathf/src/type_mapping_builder.rs
+++ b/mingling_pathf/src/type_mapping_builder.rs
@@ -25,7 +25,7 @@ pub fn analyze_and_build_type_mapping_for(
let module_mapping = module_pathf::analyze(crate_dir)?;
let analyzer = pattern_analyzer::init_with_config(config.clone());
- let mut type_mappings: Vec<(String, String)> = Vec::new();
+ let mut type_mappings: Vec<(String, String, bool)> = Vec::new();
for item in module_mapping {
let file_abs = crate_dir.join(item.file_path());
@@ -40,14 +40,13 @@ pub fn analyze_and_build_type_mapping_for(
for ai in analyze_items {
let full_path = if ai.is_foreign {
- // Foreign item — use its own module path as-is
format!("{}::{}", ai.module, ai.item_name)
} else if ai.module.is_empty() {
format!("{}::{}", module_path, ai.item_name)
} else {
format!("{}::{}::{}", module_path, ai.module, ai.item_name)
};
- type_mappings.push((ai.item_name, full_path));
+ type_mappings.push((ai.item_name, full_path, ai.is_module));
}
}
@@ -56,7 +55,7 @@ pub fn analyze_and_build_type_mapping_for(
// Deduplicate by type name, keeping the first occurrence
let mut seen = HashSet::new();
- type_mappings.retain(|(name, _)| seen.insert(name.clone()));
+ type_mappings.retain(|(name, _, _)| seen.insert(name.clone()));
// Create output directory
std::fs::create_dir_all(output_dir)?;
@@ -66,14 +65,18 @@ pub fn analyze_and_build_type_mapping_for(
let type_using_path = output_dir.join("type_using.rs");
let mut content_mapping = String::new();
- for (name, path) in &type_mappings {
+ for (name, path, _) in &type_mappings {
content_mapping.push_str(&format!("{name} = {path}\n"));
}
std::fs::write(&output_path, content_mapping)?;
let mut content_using = String::new();
- for (_, path) in &type_mappings {
- content_using.push_str(&format!("use {path};\n"));
+ for (_, path, is_module) in &type_mappings {
+ if *is_module {
+ content_using.push_str(&format!("use {path}::*;\n"));
+ } else {
+ content_using.push_str(&format!("use {path};\n"));
+ }
}
std::fs::write(&type_using_path, content_using)?;