aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros')
-rw-r--r--mingling_macros/Cargo.toml2
-rw-r--r--mingling_macros/src/dispatcher.rs5
-rw-r--r--mingling_macros/src/dispatcher_clap.rs7
-rw-r--r--mingling_macros/src/help.rs38
-rw-r--r--mingling_macros/src/lib.rs311
-rw-r--r--mingling_macros/src/render.rs56
-rw-r--r--mingling_macros/src/renderer.rs82
-rw-r--r--mingling_macros/src/res_injection.rs5
-rw-r--r--mingling_macros/src/structural_data.rs12
9 files changed, 207 insertions, 311 deletions
diff --git a/mingling_macros/Cargo.toml b/mingling_macros/Cargo.toml
index 66f654b..1191015 100644
--- a/mingling_macros/Cargo.toml
+++ b/mingling_macros/Cargo.toml
@@ -8,7 +8,7 @@ readme = "README.md"
repository.workspace = true
description = "Macros of the mingling library"
keywords = ["cli", "macros", "procedural", "command-line", "framework"]
-categories = ["command-line-interface", "development-tools::procedural-macros"]
+categories = ["command-line-interface"]
[lib]
proc-macro = true
diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/dispatcher.rs
index 36bdf31..2a7c850 100644
--- a/mingling_macros/src/dispatcher.rs
+++ b/mingling_macros/src/dispatcher.rs
@@ -217,7 +217,10 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
} = syn::parse_macro_input!(input as RegisterDispatcherInput);
let node_name_str = node_name.value();
- let static_name = format!("__internal_dispatcher_{}", snake_case!(node_name_str.clone()));
+ let static_name = format!(
+ "__internal_dispatcher_{}",
+ snake_case!(node_name_str.clone())
+ );
let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site());
// Register node info in the global collection at compile time
diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/dispatcher_clap.rs
index 5f06fb4..0945e31 100644
--- a/mingling_macros/src/dispatcher_clap.rs
+++ b/mingling_macros/src/dispatcher_clap.rs
@@ -144,7 +144,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
Some(quote! {
#[allow(non_snake_case)]
#[::mingling::macros::help]
- pub fn #help_fn_name(_prev: #struct_name) {
+ pub fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult {
use std::io::Write;
use clap::ColorChoice;
@@ -154,11 +154,14 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
let mut cmd = <#struct_name as ::clap::CommandFactory>::command()
.color(ColorChoice::Always);
let styled = cmd.render_help();
- write!(__renderer_inner_result, "{}", styled.ansi()).unwrap();
+ let mut result = ::mingling::RenderResult::new();
+ let _ = write!(result, "{}", styled.ansi());
+ result
}
::mingling::ClapHelpPrintBehaviour::PrintDirectly => {
let mut command = <#struct_name as ::clap::CommandFactory>::command();
command.print_help().unwrap();
+ ::mingling::RenderResult::new()
}
}
}
diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs
index 6d427b9..1903d07 100644
--- a/mingling_macros/src/help.rs
+++ b/mingling_macros/src/help.rs
@@ -1,22 +1,34 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{Ident, ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input};
+use syn::{Ident, ItemFn, ReturnType, Type, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Validates the return type is () or empty
-fn validate_return_type(sig: &Signature) -> syn::Result<()> {
+/// Validates that the function returns `::mingling::RenderResult`.
+fn validate_render_result_return(sig: &syn::Signature) -> syn::Result<()> {
match &sig.output {
ReturnType::Type(_, ty) => match &**ty {
- Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()),
+ Type::Path(type_path) => {
+ let last_seg = type_path.path.segments.last().map(|s| s.ident.to_string());
+ match last_seg.as_deref() {
+ Some("RenderResult") => Ok(()),
+ _ => Err(syn::Error::new(
+ ty.span(),
+ "Help function must return `RenderResult`",
+ )),
+ }
+ }
_ => Err(syn::Error::new(
ty.span(),
- "Help function must return () or have no return type",
+ "Help function must return `RenderResult`",
)),
},
- ReturnType::Default => Ok(()),
+ ReturnType::Default => Err(syn::Error::new(
+ sig.span(),
+ "Help function must have a return type `-> RenderResult`",
+ )),
}
}
@@ -37,8 +49,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
Err(e) => return e.to_compile_error().into(),
};
- // Validate return type
- if let Err(e) = validate_return_type(&input_fn.sig) {
+ // Validate return type is RenderResult
+ if let Err(e) = validate_render_result_return(&input_fn.sig) {
return e.to_compile_error().into();
}
@@ -136,19 +148,17 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
impl ::mingling::HelpRequest for #struct_name {
type Entry = #entry_type;
- fn render_help(#prev_param: Self::Entry, __renderer_inner_result: &mut ::mingling::RenderResult) {
+ fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult {
#help_render_body
}
}
::mingling::macros::register_help!(#entry_type, #struct_name);
- // Keep the original function for internal use with original params without __renderer_inner_result
+ // Keep the original function unchanged
#(#fn_attrs)*
- #vis fn #fn_name(#original_inputs) {
- let mut dummy_r = ::mingling::RenderResult::default();
- let __renderer_inner_result = &mut dummy_r;
+ #vis fn #fn_name(#original_inputs) -> ::mingling::RenderResult {
#fn_body
}
};
@@ -164,7 +174,7 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::
// SAFETY: The member_id check ensures that `any` contains a value of type `#entry_type`,
// so downcasting to `#entry_type` is safe.
let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() };
- <#struct_name as ::mingling::HelpRequest>::render_help(value, __renderer_inner_result);
+ <#struct_name as ::mingling::HelpRequest>::render_help(value)
}
}
}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 546416d..d0f603a 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -2,7 +2,7 @@
//!
//! This crate is the **macro layer** of Mingling. Each `#[attribute]` or `!`-callable
//! macro collects metadata into **compile-time global registries** (`OnceLock<Mutex<BTreeSet>>`).
-//! At the end, [`gen_program!`] reads all registries and generates the final program struct
+//! At the end, `gen_program!` reads all registries and generates the final program struct
//! with all dispatchers, chains, renderers, and completions wired together.
//!
//! # How Macros Work Together
@@ -47,16 +47,16 @@
//!
//! | Macro | What it does |
//! |-------|-------------|
-//! | [`dispatcher!`] | Declares a command entry point and its argument type |
-//! | [`macro@dispatcher_clap`] | Like `dispatcher!` but powered by `clap::Parser` |
-//! | [`node!`] | Builds a [`Node`](https://docs.rs/mingling/latest/mingling/struct.Node.html) from a dot-separated path string |
-//! | [`pack!`] | Creates a newtype wrapper around an inner type for use in Chain/Renderer |
-//! | [`pack_structural!`] | Like `pack!` but also derives `StructuralData` for structured output |
-//! | [`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 |
+//! | `dispatcher!` | Declares a command entry point and its argument type |
+//! | `dispatcher_clap!` | Like `dispatcher!` but powered by `clap::Parser` |
+//! | `node!` | Builds a [`Node`](https://docs.rs/mingling/latest/mingling/struct.Node.html) from a dot-separated path string |
+//! | `pack!` | Creates a newtype wrapper around an inner type for use in Chain/Renderer |
+//! | `pack_structural!` | Like `pack!` but also derives `StructuralData` for structured output |
+//! | `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(StructuralData)]`](derive@StructuralData) | Marks a type as eligible for structured output (JSON/YAML/etc.) |
+//! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) |
//! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) |
//!
//! ## Phase 2: Processing & Rendering Registration
@@ -66,28 +66,27 @@
//! | [`#[chain]`](attr.chain.html) | Transforms a function into a chain processing step |
//! | [`#[renderer]`](attr.renderer.html) | Transforms a function into a renderer for a type |
//! | [`#[help]`](attr.help.html) | Defines help output for a command entry type |
-//! | [`route!`] | Routes execution depending on a condition |
-//! | [`empty_result!`] | Returns an empty result for early termination |
-//! | [`r_print!`] / [`r_println!`] | Print to the `RenderResult` buffer inside a renderer |
+//! | `route!` | Routes execution depending on a condition |
+//! | `empty_result!` | Returns an empty result for early termination |
//! | [`#[completion]`](attr.completion.html) | Registers a shell completion handler |
//!
//! ## Phase 3: Program Generation
//!
//! | Macro | What it does |
//! |-------|-------------|
-//! | [`gen_program!`] | **Final step**: reads all registries and generates the full program |
-//! | [`suggest!`] | Generates suggestion logic for a dispatcher |
-//! | [`suggest_enum!`] | Generates suggestion logic for an enum dispatcher |
+//! | `gen_program!` | **Final step**: reads all registries and generates the full program |
+//! | `suggest!` | Generates suggestion logic for a dispatcher |
+//! | `suggest_enum!` | Generates suggestion logic for an enum dispatcher |
//!
//! ## Internal (used by the macros above)
//!
//! | Macro | What it does |
//! |-------|-------------|
-//! | [`register_type!`] | Registers a type in the packed-type registry |
-//! | [`register_chain!`] | Registers a chain mapping in the chain registry |
-//! | [`register_renderer!`] | Registers a renderer mapping in the renderer registry |
-//! | [`register_dispatcher!`] | Registers a dispatcher for the `dispatch_tree` feature |
-//! | [`register_help!`] | Registers a help request handler |
+//! | `register_type!` | Registers a type in the packed-type registry |
+//! | `register_chain!` | Registers a chain mapping in the chain registry |
+//! | `register_renderer!` | Registers a renderer mapping in the renderer registry |
+//! | `register_dispatcher!` | Registers a dispatcher for the `dispatch_tree` feature |
+//! | `register_help!` | Registers a help request handler |
//! | `program_fallback_gen!` | Generates fallback error types |
//! | `program_final_gen!` | Generates the `ProgramCollect` impl and `ThisProgram` struct |
//! | `program_comp_gen!` | Generates completion logic |
@@ -99,12 +98,12 @@
//!
//! | Feature | Macros enabled |
//! |---------|---------------|
-//! | `clap` | [`macro@dispatcher_clap`] |
-//! | `comp` | [`#[completion]`](attr.completion.html), [`suggest!`], [`suggest_enum!`] |
-//! | `extra_macros` | [`entry!`], [`empty_result!`], [`route!`], [`#[program_setup]`](attr.program_setup.html), [`macro@group`] |
+//! | `clap` | `dispatcher_clap!` |
+//! | `comp` | [`#[completion]`](attr.completion.html), `suggest!`, `suggest_enum!` |
+//! | `extra_macros` | `entry!`, `empty_result!`, `route!`, [`#[program_setup]`](attr.program_setup.html), `group!` |
//! | `dispatch_tree` | `register_dispatcher!` (enables trie-based command dispatch) |
-//! | `structural_renderer` | [`#[derive(StructuralData)]`](derive@StructuralData), [`pack_structural!`], [`pack_err_structural!`], [`macro@group_structural`] |
-//! | `structural_renderer` + `extra_macros` | [`group_structural!`], [`pack_err_structural!`] |
+//! | `structural_renderer` | `#[derive(StructuralData)]`, `pack_structural!`, `pack_err_structural!`, `group_structural!` |
+//! | `structural_renderer` + `extra_macros` | `group_structural!`, `pack_err_structural!` |
//! | `async` | Enables async `#[chain]` functions |
//! | `repl` | Enables REPL execution loop |
//!
@@ -115,7 +114,7 @@
//! entries contain the **token-stream representation** of match arms, type mappings,
//! and struct definitions.
//!
-//! When [`gen_program!`] is called, it reads all registries, concatenates their
+//! When `gen_program!` is called, it reads all registries, concatenates their
//! entries, and emits the complete program:
//!
//! ```rust,ignore
@@ -170,7 +169,6 @@ mod pack;
mod pack_err;
#[cfg(feature = "extra_macros")]
mod program_setup;
-mod render;
mod renderer;
mod res_injection;
#[cfg(feature = "structural_renderer")]
@@ -241,10 +239,27 @@ pub(crate) fn check_duplicate_variant(
/// Checks if a stored entry string contains the given variant name.
/// Handles both "StructName => Variant," and "Self::Variant => ..." formats.
fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
- entry.contains(&format!("=> {variant_name},"))
- || entry.contains(&format!("=> {variant_name} "))
- || entry.contains(&format!("=> {variant_name}"))
- || entry.contains(&format!(":: {variant_name} =>"))
+ let variant_match = format!("=> {variant_name}");
+
+ // "StructName => Variant," — exact match with trailing comma
+ if entry.contains(&format!("{variant_match},")) {
+ return true;
+ }
+ // "StructName => Variant " — exact match with trailing space
+ if entry.contains(&format!("{variant_match} ")) {
+ return true;
+ }
+ // "StructName => Variant" (fallback) — must NOT be followed by identifier chars
+ if let Some(idx) = entry.find(&variant_match) {
+ let after = idx + variant_match.len();
+ if after >= entry.len()
+ || !entry[after..].starts_with(|c: char| c.is_alphanumeric() || c == '_')
+ {
+ return true;
+ }
+ }
+ // "Self::Variant => ..." — match-arm existence check format
+ entry.contains(&format!(":: {variant_name} =>"))
}
/// Registers an outside-type as a member of a program group without modifying its definition.
@@ -584,13 +599,13 @@ pub fn route(input: TokenStream) -> TokenStream {
/// crate::ResultEmpty::new(()).to_chain()
/// ```
///
-/// This works because [`ResultEmpty`] is automatically generated by [`gen_program!`]
+/// This works because [`ResultEmpty`] is automatically generated by `gen_program!`
/// and implements the necessary trait conversions into [`ChainProcess`].
///
/// # See also
///
/// - [`ResultEmpty`] — The type that represents an empty result.
-/// - [`route!`] — For early-return from `Result` expressions.
+/// - `route!` — For early-return from `Result` expressions.
///
/// [`ResultEmpty`]: https://docs.rs/mingling/latest/mingling/type.ResultEmpty.html
/// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html
@@ -675,8 +690,8 @@ pub fn empty_result(_input: TokenStream) -> TokenStream {
///
/// # See also
///
-/// - [`macro@dispatcher_clap`] — For clap-powered argument parsing.
-/// - [`node!`] — For building custom [`Node`] paths.
+/// - `dispatcher_clap!` — For clap-powered argument parsing.
+/// - `node!` — For building custom [`Node`] paths.
/// - [`#[chain]`](attr.chain.html) — For processing the dispatched entry.
///
/// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html
@@ -687,95 +702,6 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
dispatcher::dispatcher(input)
}
-/// Prints formatted text to the current [`RenderResult`] buffer within a
-/// `#[renderer]` function.
-///
-/// Unlike `print!`, this macro writes to the in-memory `RenderResult` buffer
-/// rather than directly to stdout. The buffered output is flushed automatically
-/// when the renderer returns, allowing the framework to control output timing
-/// and capture (e.g., for testing or structural rendering to JSON/YAML).
-///
-/// This macro requires a mutable reference to a [`RenderResult`] named
-/// `__renderer_inner_result` to be in scope, which is automatically provided
-/// inside `#[renderer]` and `#[help]` functions.
-///
-/// # Syntax
-///
-/// Same as [`format!`] / [`print!`]:
-///
-/// ```rust,ignore
-/// r_print!("Hello, {}!", name);
-/// r_print!("Value: {value}");
-/// ```
-///
-/// # Example
-///
-/// ```rust,ignore
-/// use mingling::macros::{renderer, r_print};
-///
-/// #[renderer]
-/// fn show_greeting(prev: Greeting) {
-/// r_print!("Hello, {}!", *prev);
-/// }
-/// ```
-///
-/// # Difference from `r_println!`
-///
-/// `r_print!` does **not** append a newline. Use [`r_println!`] for newline-terminated output.
-///
-/// # Panics
-///
-/// Does not panic under normal circumstances. The underlying `RenderResult`
-/// buffer operations are infallible.
-///
-/// [`RenderResult`]: https://docs.rs/mingling/latest/mingling/struct.RenderResult.html
-#[proc_macro]
-pub fn r_print(input: TokenStream) -> TokenStream {
- render::r_print(input)
-}
-
-/// Prints formatted text followed by a newline to the current [`RenderResult`] buffer
-/// within a `#[renderer]` or `#[help]` function.
-///
-/// Unlike `println!`, this macro writes to the in-memory `RenderResult` buffer
-/// rather than directly to stdout. The buffered output is flushed automatically
-/// when the renderer returns.
-///
-/// This macro requires a mutable reference to a [`RenderResult`] named
-/// `__renderer_inner_result` to be in scope, which is automatically provided
-/// inside `#[renderer]` and `#[help]` functions.
-///
-/// # Syntax
-///
-/// Same as [`println!`]:
-///
-/// ```rust,ignore
-/// r_println!("Hello, {}!", name);
-/// r_println!("Value: {value}");
-/// r_println!(); // just a newline
-/// ```
-///
-/// # Example
-///
-/// ```rust,ignore
-/// use mingling::macros::{renderer, r_println};
-///
-/// #[renderer]
-/// fn show_greeting(prev: Greeting) {
-/// r_println!("Hello, {}!", *prev);
-/// }
-/// ```
-///
-/// # See also
-///
-/// - [`r_print!`] for output without a trailing newline.
-///
-/// [`RenderResult`]: https://docs.rs/mingling/latest/mingling/struct.RenderResult.html
-#[proc_macro]
-pub fn r_println(input: TokenStream) -> TokenStream {
- render::r_println(input)
-}
-
/// Declares a chain processing step that transforms one type into another
/// within a Mingling pipeline.
///
@@ -876,7 +802,7 @@ pub fn r_println(input: TokenStream) -> TokenStream {
/// # Sync Example with Resource Injection
///
/// ```rust,ignore
-/// use mingling::macros::{chain, pack, gen_program, r_println};
+/// use mingling::macros::{chain, pack, gen_program};
///
/// #[derive(Default, Clone)]
/// struct UserName(String);
@@ -886,7 +812,6 @@ pub fn r_println(input: TokenStream) -> TokenStream {
///
/// #[chain]
/// fn greet(prev: HelloEntry, user_name: &UserName, count: &mut u64) -> Next {
-/// r_println!("User: {:?}", user_name);
/// *count += 1;
/// Greeting::new(format!("Hello, {}!", user_name.0))
/// }
@@ -954,32 +879,32 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
/// The `#[renderer]` attribute converts a function into a renderer by:
/// 1. Generating a hidden struct implementing the `Renderer` trait.
/// 2. Registering the renderer mapping in the global renderer registry.
-/// 3. Keeping the original function for direct calls. When called directly,
-/// a new `RenderResult` is created and the renderer function writes its
-/// output directly to the current terminal output buffer.
-///
-/// Inside a `#[renderer]` function, you can use `r_print!` and `r_println!`
-/// to write output to the `RenderResult` buffer.
+/// 3. Keeping the original function for direct calls.
///
/// # Syntax
///
/// ```rust,ignore
/// #[renderer]
-/// fn render_my_type(prev: MyType) {
-/// r_println!("Output: {:?}", *prev);
+/// fn render_my_type(prev: MyType) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "Output: {:?}", *prev);
+/// result
/// }
/// ```
///
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::{renderer, r_println, pack, gen_program};
+/// use mingling::macros::{renderer, pack, gen_program};
+/// use std::io::Write;
///
/// pack!(Greeting = String);
///
/// #[renderer]
-/// fn render_greeting(prev: Greeting) {
-/// r_println!("Hello, {}!", *prev);
+/// fn render_greeting(prev: Greeting) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "Hello, {}!", *prev);
+/// result
/// }
/// ```
///
@@ -998,13 +923,17 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// ```rust,ignore
/// #[renderer]
-/// fn fallback_dispatcher_not_found(prev: ErrorDispatcherNotFound) {
-/// r_println!("Unknown command: {}", prev.join(", "));
+/// fn fallback_dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "Unknown command: {}", prev.join(", "));
+/// result
/// }
///
/// #[renderer]
-/// fn fallback_renderer_not_found(prev: ErrorRendererNotFound) {
-/// r_println!("No renderer for `{}`", *prev);
+/// fn fallback_renderer_not_found(prev: ErrorRendererNotFound) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "No renderer for `{}`", *prev);
+/// result
/// }
/// ```
#[proc_macro_attribute]
@@ -1159,7 +1088,7 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream {
/// Creates a packed entry value from a list of string literals.
///
/// This is a convenience macro for constructing entry wrapper types (created
-/// via [`pack!`] or [`dispatcher!`]) with test data, typically used in unit tests
+/// via `pack!` or `dispatcher!`) with test data, typically used in unit tests
/// or quick prototypes.
///
/// # Syntax
@@ -1190,8 +1119,8 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// # See also
///
-/// - [`pack!`] — For creating the wrapper types used with `entry!`.
-/// - [`dispatcher!`] — Which implicitly creates entry types via `pack!`.
+/// - `pack!` — For creating the wrapper types used with `entry!`.
+/// - `dispatcher!` — Which implicitly creates entry types via `pack!`.
#[cfg(feature = "extra_macros")]
#[proc_macro]
pub fn entry(input: TokenStream) -> TokenStream {
@@ -1218,10 +1147,10 @@ pub fn register_help(input: TokenStream) -> TokenStream {
/// Registers a dispatcher at compile time for the `dispatch_tree` feature.
///
-/// This macro is called internally by [`dispatcher!`] when the `dispatch_tree`
+/// This macro is called internally by `dispatcher!` when the `dispatch_tree`
/// feature is enabled. Each call stores the node name into the global
/// `COMPILE_TIME_DISPATCHERS` registry and generates a static variable for the
-/// dispatcher instance. This data is later consumed by [`gen_program!`] to
+/// dispatcher instance. This data is later consumed by `gen_program!` to
/// generate a character-level **Trie** for efficient command dispatch.
///
/// The trie dispatch works by grouping commands by their character prefix,
@@ -1238,7 +1167,7 @@ pub fn register_help(input: TokenStream) -> TokenStream {
///
/// # See also
///
-/// - [`dispatcher!`] — The primary way to declare dispatchers (calls this internally).
+/// - `dispatcher!` — The primary way to declare dispatchers (calls this internally).
/// - `dispatch_tree_gen` module — The trie generation logic.
#[proc_macro]
pub fn register_dispatcher(input: TokenStream) -> TokenStream {
@@ -1258,33 +1187,39 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
/// The macro works by:
/// 1. Generating a hidden struct implementing the `HelpRequest` trait.
/// 2. Registering the help mapping in the global `HELP_REQUESTS` registry.
-/// 3. Keeping the original function for direct calls (with a dummy `RenderResult`).
+/// 3. Keeping the original function for direct calls.
///
-/// Inside a `#[help]` function, you can use [`r_print!`] and [`r_println!`]
-/// to write help text to the `RenderResult` buffer.
+/// Inside a `#[help]` function, you must manually create a `RenderResult`
+/// and return it. Use `writeln!` on the result to
+/// write help text.
///
/// # Syntax
///
/// ```rust,ignore
/// #[help]
-/// fn help_my_entry(prev: MyEntry) {
-/// r_println!("Usage: myapp myentry [options]");
-/// r_println!(" Does something useful.");
+/// fn help_my_entry(prev: MyEntry) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "Usage: myapp myentry [options]");
+/// writeln!(result, " Does something useful.");
+/// result
/// }
/// ```
///
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::{help, r_println, pack, gen_program};
-/// use mingling::{prelude::*, setup::BasicProgramSetup};
+/// use mingling::macros::{help, pack, gen_program};
+/// use mingling::{prelude::*, setup::BasicProgramSetup, RenderResult};
+/// use std::io::Write;
///
/// pack!(MyEntry = Vec<String>);
///
/// #[help]
-/// fn help_my_entry(prev: MyEntry) {
-/// r_println!("Usage: myapp greet [name]");
-/// r_println!("Greets the user.");
+/// fn help_my_entry(prev: MyEntry) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "Usage: myapp greet [name]");
+/// writeln!(result, "Greets the user.");
+/// result
/// }
///
/// fn main() {
@@ -1299,13 +1234,13 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
///
/// - The function must have exactly one parameter (the entry type to provide help for).
/// - The parameter type must be a single-segment type path (e.g., `MyEntry`, not `other::MyEntry`).
-/// - The function must return `()`.
+/// - The function must return `RenderResult`.
/// - The function cannot be async.
///
/// # See also
///
/// - [`BasicProgramSetup`] — The setup that enables `--help` and `-h` flag processing.
-/// - [`r_print!`] / [`r_println!`] — For outputting help text.
+/// - `RenderResult` — The return type for help functions.
/// - [`#[chain]`](attr.chain.html) — For processing the dispatched entry after help.
///
/// [`BasicProgramSetup`]: https://docs.rs/mingling/latest/mingling/setup/struct.BasicProgramSetup.html
@@ -1391,7 +1326,7 @@ pub fn derive_enum_tag(input: TokenStream) -> TokenStream {
enum_tag::derive_enum_tag(input)
}
-/// Derive macro for [`StructuralData`], marking a type as eligible for structured
+/// Derive macro for `StructuralData`, marking a type as eligible for structured
/// structured output (JSON / YAML / TOML / RON).
///
/// The type must also implement `serde::Serialize` — the generated
@@ -1479,7 +1414,8 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::{dispatcher, chain, renderer, gen_program};
+/// use mingling::macros::{dispatcher, chain, renderer, gen_program, RenderResult};
+/// use std::io::Write;
///
/// dispatcher!("hello", HelloCommand => HelloEntry);
///
@@ -1489,8 +1425,10 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
/// }
///
/// #[renderer]
-/// fn render(prev: /* ... */) {
-/// r_println!("Done!");
+/// fn render(prev: /* ... */) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// writeln!(result, "Done!");
+/// result
/// }
///
/// // Collect everything:
@@ -1595,9 +1533,11 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
#[allow(unused)]
#[doc(hidden)]
#[::mingling::macros::renderer]
- pub fn __render_completion(prev: CompletionSuggest) {
+ pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
+ let result = ::mingling::RenderResult::default();
let (ctx, suggest) = prev.inner;
::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
+ result
}
};
@@ -1766,9 +1706,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
/// impl ProgramCollect for MyProgram {
/// type Enum = MyProgram;
/// type ResultEmpty = ResultEmpty;
-/// fn render(any, r) { /* dispatches to all registered renderers */ }
+/// fn render(any) -> RenderResult { /* dispatches to all registered renderers */ }
/// fn do_chain(any) -> ChainProcess { /* dispatches to all registered chain steps */ }
-/// fn render_help(any, r) { /* dispatches to all registered help handlers */ }
+/// fn render_help(any) -> RenderResult { /* dispatches to all registered help handlers */ }
/// fn has_renderer(any) -> bool { /* checks renderer registry */ }
/// fn has_chain(any) -> bool { /* checks chain registry */ }
/// // (with comp feature) fn do_comp(...)
@@ -2003,12 +1943,15 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream {
let comp = quote! {};
// Build render function arms from stored entries
- let render_fn = if renderer_tokens.is_empty() {
- quote! {
- fn render(_any: ::mingling::AnyOutput<Self::Enum>, _renderer_inner_result: &mut ::mingling::RenderResult) {}
- }
- } else {
- let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
+ let render_fn =
+ if renderer_tokens.is_empty() {
+ quote! {
+ fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ ::mingling::RenderResult::default()
+ }
+ }
+ } else {
+ let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
let (struct_ident, variant_ident) = parse_entry_pair(entry);
let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
@@ -2017,19 +1960,19 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream {
// 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, __renderer_inner_result);
+ <#resolved_struct as ::mingling::Renderer>::render(value)
}
}
}).collect();
- quote! {
- fn render(any: ::mingling::AnyOutput<Self::Enum>, __renderer_inner_result: &mut ::mingling::RenderResult) {
- match any.member_id {
- #(#render_arms)*
- _ => (),
+ quote! {
+ fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ match any.member_id {
+ #(#render_arms)*
+ _ => ::mingling::RenderResult::default(),
+ }
}
}
- }
- };
+ };
// Build do_chain function (async and sync versions)
let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
@@ -2145,12 +2088,12 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream {
}
#render_fn
#do_chain_fn
- fn render_help(any: ::mingling::AnyOutput<Self::Enum>, __renderer_inner_result: &mut ::mingling::RenderResult) {
+ fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
#[allow(unused_imports)]
#(#pathf_uses)*
match any.member_id {
#(#help_tokens)*
- _ => (),
+ _ => ::mingling::RenderResult::default(),
}
}
fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
diff --git a/mingling_macros/src/render.rs b/mingling_macros/src/render.rs
deleted file mode 100644
index 10fce7d..0000000
--- a/mingling_macros/src/render.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-use proc_macro::TokenStream;
-use quote::quote;
-use syn::parse::Parser;
-use syn::{Expr, Token};
-
-pub fn r_print(input: TokenStream) -> TokenStream {
- // Parse the input as format arguments
- let parser = syn::punctuated::Punctuated::<Expr, Token![,]>::parse_terminated;
- let format_args = match parser.parse(input) {
- Ok(args) => args,
- Err(e) => return e.to_compile_error().into(),
- };
-
- // Build the format macro call
- let format_call = if format_args.is_empty() {
- quote! { ::std::format!("") }
- } else {
- let args_iter = format_args.iter();
- quote! { ::std::format!(#(#args_iter),*) }
- };
-
- let expanded = quote! {
- {
- let formatted = #format_call;
- ::mingling::RenderResult::print(__renderer_inner_result, &formatted)
- }
- };
-
- expanded.into()
-}
-
-pub fn r_println(input: TokenStream) -> TokenStream {
- // Parse the input as format arguments
- let parser = syn::punctuated::Punctuated::<Expr, Token![,]>::parse_terminated;
- let format_args = match parser.parse(input) {
- Ok(args) => args,
- Err(e) => return e.to_compile_error().into(),
- };
-
- // Build the format macro call
- let format_call = if format_args.is_empty() {
- quote! { ::std::format!("") }
- } else {
- let args_iter = format_args.iter();
- quote! { ::std::format!(#(#args_iter),*) }
- };
-
- let expanded = quote! {
- {
- let formatted = #format_call;
- ::mingling::RenderResult::println(__renderer_inner_result, &formatted)
- }
- };
-
- expanded.into()
-}
diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/renderer.rs
index 880c50f..f47511c 100644
--- a/mingling_macros/src/renderer.rs
+++ b/mingling_macros/src/renderer.rs
@@ -6,18 +6,33 @@ use syn::{ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Extracts and returns the return type from the function signature (or None for `()` / no return type).
-fn extract_return_type(sig: &Signature) -> Option<syn::Type> {
+/// Validates that the function returns `::mingling::RenderResult`.
+fn validate_render_result_return(sig: &Signature) -> syn::Result<()> {
match &sig.output {
ReturnType::Type(_, ty) => {
+ // Check if the return type is RenderResult
match &**ty {
- // `()` means no custom return type
- Type::Tuple(tuple) if tuple.elems.is_empty() => None,
- // Any other return type is allowed
- custom_ty => Some((*custom_ty).clone()),
+ Type::Path(type_path) => {
+ let segments = &type_path.path.segments;
+ let last_seg = segments.last().map(|s| s.ident.to_string());
+ match last_seg.as_deref() {
+ Some("RenderResult") => Ok(()),
+ _ => Err(syn::Error::new(
+ ty.span(),
+ "Renderer function must return `RenderResult`",
+ )),
+ }
+ }
+ _ => Err(syn::Error::new(
+ ty.span(),
+ "Renderer function must return `RenderResult`",
+ )),
}
}
- ReturnType::Default => None,
+ ReturnType::Default => Err(syn::Error::new(
+ sig.span(),
+ "Renderer function must have a return type `-> RenderResult`",
+ )),
}
}
@@ -44,8 +59,10 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
Err(e) => return e.to_compile_error().into(),
};
- // Validate return type – now returns Some(type) if custom type, None if ()
- let return_type = extract_return_type(&input_fn.sig);
+ // Validate that the function returns RenderResult
+ if let Err(e) = validate_render_result_return(&input_fn.sig) {
+ return e.to_compile_error().into();
+ }
// Get function body statements
let fn_body_stmts: Vec<syn::Stmt> = input_fn.block.stmts.clone();
@@ -76,23 +93,6 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> 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();
- // Determine public return type and the expression to return dummy_r
- let (public_return_type, result_return) = if let Some(custom_ty) = &return_type {
- // User specified a custom return type (e.g. -> String)
- let ret_ty = quote! { #custom_ty };
- let expr = quote! { dummy_r.into() };
- (ret_ty, expr)
- } else {
- // Return type is () — no custom return type specified
- let ret_ty = quote! { () };
- let expr = quote! {
- if !dummy_r.is_empty() {
- ::std::println!("{}", &*dummy_r);
- }
- };
- (ret_ty, expr)
- };
-
let inner_body_with_resources = if has_mut_resources {
let mut wrapped = quote! { #(#fn_body_stmts)* };
for res in mut_resources.iter().rev() {
@@ -110,8 +110,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
};
// Build the Renderer::render body with resource injection
- // Renderer::render returns (), output goes through __renderer_inner_result parameter.
- // Resources are injected from the program context here.
+ // The user's body now directly creates and returns a RenderResult.
let render_fn_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
@@ -121,22 +120,13 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
quote! { #inner_body_with_resources }
};
- // Build the original function body
- // The original function preserves the user's signature and return type.
+ // The original function preserves the user's exact signature and body.
// Resource parameters are passed directly by the caller, NOT injected from context.
- let original_fn_body = {
- quote! {
- let mut dummy_r = ::mingling::RenderResult::default();
- {
- let __renderer_inner_result = &mut dummy_r;
- #(#fn_body_stmts)*
- }
- #result_return
- }
- };
-
- // Keep the original function signature unchanged (same params as user wrote)
let original_inputs = input_fn.sig.inputs.clone();
+ let original_return_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => quote! { #ty },
+ ReturnType::Default => unreachable!("Already validated that return type is RenderResult"),
+ };
let expanded = quote! {
#(#fn_attrs)*
@@ -149,15 +139,15 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
impl ::mingling::Renderer for #struct_name {
type Previous = #previous_type;
- fn render(#prev_param: Self::Previous, __renderer_inner_result: &mut ::mingling::RenderResult) {
+ fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult {
#render_fn_body
}
}
- // Keep the original function for internal use (without r parameter)
+ // Keep the original function unchanged
#(#fn_attrs)*
- #vis fn #fn_name(#original_inputs) -> #public_return_type {
- #original_fn_body
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
}
};
diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/res_injection.rs
index f2952cc..09da889 100644
--- a/mingling_macros/src/res_injection.rs
+++ b/mingling_macros/src/res_injection.rs
@@ -160,10 +160,7 @@ pub(crate) fn generate_immut_resource_bindings<'a>(
/// Generates a unique binding name for a mutable resource variable.
fn mut_res_binding_name(var_name: &Ident) -> Ident {
- syn::Ident::new(
- &format!("__{}_binding", var_name),
- var_name.span(),
- )
+ syn::Ident::new(&format!("__{}_binding", var_name), var_name.span())
}
/// Wraps the function body in nested `__modify_res_and_return_route` closures for
diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/structural_data.rs
index 37fee0f..5350d7e 100644
--- a/mingling_macros/src/structural_data.rs
+++ b/mingling_macros/src/structural_data.rs
@@ -222,7 +222,6 @@ impl syn::parse::Parse for PackStructuralInput {
/// impl ::mingling::StructuralData for Info {}
/// ```
pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
-
// Parse the same input as group!
let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput);
@@ -269,11 +268,18 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
proc_macro2::Span::call_site(),
);
- // Generate the appropriate `use` statement
+ // Generate the appropriate `use` statement for the original type
+ // (consistent with gen_type_use in group_impl.rs)
let type_use = if type_path.path.segments.len() > 1 {
quote! { #[allow(unused_imports)] use #type_path; }
} else {
- let ident = &type_name;
+ let ident = type_path
+ .path
+ .segments
+ .last()
+ .expect("TypePath must have at least one segment")
+ .ident
+ .clone();
quote! { #[allow(unused_imports)] use super::#ident; }
};