diff options
Diffstat (limited to 'mingling_macros/src')
| -rw-r--r-- | mingling_macros/src/dispatcher.rs | 5 | ||||
| -rw-r--r-- | mingling_macros/src/dispatcher_clap.rs | 7 | ||||
| -rw-r--r-- | mingling_macros/src/help.rs | 38 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 311 | ||||
| -rw-r--r-- | mingling_macros/src/render.rs | 56 | ||||
| -rw-r--r-- | mingling_macros/src/renderer.rs | 82 | ||||
| -rw-r--r-- | mingling_macros/src/res_injection.rs | 5 | ||||
| -rw-r--r-- | mingling_macros/src/structural_data.rs | 12 |
8 files changed, 206 insertions, 310 deletions
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; } }; |
