aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/chain.rs216
-rw-r--r--mingling_macros/src/dispatcher.rs2
-rw-r--r--mingling_macros/src/group_impl.rs4
-rw-r--r--mingling_macros/src/grouped.rs (renamed from mingling_macros/src/groupped.rs)12
-rw-r--r--mingling_macros/src/help.rs45
-rw-r--r--mingling_macros/src/lib.rs120
-rw-r--r--mingling_macros/src/pack.rs2
-rw-r--r--mingling_macros/src/pack_err.rs8
-rw-r--r--mingling_macros/src/renderer.rs47
-rw-r--r--mingling_macros/src/res_injection.rs25
-rw-r--r--mingling_macros/src/structural_data.rs4
11 files changed, 190 insertions, 295 deletions
diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/chain.rs
index 5f72422..ef31854 100644
--- a/mingling_macros/src/chain.rs
+++ b/mingling_macros/src/chain.rs
@@ -9,8 +9,7 @@ use quote::{ToTokens, quote};
use syn::spanned::Spanned;
use syn::{Ident, ItemFn, Pat, ReturnType, Signature, Type, TypePath, parse_macro_input};
-/// Validates that the return type of the function is `Next`.
-/// Checks whether the return type is `()` (unit).
+/// Checks whether the return type is `()`
fn is_unit_return_type(sig: &Signature) -> bool {
match &sig.output {
ReturnType::Type(_, ty) => match &**ty {
@@ -21,48 +20,22 @@ fn is_unit_return_type(sig: &Signature) -> bool {
}
}
+/// Validates that the return type is acceptable.
+/// Accepts `()`, `Next`, `ChainProcess<...>`, or any type that can
+/// be converted to `ChainProcess` via `.into()` (i.e. any pack type).
fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
- // If return type is `()`, it's valid (no Next required)
+ // `()` or omitted is always valid
if is_unit_return_type(sig) {
return Ok(());
}
- match &sig.output {
- ReturnType::Type(_, ty) => match &**ty {
- Type::Path(type_path) => {
- let last_segment = type_path.path.segments.last().unwrap();
- if last_segment.ident != "Next" {
- return Err(syn::Error::new(
- ty.span(),
- "Chain function must return `Next` or `()`",
- )
- .to_compile_error());
- }
- }
- _ => {
- return Err(syn::Error::new(
- ty.span(),
- "Chain function must return `Next` or `()`",
- )
- .to_compile_error());
- }
- },
- ReturnType::Default => {
- return Err(syn::Error::new(
- sig.span(),
- "Chain function must specify a return type (must be `Next` or `()`)",
- )
- .to_compile_error());
- }
- }
Ok(())
}
-/// Builds the `proc` function implementation that serves as the actual chain
-/// entry point inside the generated `Chain` impl.
+/// Builds the `proc` function implementation inside the generated `Chain` impl.
///
-/// * Without resources: delegates directly to the original function.
-/// * With resources: inlines the body and prepends resource bindings.
+/// The user's function body is inlined directly, and its result is converted
+/// via `.into()` to `ChainProcess<ProgramType>`.
#[allow(unused_variables)]
fn generate_proc_fn(
has_resources: bool,
@@ -70,67 +43,55 @@ fn generate_proc_fn(
program_type: &proc_macro2::TokenStream,
previous_type: &TypePath,
prev_param: &Pat,
- fn_name: &Ident,
fn_body_stmts: &[syn::Stmt],
is_async_fn: bool,
is_unit_return: bool,
+ origin_return_type: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
- let body_stmts: &[syn::Stmt] = if is_unit_return && has_resources {
- let mut stmts = fn_body_stmts.to_vec();
- stmts.push(syn::Stmt::Expr(
- syn::parse_quote! {
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>
- ::to_chain(crate::ResultEmpty)
- },
- None,
- ));
- // Box::leak to get a &'static [syn::Stmt]
- Box::leak(Box::new(stmts))
- } else {
- fn_body_stmts
- };
-
let wrapped_body = if is_async_fn && !mut_resources.is_empty() {
- wrap_body_with_mut_resources_async(body_stmts, &mut_resources, program_type)
+ wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type)
} else {
- wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type)
+ wrap_body_with_mut_resources(fn_body_stmts, &mut_resources, program_type, is_unit_return)
};
- // When the function returns `()`, wrap the result with ResultEmpty
- let call_or_wrapped = if is_unit_return {
- if has_resources {
+ let proc_body = if is_unit_return {
+ let body_with_ending = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #wrapped_body;
+ <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>
+ ::to_chain(crate::ResultEmpty)
}
} else {
- let call = if is_async_fn {
- quote! { #fn_name(#prev_param).await; }
- } else {
- quote! { #fn_name(#prev_param); }
- };
quote! {
- #call
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>
+ #wrapped_body;
+ <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>
::to_chain(crate::ResultEmpty)
}
- }
- } else if has_resources {
- quote! {
- #(#immut_resource_stmts)*
- #wrapped_body
- }
+ };
+ quote! { #body_with_ending }
} else {
- let call = if is_async_fn {
- quote! { #fn_name(#prev_param).await.into() }
+ let body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #wrapped_body
+ }
} else {
- quote! { #fn_name(#prev_param).into() }
+ quote! { #wrapped_body }
};
+ // Convert the body result to `ChainProcess` using the user-declared
+ // return type as the source type for a fully-qualified `Into` call.
+ // This works for both:
+ // - `-> Next` / `-> ChainProcess`: identity `From<T> for T`
+ // - `-> PackType`: `Into<ChainProcess>` from pack!/derive
quote! {
- #call
+ let __chain_result = { #body };
+ <#origin_return_type as ::std::convert::Into<
+ ::mingling::ChainProcess<#program_type>
+ >>::into(__chain_result)
}
};
@@ -138,7 +99,7 @@ fn generate_proc_fn(
{
quote! {
async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
- #call_or_wrapped
+ #proc_body
}
}
}
@@ -147,73 +108,14 @@ fn generate_proc_fn(
{
quote! {
fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
- #call_or_wrapped
- }
- }
- }
-}
-
-/// Generates the original function signature (kept for backwards compatibility /
-/// internal use), with its return type changed to `impl Into<ChainProcess<..>>`.
-#[allow(unused_variables)]
-fn generate_original_fn(
- fn_attrs: &[syn::Attribute],
- vis: &syn::Visibility,
- fn_name: &Ident,
- inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
- fn_body: &syn::Block,
- is_async_fn: bool,
- program_type: &proc_macro2::TokenStream,
- is_unit_return: bool,
-) -> proc_macro2::TokenStream {
- // Both unit and Next return types need to produce `impl Into<ChainProcess<ProgramType>>`
- let return_type = quote! { impl Into<::mingling::ChainProcess<#program_type>> };
-
- let body = if is_unit_return {
- quote! {
- {
- #fn_body
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
- }
- }
- } else {
- quote! {
- {
- let _: crate::Next;
- let _: Next;
- #fn_body
- }
- }
- };
-
- #[cfg(feature = "async")]
- {
- let async_kw = if is_async_fn {
- quote! { async }
- } else {
- quote! {}
- };
- quote! {
- #(#fn_attrs)*
- #vis #async_kw fn #fn_name(#inputs) -> #return_type {
- #body
- }
- }
- }
-
- #[cfg(not(feature = "async"))]
- {
- quote! {
- #(#fn_attrs)*
- #vis fn #fn_name(#inputs) -> #return_type {
- #body
+ #proc_body
}
}
}
}
/// Assembles the final expanded output: hidden struct, `register_chain!` invocation,
-/// `Chain` impl with the `proc` method, and the original function.
+/// `Chain` impl with the `proc` method, and the preserved original function.
fn generate_struct_and_impl(
fn_attrs: &[syn::Attribute],
vis: &syn::Visibility,
@@ -222,7 +124,7 @@ fn generate_struct_and_impl(
previous_type_str: &proc_macro2::TokenStream,
program_type: &proc_macro2::TokenStream,
proc_fn: &proc_macro2::TokenStream,
- origin_proc_fn: &proc_macro2::TokenStream,
+ original_fn: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
#(#fn_attrs)*
@@ -238,8 +140,8 @@ fn generate_struct_and_impl(
#proc_fn
}
- // Keep the original function for internal use
- #origin_proc_fn
+ // Keep the original function unchanged
+ #original_fn
}
}
@@ -296,8 +198,6 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
};
// Prepare building blocks
- let sig = &input_fn.sig;
- let inputs = &sig.inputs;
let fn_body = &input_fn.block;
let mut fn_attrs = input_fn.attrs.clone();
fn_attrs.retain(|attr| !attr.path().is_ident("chain"));
@@ -315,36 +215,36 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Always use the default crate-defined program path
let program_type = crate::default_program_path();
- // Generate the `proc` function
+ // Extract the user's return type for the explicit Into turbofish
+ let origin_return_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => quote! { #ty },
+ ReturnType::Default => quote! { () },
+ };
+
+ // Generate the `proc` function for the Chain impl
let proc_fn = generate_proc_fn(
has_resources,
&resources,
&program_type,
&previous_type,
&prev_param,
- fn_name,
&fn_body.stmts,
#[cfg(feature = "async")]
is_async_fn,
#[cfg(not(feature = "async"))]
false,
is_unit_return,
+ &origin_return_type,
);
- // Generate the original function
- let origin_proc_fn = generate_original_fn(
- &fn_attrs,
- vis,
- fn_name,
- inputs,
- fn_body,
- #[cfg(feature = "async")]
- is_async_fn,
- #[cfg(not(feature = "async"))]
- false,
- &program_type,
- is_unit_return,
- );
+ // Preserve the original function untouched, with dead_code allowed
+ // since it may only be called through the Chain trait dispatch.
+ // Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility.
+ let original_fn = quote! {
+ #[allow(dead_code)]
+ #(#fn_attrs)*
+ #input_fn
+ };
// Assemble the final output
let previous_type_str = quote! { #previous_type };
@@ -356,7 +256,7 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
&previous_type_str,
&program_type,
&proc_fn,
- &origin_proc_fn,
+ &original_fn,
);
expanded.into()
diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/dispatcher.rs
index 2a7c850..3698ede 100644
--- a/mingling_macros/src/dispatcher.rs
+++ b/mingling_macros/src/dispatcher.rs
@@ -134,7 +134,7 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
::mingling::macros::node!(#command_name_str)
}
fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> {
- use ::mingling::Groupped;
+ use ::mingling::Grouped;
#pack::new(args).to_chain()
}
fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> {
diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/group_impl.rs
index 59da9dd..cac2734 100644
--- a/mingling_macros/src/group_impl.rs
+++ b/mingling_macros/src/group_impl.rs
@@ -124,7 +124,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream {
quote! {}
};
- // Generate the module with the Groupped implementation
+ // Generate the module with the Grouped implementation
let expanded = quote! {
#alias_stmt
#[allow(non_camel_case_types)]
@@ -133,7 +133,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Groupped<__MinglingProgram> for #type_name {
+ impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}
diff --git a/mingling_macros/src/groupped.rs b/mingling_macros/src/grouped.rs
index 8aee003..9014c37 100644
--- a/mingling_macros/src/groupped.rs
+++ b/mingling_macros/src/grouped.rs
@@ -2,7 +2,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Ident, parse_macro_input};
-pub fn derive_groupped(input: TokenStream) -> TokenStream {
+pub fn derive_grouped(input: TokenStream) -> TokenStream {
// Parse the input struct/enum
let input = parse_macro_input!(input as DeriveInput);
let struct_name = input.ident;
@@ -12,11 +12,11 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream {
let any_output_convert_impls =
proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident));
- // Generate the Groupped trait implementation
+ // Generate the Grouped trait implementation
let expanded = quote! {
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Groupped<#group_ident> for #struct_name {
+ impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
@@ -29,7 +29,7 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream {
}
#[cfg(feature = "structural_renderer")]
-pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
+pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
// Parse the input struct/enum
let input_parsed = parse_macro_input!(input as DeriveInput);
let struct_name = input_parsed.ident.clone();
@@ -39,14 +39,14 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
let any_output_convert_impls =
proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident));
- // Generate both Serialize and Groupped implementations
+ // Generate both Serialize and Grouped implementations
let expanded = quote! {
#[derive(serde::Serialize)]
#input_parsed
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Groupped<#group_ident> for #struct_name {
+ impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs
index 1903d07..6e0d12e 100644
--- a/mingling_macros/src/help.rs
+++ b/mingling_macros/src/help.rs
@@ -1,34 +1,16 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{Ident, ItemFn, ReturnType, Type, TypePath, parse_macro_input};
+use syn::{Ident, ItemFn, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Validates that the function returns `::mingling::RenderResult`.
-fn validate_render_result_return(sig: &syn::Signature) -> syn::Result<()> {
+/// Extracts the user's return type, returning `None` for no return type.
+fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> {
match &sig.output {
- ReturnType::Type(_, ty) => match &**ty {
- 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 `RenderResult`",
- )),
- },
- ReturnType::Default => Err(syn::Error::new(
- sig.span(),
- "Help function must have a return type `-> RenderResult`",
- )),
+ ReturnType::Type(_, ty) => Some(quote! { #ty }),
+ ReturnType::Default => None,
}
}
@@ -49,10 +31,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
Err(e) => return e.to_compile_error().into(),
};
- // Validate return type is RenderResult
- if let Err(e) = validate_render_result_return(&input_fn.sig) {
- return e.to_compile_error().into();
- }
+ // Determine the user's return type for preserving the original function
+ let user_return_type = extract_user_return_type(&input_fn.sig);
// Get the function body
let fn_body = &input_fn.block;
@@ -69,8 +49,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
let fn_name = &input_fn.sig.ident;
// Get original inputs to keep the original function
-
let original_inputs = input_fn.sig.inputs.clone();
+ let original_return_type = user_return_type.clone().unwrap_or(quote! { () });
// Generate internal name using snake_case
let internal_name = format!(
@@ -149,17 +129,18 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
type Entry = #entry_type;
fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult {
- #help_render_body
+ let __help_result = { #help_render_body };
+ ::std::convert::Into::into(__help_result)
}
}
::mingling::macros::register_help!(#entry_type, #struct_name);
// Keep the original function unchanged
-
+ #[allow(dead_code)]
#(#fn_attrs)*
- #vis fn #fn_name(#original_inputs) -> ::mingling::RenderResult {
- #fn_body
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
}
};
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index d0f603a..ea33577 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -13,7 +13,7 @@
//! ┌──────────────────────────────────────────────────────────────────┐
//! │ Phase 1: Declaration │
//! │ │
-//! │ dispatcher! pack! node! #[derive(Groupped)] │
+//! │ dispatcher! pack! node! #[derive(Grouped)] │
//! │ │ │ │ │ │
//! │ V V V V │
//! │ Declares Wraps a Builds Makes a type │
@@ -55,7 +55,7 @@
//! | `pack_err!` | Creates an error struct with automatic `name` field |
//! | `pack_err_structural!` | Like `pack_err!` but also derives `StructuralData` for structured output |
//! | `entry!` | Creates a packed entry from string literals |
-//! | [`#[derive(Groupped)]`](derive@Groupped) | Makes a type recognizable by the framework's type registry |
+//! | [`#[derive(Grouped)]`](derive@Grouped) | Makes a type recognizable by the framework's type registry |
//! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) |
//! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) |
//!
@@ -161,7 +161,7 @@ mod entry;
mod enum_tag;
#[cfg(feature = "extra_macros")]
mod group_impl;
-mod groupped;
+mod grouped;
mod help;
mod node;
mod pack;
@@ -265,7 +265,7 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
/// Registers an outside-type as a member of a program group without modifying its definition.
///
/// This macro allows you to use outside-types from external crates (like `std::io::Error`)
-/// within the Mingling framework by generating a `Groupped` implementation and registering
+/// within the Mingling framework by generating a `Grouped` implementation and registering
/// the type's simple name as an enum variant.
///
/// # Syntax
@@ -281,11 +281,11 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
///
/// The macro generates a module containing:
/// - A `use` import for the program path and the outside-type
-/// - An `impl Groupped<Program>` for the outside-type
+/// - An `impl Grouped<Program>` for the outside-type
/// - A `register_type!` call with the type's simple name
///
/// The type's simple name (e.g. `Error`) is used as the enum variant in the generated
-/// program enum, just like `#[derive(Groupped)]` or `pack!`.
+/// program enum, just like `#[derive(Grouped)]` or `pack!`.
///
/// # Example
///
@@ -297,7 +297,7 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
/// ```
///
/// After expansion, the type can be used in chains and renderers like any
-/// `#[derive(Groupped)]` type.
+/// `#[derive(Grouped)]` type.
///
/// This macro is only available with the `extra_macros` feature.
#[cfg(feature = "extra_macros")]
@@ -402,7 +402,7 @@ pub fn node(input: TokenStream) -> TokenStream {
/// - `AsRef<String>`, `AsMut<String>`
/// - `Default` if `String: Default`
/// - `Into<AnyOutput<ThisProgram>>`, `Into<ChainProcess<ThisProgram>>`
-/// - Implements `Groupped<ThisProgram>` with `member_id()` returning the enum variant
+/// - Implements `Grouped<ThisProgram>` with `member_id()` returning the enum variant
///
/// The struct is also registered via `register_type!` so that `gen_program!`
/// can include it in the program enum.
@@ -438,7 +438,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// Creates an error struct with a `name: String` field and optional `info: Type` field.
///
-/// This macro provides a concise way to define error types that implement `Groupped`
+/// This macro provides a concise way to define error types that implement `Grouped`
/// and are registered for inclusion in the program enum.
///
/// The `name` field is automatically set to the snake_case version of the struct name
@@ -461,7 +461,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// For `pack_err!(ErrorNotFound)`:
///
/// ```rust,ignore
-/// #[derive(::mingling::Groupped)]
+/// #[derive(::mingling::Grouped)]
/// pub struct ErrorNotFound {
/// name: String,
/// }
@@ -478,7 +478,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// For `pack_err!(ErrorNotDir = PathBuf)`:
///
/// ```rust,ignore
-/// #[derive(::mingling::Groupped)]
+/// #[derive(::mingling::Grouped)]
/// pub struct ErrorNotDir {
/// name: String,
/// info: PathBuf,
@@ -521,20 +521,25 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
pack_err::pack_err_structural(input)
}
-/// Early-returns an error from a `Result`, converting the `Ok` branch to a
-/// `ChainProcess`.
+/// Early-returns the error from a `Result`, converting the `Ok` branch to the
+/// next chain process value.
///
/// This macro is equivalent to:
/// ```rust,ignore
/// match expr {
/// Ok(r) => r,
-/// Err(e) => return ::mingling::Groupped::to_chain(e),
+/// Err(e) => return ::mingling::Routable::to_chain(e),
/// }
/// ```
///
-/// It is useful inside chain functions where you have a `Result<SomeType, SomeType>`
-/// where both types implement `Groupped` and want to propagate the error case
-/// as an early return via `Groupped::to_chain()`.
+/// It is useful inside chain functions where you have a `Result<SuccessType, ErrorType>`
+/// where both types implement `Routable` and you want to propagate the error case
+/// as an early return via `Routable::to_chain()`.
+///
+/// The key difference from a simple `?` operator is that `route!` converts **both**
+/// the success and error types into the chain process — the `Ok` value is unwrapped
+/// directly, while the `Err` value is converted via `Routable::to_chain()` and
+/// returned early.
///
/// # Example
///
@@ -542,7 +547,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
/// use mingling::macros::{chain, route};
///
/// #[chain]
-/// fn process(prev: SomeEntry) -> ChainProcess<ThisProgram> {
+/// fn process(prev: SomeEntry) -> Next {
/// let value = route!(current_dir().map_err(|e| ErrorEntry::new(e.to_string_lossy().to_string())));
/// // value is the PathBuf from current_dir()
/// value.to_chain()
@@ -555,7 +560,7 @@ pub fn route(input: TokenStream) -> TokenStream {
let expanded = quote! {
match #expr {
Ok(r) => r,
- Err(e) => return ::mingling::Groupped::to_chain(e),
+ Err(e) => return ::mingling::Routable::to_chain(e),
}
};
TokenStream::from(expanded)
@@ -613,7 +618,7 @@ pub fn route(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn empty_result(_input: TokenStream) -> TokenStream {
let expanded = quote! {
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
+ <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
};
TokenStream::from(expanded)
}
@@ -867,7 +872,8 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
///
/// - The function must have at least **one** parameter (the previous type in the chain).
/// - The first parameter must be taken **by move**.
-/// - The function must return `Next` (the type alias generated by `gen_program!`, which equals `ChainProcess<ProgramName>`).
+/// - The function may return `Next`, `ChainProcess<ProgramName>`, `()`, or omit the return type.
+/// - The original function signature is preserved unchanged.
/// - With the `async` feature, async functions are supported; without it, async functions are rejected.
#[proc_macro_attribute]
pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
@@ -1234,7 +1240,7 @@ 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 `RenderResult`.
+/// - The function may return `RenderResult`, `()`, or any type that implements `Into<RenderResult>`.
/// - The function cannot be async.
///
/// # See also
@@ -1249,10 +1255,10 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
help::help_attr(item)
}
-/// Derive macro for automatically implementing the `Groupped` trait on a struct.
+/// Derive macro for automatically implementing the `Grouped` trait on a struct.
///
-/// The `#[derive(Groupped)]` macro:
-/// 1. Implements `Groupped<crate::ThisProgram>`.
+/// The `#[derive(Grouped)]` macro:
+/// 1. Implements `Grouped<crate::ThisProgram>`.
/// 2. Registers the type via `register_type!` so it's included in the program enum.
/// 3. Generates `Into<AnyOutput<Group>>` and `Into<ChainProcess<Group>>` conversions.
/// 4. Adds `to_chain()` and `to_render()` methods to the struct.
@@ -1260,7 +1266,7 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Syntax
///
/// ```rust,ignore
-/// #[derive(Groupped)]
+/// #[derive(Grouped)]
/// struct MyStruct {
/// // ...
/// }
@@ -1269,9 +1275,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::Groupped;
+/// use mingling::macros::Grouped;
///
-/// #[derive(Groupped)]
+/// #[derive(Grouped)]
/// struct Greeting {
/// name: String,
/// }
@@ -1279,9 +1285,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// This is equivalent to using `pack!` but works with custom structs that
/// have named fields. For simple wrappers, prefer `pack!`.
-#[proc_macro_derive(Groupped, attributes(group))]
-pub fn derive_groupped(input: TokenStream) -> TokenStream {
- groupped::derive_groupped(input)
+#[proc_macro_derive(Grouped, attributes(group))]
+pub fn derive_grouped(input: TokenStream) -> TokenStream {
+ grouped::derive_grouped(input)
}
/// Derive macro for automatically implementing the `EnumTag` trait on an enum
@@ -1350,18 +1356,18 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream {
structural_data::derive_structural_data(input)
}
-/// Derive macro for implementing both `Groupped` and `serde::Serialize` on a struct.
+/// Derive macro for implementing both `Grouped` and `serde::Serialize` on a struct.
///
/// **This macro is only available with the `structural_renderer` feature.**
///
-/// This is identical to `#[derive(Groupped)]` but also adds `#[derive(serde::Serialize)]`
+/// This is identical to `#[derive(Grouped)]` but also adds `#[derive(serde::Serialize)]`
/// to the struct, which is required for the structural renderer to serialize output
/// to formats like JSON, YAML, TOML, or RON.
///
/// # Syntax
///
/// ```rust,ignore
-/// #[derive(GrouppedSerialize)]
+/// #[derive(GroupedSerialize)]
/// struct Info {
/// name: String,
/// age: i32,
@@ -1371,19 +1377,19 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::GrouppedSerialize;
+/// use mingling::GroupedSerialize;
/// use serde::Serialize;
///
-/// #[derive(GrouppedSerialize)]
+/// #[derive(GroupedSerialize)]
/// struct Info {
/// name: String,
/// age: i32,
/// }
/// ```
#[cfg(feature = "structural_renderer")]
-#[proc_macro_derive(GrouppedSerialize, attributes(group))]
-pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
- groupped::derive_groupped_serialize(input)
+#[proc_macro_derive(GroupedSerialize, attributes(group))]
+pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
+ grouped::derive_grouped_serialize(input)
}
/// Generates the program enum and all collected types, chains, and renderers.
@@ -1445,8 +1451,30 @@ pub fn gen_program(_input: TokenStream) -> TokenStream {
let comp_gen = quote! {};
TokenStream::from(quote! {
+ /// Alias for the current program type `crate::ThisProgram`
pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
+ impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram>
+ {
+ fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
+ }
+ other => other,
+ }
+ }
+
+ fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
+ }
+ }
+ }
+
#comp_gen
::mingling::macros::program_fallback_gen!();
::mingling::macros::program_final_gen!();
@@ -1473,7 +1501,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
#[doc(hidden)]
#[::mingling::macros::chain]
pub async fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Groupped;
+ use ::mingling::Grouped;
let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
match read_ctx {
@@ -1491,7 +1519,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
#[doc(hidden)]
#[::mingling::macros::chain]
pub fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Groupped;
+ use ::mingling::Grouped;
let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
match read_ctx {
@@ -1515,7 +1543,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
let comp_dispatcher = quote! {
#[doc(hidden)]
mod __internal_completion_mod {
- use ::mingling::Groupped;
+ use ::mingling::Grouped;
::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
::mingling::macros::pack!(
CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
@@ -1547,7 +1575,7 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
/// Registers a type into the global packed types registry for inclusion in
/// the program enum generated by `gen_program!`.
///
-/// This macro is called internally by `pack!` and `#[derive(Groupped)]`(`macro.derive_groupped.html`)
+/// This macro is called internally by `pack!` and `#[derive(Grouped)]`(`macro.derive_grouped.html`)
/// and is generally not needed in user code. However, it can be used for manual
/// registration if you are implementing custom type registration outside of
/// the standard macros.
@@ -1652,13 +1680,13 @@ pub fn register_renderer(input: TokenStream) -> TokenStream {
pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
#[cfg(feature = "structural_renderer")]
let pack_empty = quote! {
- #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Groupped, Default)]
+ #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)]
pub struct ResultEmpty;
};
#[cfg(not(feature = "structural_renderer"))]
let pack_empty = quote! {
- #[derive(::mingling::Groupped, Default)]
+ #[derive(::mingling::Grouped, Default)]
pub struct ResultEmpty;
};
@@ -1674,7 +1702,7 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
/// and its `ProgramCollect` implementation.
///
/// This is the core code generation macro that:
-/// 1. Collects all registered types (from `pack!`, `#[derive(Groupped)]`, etc.) and
+/// 1. Collects all registered types (from `pack!`, `#[derive(Grouped)]`, etc.) and
/// creates an enum with each type as a variant.
/// 2. Generates the `Display` implementation for the enum.
/// 3. Generates the `ProgramCollect` implementation that dispatches to all
diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/pack.rs
index 7f05232..0af1b4c 100644
--- a/mingling_macros/src/pack.rs
+++ b/mingling_macros/src/pack.rs
@@ -138,7 +138,7 @@ pub fn pack(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Groupped<#group_name> for #type_name {
+ impl ::mingling::Grouped<#group_name> for #type_name {
fn member_id() -> #group_name {
#group_name::#type_name
}
diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/pack_err.rs
index ba7cf17..a747aa9 100644
--- a/mingling_macros/src/pack_err.rs
+++ b/mingling_macros/src/pack_err.rs
@@ -42,7 +42,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
// Note: No longer derives Serialize under structural_renderer.
// Use pack_err_structural for structured output support.
let derive = quote! {
- #[derive(::mingling::Groupped)]
+ #[derive(::mingling::Grouped)]
};
let expanded = quote! {
@@ -75,7 +75,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
// Note: No longer derives Serialize under structural_renderer.
// Use pack_err_structural for structured output support.
let derive = quote! {
- #[derive(::mingling::Groupped)]
+ #[derive(::mingling::Grouped)]
};
let expanded = quote! {
@@ -150,7 +150,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
let snake_name = snake_case!(&name_str);
let expanded = quote! {
- #[derive(::mingling::Groupped, ::serde::Serialize)]
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
pub struct #type_name {
/// The snake_case name of this error, automatically set at compile time.
pub name: String,
@@ -179,7 +179,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
let snake_name = snake_case!(&name_str);
let expanded = quote! {
- #[derive(::mingling::Groupped, ::serde::Serialize)]
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
pub struct #type_name {
/// The snake_case name of this error, automatically set at compile time.
pub name: String,
diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/renderer.rs
index f47511c..d124ec9 100644
--- a/mingling_macros/src/renderer.rs
+++ b/mingling_macros/src/renderer.rs
@@ -1,38 +1,16 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input};
+use syn::{ItemFn, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Validates that the function returns `::mingling::RenderResult`.
-fn validate_render_result_return(sig: &Signature) -> syn::Result<()> {
+/// Extracts the user's return type, returning `None` for no return type.
+fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> {
match &sig.output {
- ReturnType::Type(_, ty) => {
- // Check if the return type is RenderResult
- match &**ty {
- 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 => Err(syn::Error::new(
- sig.span(),
- "Renderer function must have a return type `-> RenderResult`",
- )),
+ ReturnType::Type(_, ty) => Some(quote! { #ty }),
+ ReturnType::Default => None,
}
}
@@ -59,10 +37,8 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
Err(e) => return e.to_compile_error().into(),
};
- // Validate that the function returns RenderResult
- if let Err(e) = validate_render_result_return(&input_fn.sig) {
- return e.to_compile_error().into();
- }
+ // Determine the user's return type and whether it needs to be converted to RenderResult
+ let user_return_type = extract_user_return_type(&input_fn.sig);
// Get function body statements
let fn_body_stmts: Vec<syn::Stmt> = input_fn.block.stmts.clone();
@@ -123,10 +99,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// 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_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 original_return_type = user_return_type.clone().unwrap_or(quote! { () });
let expanded = quote! {
#(#fn_attrs)*
@@ -140,11 +113,13 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
type Previous = #previous_type;
fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult {
- #render_fn_body
+ let __renderer_result = { #render_fn_body };
+ ::std::convert::Into::into(__renderer_result)
}
}
// Keep the original function unchanged
+ #[allow(dead_code)]
#(#fn_attrs)*
#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 09da889..606b9a6 100644
--- a/mingling_macros/src/res_injection.rs
+++ b/mingling_macros/src/res_injection.rs
@@ -163,8 +163,10 @@ fn mut_res_binding_name(var_name: &Ident) -> Ident {
syn::Ident::new(&format!("__{}_binding", var_name), var_name.span())
}
-/// Wraps the function body in nested `__modify_res_and_return_route` closures for
-/// each mutable resource parameter (sync version).
+/// Wraps the function body in mutable resource closures (sync version).
+///
+/// For unit return types: uses `modify_res` (closure returns `()`).
+/// For non-unit return types: uses `__modify_res_and_return_route` (closure returns `ChainProcess<C>`).
///
/// The innermost closure gets the original body, and each mutable parameter wraps
/// outward from last to first.
@@ -172,6 +174,7 @@ pub(crate) fn wrap_body_with_mut_resources(
fn_body_stmts: &[syn::Stmt],
mut_resources: &[&ResourceInjection],
program_type: &proc_macro2::TokenStream,
+ is_unit_return: bool,
) -> proc_macro2::TokenStream {
let mut wrapped = quote! {
#(#fn_body_stmts)*
@@ -180,11 +183,19 @@ pub(crate) fn wrap_body_with_mut_resources(
for res in mut_resources {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
- wrapped = quote! {
- ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| {
- #wrapped
- }).into()
- };
+ if is_unit_return {
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ } else {
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
}
wrapped
diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/structural_data.rs
index 5350d7e..74bcf09 100644
--- a/mingling_macros/src/structural_data.rs
+++ b/mingling_macros/src/structural_data.rs
@@ -176,7 +176,7 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Groupped<#program_path> for #type_name {
+ impl ::mingling::Grouped<#program_path> for #type_name {
fn member_id() -> #program_path {
#program_path::#type_name
}
@@ -297,7 +297,7 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Groupped<__MinglingProgram> for #type_name {
+ impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}