aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/attr
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src/attr')
-rw-r--r--mingling_macros/src/attr/chain.rs374
-rw-r--r--mingling_macros/src/attr/completion.rs250
-rw-r--r--mingling_macros/src/attr/dispatcher_clap.rs241
-rw-r--r--mingling_macros/src/attr/help.rs231
-rw-r--r--mingling_macros/src/attr/program_setup.rs116
-rw-r--r--mingling_macros/src/attr/renderer.rs274
6 files changed, 1486 insertions, 0 deletions
diff --git a/mingling_macros/src/attr/chain.rs b/mingling_macros/src/attr/chain.rs
new file mode 100644
index 0000000..dc28a39
--- /dev/null
+++ b/mingling_macros/src/attr/chain.rs
@@ -0,0 +1,374 @@
+#![allow(clippy::too_many_arguments)]
+
+use crate::res_injection::{
+ ResourceInjection, extract_args_info, generate_immut_resource_bindings,
+ wrap_body_with_mut_resources, wrap_body_with_mut_resources_async,
+};
+use proc_macro::TokenStream;
+use quote::{ToTokens, quote};
+use syn::spanned::Spanned;
+use syn::{Ident, ItemFn, Pat, ReturnType, Signature, Type, TypePath, parse_macro_input};
+
+/// Checks whether the return type is `()`
+fn is_unit_return_type(sig: &Signature) -> bool {
+ match &sig.output {
+ ReturnType::Type(_, ty) => match &**ty {
+ Type::Tuple(tuple) => tuple.elems.is_empty(),
+ _ => false,
+ },
+ ReturnType::Default => true,
+ }
+}
+
+/// Validates that the return type is acceptable.
+/// Accepts `()`, `Next`, `ChainProcess<...>`, or any type that can
+/// be converted to `ChainProcess` via `.into()` (i.e. any pack type).
+fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
+ // `()` or omitted is always valid
+ if is_unit_return_type(sig) {
+ return Ok(());
+ }
+
+ Ok(())
+}
+
+/// Builds the `proc` function implementation inside the generated `Chain` impl.
+///
+/// Instead of inlining the user's body, the trait method calls the original
+/// function by name, with resources injected from the application context.
+fn generate_proc_fn(
+ fn_name: &Ident,
+ has_resources: bool,
+ resources: &[ResourceInjection],
+ program_type: &proc_macro2::TokenStream,
+ previous_type: &TypePath,
+ is_async_fn: bool,
+ is_unit_return: bool,
+) -> 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();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ // Build the call to the original function with resource arguments injected.
+ // The variable names come from the resource injection bindings
+ // (immut bindings are `let #name = …`, mut closures receive `|#name: &mut T|`),
+ // which match the original function's parameter names.
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ let call = quote! { #fn_name(#fixed_prev, #(#resource_args),*) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
+ } else {
+ let call = quote! { #fn_name(#fixed_prev) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
+ };
+
+ // Convert the function call to a syn::Stmt so existing wrapping functions can use it.
+ // For non-unit returns, wrap in `to_chain()` so mutable-resource closures
+ // return `ChainProcess<C>` (as required by `__modify_res_and_return_route`).
+ let fn_call_expr: syn::Expr = if is_unit_return {
+ syn::parse_quote! { #fn_call }
+ } else {
+ syn::parse_quote! { ::mingling::Routable::<#program_type>::to_chain(#fn_call) }
+ };
+ let fn_call_stmt = syn::Stmt::Expr(fn_call_expr, None);
+
+ let wrapped_body = if is_async_fn && !mut_resources.is_empty() {
+ wrap_body_with_mut_resources_async(&[fn_call_stmt], &mut_resources, program_type)
+ } else {
+ wrap_body_with_mut_resources(
+ &[fn_call_stmt],
+ &mut_resources,
+ program_type,
+ is_unit_return,
+ )
+ };
+
+ let proc_body = if is_unit_return {
+ let body_with_ending = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #wrapped_body;
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
+ ::to_chain(crate::ResultEmpty)
+ }
+ } else {
+ quote! {
+ #wrapped_body;
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
+ ::to_chain(crate::ResultEmpty)
+ }
+ };
+ quote! { #body_with_ending }
+ } else {
+ let body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #wrapped_body
+ }
+ } else {
+ quote! { #wrapped_body }
+ };
+ quote! {
+ let __chain_result = { #body };
+ ::mingling::Routable::<#program_type>::to_chain(__chain_result)
+ }
+ };
+
+ #[cfg(feature = "async")]
+ {
+ quote! {
+ async fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ #proc_body
+ }
+ }
+ }
+
+ #[cfg(not(feature = "async"))]
+ {
+ quote! {
+ fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ #proc_body
+ }
+ }
+ }
+}
+
+/// Assembles the final expanded output: hidden struct, `register_chain!` invocation,
+/// `Chain` impl with the `proc` method, and the preserved original function.
+fn generate_struct_and_impl(
+ fn_attrs: &[syn::Attribute],
+ vis: &syn::Visibility,
+ struct_name: &Ident,
+ previous_type: &TypePath,
+ previous_type_str: &proc_macro2::TokenStream,
+ program_type: &proc_macro2::TokenStream,
+ proc_fn: &proc_macro2::TokenStream,
+ original_fn: &proc_macro2::TokenStream,
+) -> proc_macro2::TokenStream {
+ quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ #vis struct #struct_name;
+
+ ::mingling::macros::register_chain!(#previous_type_str, #struct_name);
+
+ impl ::mingling::Chain<#program_type> for #struct_name {
+ type Previous = #previous_type;
+
+ #proc_fn
+ }
+
+ // Keep the original function unchanged
+ #original_fn
+ }
+}
+
+/// Ensures the function is not async when the `async` feature is disabled.
+#[cfg(not(feature = "async"))]
+fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
+ if sig.asyncness.is_some() {
+ return Err(syn::Error::new(
+ sig.span(),
+ "Chain function cannot be async when async feature is disabled",
+ )
+ .to_compile_error());
+ }
+ Ok(())
+}
+
+pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Reject non-empty attribute arguments; #[chain] must be bare
+ if !attr.is_empty() {
+ return syn::Error::new(
+ attr.into_iter().next().unwrap().span().into(),
+ "#[chain] does not accept arguments",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Handle async feature gate
+ #[cfg(feature = "async")]
+ let is_async_fn = input_fn.sig.asyncness.is_some();
+
+ #[cfg(not(feature = "async"))]
+ {
+ if let Err(err) = reject_async(&input_fn.sig) {
+ return err.into();
+ }
+ }
+
+ // Check if return type is unit
+ let is_unit_return = is_unit_return_type(&input_fn.sig);
+
+ // Validate return type
+ if let Err(err) = validate_return_type(&input_fn.sig) {
+ return err.into();
+ }
+
+ // Extract the previous type, parameter name, and resource injection params
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ Ok(info) => info,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Prepare building blocks
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("chain"));
+ let vis = &input_fn.vis;
+ let fn_name = &input_fn.sig.ident;
+ let has_resources = !resources.is_empty();
+
+ // Generate struct name
+ let internal_name = format!(
+ "__internal_chain_{}",
+ just_fmt::snake_case!(fn_name.to_string())
+ );
+ let struct_name = Ident::new(&internal_name, fn_name.span());
+
+ // Always use the default crate-defined program path
+ let program_type = crate::default_program_path();
+
+ // Generate the `proc` function for the Chain impl
+ let proc_fn = generate_proc_fn(
+ fn_name,
+ has_resources,
+ &resources,
+ &program_type,
+ &previous_type,
+ #[cfg(feature = "async")]
+ is_async_fn,
+ #[cfg(not(feature = "async"))]
+ false,
+ is_unit_return,
+ );
+
+ // Preserve the original function untouched
+ // Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility.
+ let original_fn = quote! {
+ #(#fn_attrs)*
+ #input_fn
+ };
+
+ // Assemble the final output
+ let previous_type_str = quote! { #previous_type };
+ let expanded = generate_struct_and_impl(
+ &fn_attrs,
+ vis,
+ &struct_name,
+ &previous_type,
+ &previous_type_str,
+ &program_type,
+ &proc_fn,
+ &original_fn,
+ );
+
+ expanded.into()
+}
+
+/// Builds a match arm for chain mapping
+pub(crate) fn build_chain_arm(
+ struct_name: &Ident,
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
+ let enum_variant = &previous_type.path.segments.last().unwrap().ident;
+ quote! {
+ #struct_name => #enum_variant,
+ }
+}
+
+/// Builds a match arm for chain existence check
+pub(crate) fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream {
+ let enum_variant = &previous_type.path.segments.last().unwrap().ident;
+ quote! {
+ Self::#enum_variant => true,
+ }
+}
+
+pub(crate) fn register_chain(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of arguments
+ let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
+
+ // Check that there are exactly two elements
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `PreviousType, StructName`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the two elements
+ let previous_type_expr = &input_parsed[0];
+ let struct_name_expr = &input_parsed[1];
+
+ // Convert expressions to TypePath and Ident
+ let previous_type = match syn::parse2::<TypePath>(previous_type_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
+ Ok(ident) => ident,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Record the chain mapping: previous_type => struct_name
+ let chain_entry = build_chain_arm(&struct_name, &previous_type);
+
+ // Record the chain existence check
+ let chain_exist_entry = build_chain_exist_arm(&previous_type);
+
+ let mut chains = crate::get_global_set(&crate::CHAINS).lock().unwrap();
+ let mut chain_exist = crate::get_global_set(&crate::CHAINS_EXIST).lock().unwrap();
+
+ let chain_entry_str = chain_entry.to_string();
+ let chain_exist_entry_str = chain_exist_entry.to_string();
+
+ // Check for duplicate variant before inserting
+ let variant_name = previous_type
+ .path
+ .segments
+ .last()
+ .unwrap()
+ .ident
+ .to_string();
+ if let Err(err) = crate::check_duplicate_variant(
+ &chains,
+ &chain_entry_str,
+ &variant_name,
+ "chain",
+ previous_type.span(),
+ ) {
+ return err.into();
+ }
+
+ chains.insert(chain_entry_str);
+ chain_exist.insert(chain_exist_entry_str);
+
+ quote! {}.into()
+}
diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs
new file mode 100644
index 0000000..3ced091
--- /dev/null
+++ b/mingling_macros/src/attr/completion.rs
@@ -0,0 +1,250 @@
+use crate::res_injection::{ResourceInjection, generate_immut_resource_bindings};
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input};
+
+#[cfg(feature = "comp")]
+pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Parse the attribute arguments such as HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)]
+ use crate::get_global_set;
+ let previous_type_path: TypePath = if attr.is_empty() {
+ return syn::Error::new(
+ proc_macro2::Span::call_site(),
+ "completion attribute requires a previous type argument, e.g. #[completion(HelloEntry)]",
+ )
+ .to_compile_error()
+ .into();
+ } else {
+ parse_macro_input!(attr as TypePath)
+ };
+ let previous_type_ident = &previous_type_path.path.segments.last().unwrap().ident;
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Completion function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Get the function signature parts
+ let sig = &input_fn.sig;
+ let inputs = &sig.inputs;
+ let output = &sig.output;
+
+ // Must have at least one parameter ctx
+ if inputs.is_empty() {
+ return syn::Error::new(
+ inputs.span(),
+ "Completion function must have at least one parameter: `ctx: &ShellContext`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the first param pattern and type for the ctx parameter
+ let first_arg = &inputs[0];
+ let _ctx_type = match first_arg {
+ FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
+ FnArg::Receiver(_) => {
+ return syn::Error::new(
+ first_arg.span(),
+ "Completion function cannot have self parameter",
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+ let fixed_ctx: Pat = syn::parse_quote!(ctx);
+
+ // Extract resources from params 2 through N, skipping ctx
+ let resources = match extract_resources_from_args(sig, 1) {
+ Ok(r) => r,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Get the function body
+ let fn_body = &input_fn.block;
+
+ // Get function attributes excluding the completion attribute
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("completion"));
+
+ // Get function visibility
+ let vis = &input_fn.vis;
+
+ // Get function name
+ let fn_name = &sig.ident;
+
+ // Generate internal name from function name using snake_case
+ let internal_name = format!(
+ "__internal_completion_{}",
+ just_fmt::snake_case!(fn_name.to_string())
+ );
+ let struct_name = Ident::new(&internal_name, fn_name.span());
+
+ let program_type = crate::default_program_path();
+ let has_resources = !resources.is_empty();
+ let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
+
+ // Generate immutable resource bindings
+ let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
+
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_ctx, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_ctx) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
+ } else {
+ let mut wrapped = fn_call;
+ for res in mut_resources.iter().rev() {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
+ wrapped
+ };
+
+ let comp_body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #inner_call
+ }
+ } else {
+ quote! { #inner_call }
+ };
+
+ // Generate the struct and implementation
+ // The `comp` trait method only takes `ctx` as the first parameter; resources are injected internally
+
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ #vis struct #struct_name;
+
+ impl ::mingling::Completion for #struct_name {
+ type Previous = #previous_type_path;
+
+ fn comp(#fixed_ctx: &::mingling::ShellContext) #output {
+ #comp_body
+ }
+ }
+
+ // Keep the original function for internal use
+ #(#fn_attrs)*
+ #vis fn #fn_name(#inputs) #output {
+ #fn_body
+ }
+ };
+
+ let completion_entry = quote! {
+ Self::#previous_type_ident => <#struct_name as ::mingling::Completion>::comp(ctx),
+ };
+
+ let mut completions = get_global_set(&crate::COMPLETIONS).lock().unwrap();
+ let completion_str = completion_entry.to_string();
+
+ // Check for duplicate variant before inserting
+ let variant_name = previous_type_ident.to_string();
+ if let Err(err) = crate::check_duplicate_variant(
+ &completions,
+ &completion_str,
+ &variant_name,
+ "completion",
+ previous_type_path.span(),
+ ) {
+ return err.into();
+ }
+
+ completions.insert(completion_str);
+
+ expanded.into()
+}
+
+/// Extract resource injection parameters from function arguments (skipping the first N params).
+fn extract_resources_from_args(
+ sig: &syn::Signature,
+ skip: usize,
+) -> syn::Result<Vec<ResourceInjection>> {
+ let mut resources = Vec::new();
+ for arg in sig.inputs.iter().skip(skip) {
+ match arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ let var_name = match &**pat {
+ Pat::Ident(pat_ident) => pat_ident.ident.clone(),
+ _ => {
+ return Err(syn::Error::new(
+ pat.span(),
+ "Resource injection parameter must be a simple identifier",
+ ));
+ }
+ };
+
+ let full_type = *(*ty).clone();
+
+ let (inner_type, is_ref, is_mut) = match &full_type {
+ Type::Reference(ref_type) => match &*ref_type.elem {
+ Type::Path(type_path) => {
+ let is_mut = ref_type.mutability.is_some();
+ (type_path.clone(), true, is_mut)
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Reference resource type must be a type path",
+ ));
+ }
+ },
+ Type::Path(_) => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Resource injection parameter must be a reference (`&T` or `&mut T`)",
+ ));
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Resource injection type must be a type path or reference",
+ ));
+ }
+ };
+
+ resources.push(ResourceInjection {
+ var_name,
+ full_type,
+ inner_type,
+ is_ref,
+ is_mut,
+ });
+ }
+ FnArg::Receiver(_) => {
+ return Err(syn::Error::new(
+ arg.span(),
+ "Resource injection parameter cannot be self",
+ ));
+ }
+ }
+ }
+ Ok(resources)
+}
diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs
new file mode 100644
index 0000000..40f7d47
--- /dev/null
+++ b/mingling_macros/src/attr/dispatcher_clap.rs
@@ -0,0 +1,241 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{
+ Ident, ItemStruct, LitBool, LitStr, Token,
+ parse::{Parse, ParseStream},
+ parse_macro_input,
+};
+
+/// Parsed key-value options after the first positional arguments
+struct ClapOptions {
+ /// `error = ErrorStruct`
+ error_struct: Option<Ident>,
+ /// `help = true` (bool only)
+ help_enabled: bool,
+}
+
+impl Parse for ClapOptions {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut error_struct = None;
+ let mut help_enabled = false;
+
+ while !input.is_empty() {
+ // Parse leading comma
+ input.parse::<Token![,]>()?;
+
+ // Allow trailing comma
+ if input.is_empty() {
+ break;
+ }
+
+ let key: Ident = input.parse()?;
+ input.parse::<Token![=]>()?;
+
+ if key == "error" {
+ let value: Ident = input.parse()?;
+ if error_struct.is_some() {
+ return Err(syn::Error::new(key.span(), "duplicate `error` key"));
+ }
+ error_struct = Some(value);
+ } else if key == "help" {
+ let value: LitBool = input.parse()?;
+ if value.value() == false {
+ // help = false is allowed but does nothing
+ help_enabled = false;
+ } else {
+ help_enabled = true;
+ }
+ } else {
+ return Err(syn::Error::new(
+ key.span(),
+ "unknown key, expected `error` or `help`",
+ ));
+ }
+ }
+
+ Ok(ClapOptions {
+ error_struct,
+ help_enabled,
+ })
+ }
+}
+
+/// Input for the dispatcher_clap attribute
+struct DispatcherClapInput {
+ /// `("cmd", Disp, ...)`
+ command_name: LitStr,
+ dispatcher_struct: Ident,
+ options: ClapOptions,
+}
+
+impl Parse for DispatcherClapInput {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ // Format: "cmd", Disp, ...
+ let command_name: LitStr = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let dispatcher_struct: Ident = input.parse()?;
+
+ let options = if input.is_empty() {
+ ClapOptions {
+ error_struct: None,
+ help_enabled: false,
+ }
+ } else {
+ input.parse::<ClapOptions>()?
+ };
+
+ Ok(DispatcherClapInput {
+ command_name,
+ dispatcher_struct,
+ options,
+ })
+ }
+}
+
+#[cfg(feature = "clap")]
+pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let attr_input = parse_macro_input!(attr as DispatcherClapInput);
+ let input_struct = parse_macro_input!(item as ItemStruct);
+ let struct_name = &input_struct.ident;
+
+ let program_path = crate::default_program_path();
+
+ let command_name_str = attr_input.command_name.value();
+ let dispatcher_struct = &attr_input.dispatcher_struct;
+ let options = &attr_input.options;
+
+ // Generate the `begin` method body
+ let begin_body = if let Some(ref error_struct) = options.error_struct {
+ quote! {
+ if ::mingling::this::<#program_path>().user_context.help {
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
+ }
+ match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) {
+ Ok(parsed) => ::mingling::Routable::<#program_path>::to_chain(parsed),
+ Err(e) => {
+ return ::mingling::Routable::<#program_path>::to_render(#error_struct::new(format!("{}", e.render().ansi())))
+ },
+ }
+ }
+ } else {
+ quote! {
+ if ::mingling::this::<#program_path>().user_context.help {
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
+ }
+ let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args)
+ .unwrap_or_else(|e| e.exit());
+ ::mingling::Routable::<#program_path>::to_chain(parsed)
+ }
+ };
+
+ // Generate the error pack type
+ let error_pack = options.error_struct.as_ref().map(|error_struct| {
+ quote! {
+ ::mingling::macros::pack!(#error_struct = String);
+ }
+ });
+
+ // Generate the #[help] block if help = true
+ let help_gen = if options.help_enabled {
+ let dispatcher_name_str = dispatcher_struct.to_string();
+ let help_fn_name_str = format!("__{}_help", just_fmt::snake_case!(&dispatcher_name_str));
+ let help_fn_name = Ident::new(&help_fn_name_str, proc_macro2::Span::call_site());
+
+ Some(quote! {
+ #[allow(non_snake_case)]
+ #[::mingling::macros::help]
+ pub(crate) fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult {
+ use std::io::Write;
+ use clap::ColorChoice;
+
+ let this = ::mingling::this::<#program_path>();
+ match this.stdout_setting.clap_help_print_behaviour {
+ ::mingling::ClapHelpPrintBehaviour::WriteToRenderResult => {
+ let mut cmd = <#struct_name as ::clap::CommandFactory>::command()
+ .color(ColorChoice::Always);
+ let styled = cmd.render_help();
+ 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()
+ }
+ }
+ }
+ })
+ } else {
+ None
+ };
+
+ let dispatch_tree_entry =
+ get_dispatch_tree_entry(&command_name_str, dispatcher_struct, &struct_name);
+
+ let expanded = quote! {
+ // Keep the original struct definition
+ #input_struct
+
+ // Generate the error wrapper type via pack!
+ #error_pack
+
+ // Generate the help block if enabled
+ #help_gen
+
+ // Dispatch tree registration (if feature enabled)
+ #dispatch_tree_entry
+
+ // Generate the dispatcher struct
+ #[doc(hidden)]
+ #[derive(Default)]
+ pub(crate) struct #dispatcher_struct;
+
+ impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct {
+ fn node(&self) -> ::mingling::Node {
+ ::mingling::macros::node!(#command_name_str)
+ }
+
+ fn begin(
+ &self,
+ args: Vec<String>,
+ ) -> ::mingling::ChainProcess<#program_path> {
+ // Prepend a dummy program name for clap's parse_from
+ let clap_args = std::iter::once(String::new())
+ .chain(args)
+ .collect::<Vec<_>>();
+
+ #begin_body
+ }
+
+ fn clone_dispatcher(
+ &self,
+ ) -> Box<dyn ::mingling::Dispatcher<#program_path>> {
+ Box::new(#dispatcher_struct)
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+#[cfg(feature = "dispatch_tree")]
+fn get_dispatch_tree_entry(
+ command_name_str: &str,
+ dispatcher_struct: &Ident,
+ entry_name: &Ident,
+) -> proc_macro2::TokenStream {
+ let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site());
+ quote! {
+ ::mingling::macros::register_dispatcher!(#node_name_lit, #dispatcher_struct, #entry_name);
+ }
+}
+
+#[cfg(not(feature = "dispatch_tree"))]
+fn get_dispatch_tree_entry(
+ _command_name_str: &str,
+ _dispatcher_struct: &Ident,
+ _entry_name: &Ident,
+) -> proc_macro2::TokenStream {
+ quote! {}
+}
diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs
new file mode 100644
index 0000000..aa7bc88
--- /dev/null
+++ b/mingling_macros/src/attr/help.rs
@@ -0,0 +1,231 @@
+use proc_macro::TokenStream;
+use quote::{ToTokens, quote};
+use syn::spanned::Spanned;
+use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
+
+use crate::get_global_set;
+use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
+
+/// 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) => Some(quote! { #ty }),
+ ReturnType::Default => None,
+ }
+}
+
+pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Help function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the entry type and resource injection params
+ let (_, entry_type, resources) = match extract_args_info(&input_fn.sig) {
+ Ok(info) => info,
+ Err(e) => 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;
+ let fn_body_stmts = &fn_body.stmts;
+
+ // Get function attributes excluding the help attribute
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("help"));
+
+ // Get function visibility
+ let vis = &input_fn.vis;
+
+ // Get function name
+ 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!(
+ "__internal_help_{}",
+ just_fmt::snake_case!(fn_name.to_string())
+ );
+ let struct_name = Ident::new(&internal_name, fn_name.span());
+
+ let program_type = crate::default_program_path();
+ let has_resources = !resources.is_empty();
+ let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
+
+ // Generate immutable resource bindings
+ let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
+
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
+ } else {
+ let mut wrapped = fn_call;
+ for res in mut_resources.iter().rev() {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
+ wrapped
+ };
+
+ let help_render_body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #inner_call
+ }
+ } else {
+ quote! { #inner_call }
+ };
+
+ // Register the help request mapping
+ let help_entry = build_help_entry(&struct_name, &entry_type);
+ let entry_str = help_entry.to_string();
+
+ // Check for duplicate variant before inserting
+ let variant_name = entry_type.path.segments.last().unwrap().ident.to_string();
+ {
+ let helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap();
+ if let Err(err) = crate::check_duplicate_variant(
+ &helps,
+ &entry_str,
+ &variant_name,
+ "help",
+ entry_type.span(),
+ ) {
+ return err.into();
+ }
+ }
+
+ get_global_set(&crate::HELP_REQUESTS)
+ .lock()
+ .unwrap()
+ .insert(entry_str);
+
+ // Generate the struct and HelpRequest implementation
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ #vis struct #struct_name;
+
+ impl ::mingling::HelpRequest for #struct_name {
+ type Entry = #entry_type;
+
+ fn render_help(#fixed_prev: Self::Entry) -> ::mingling::RenderResult {
+ 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
+ #(#fn_attrs)*
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
+ }
+ };
+
+ expanded.into()
+}
+
+/// Builds a help request entry for the global help requests list
+fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::TokenStream {
+ let enum_variant = &entry_type.path.segments.last().unwrap().ident;
+ quote! {
+ Self::#enum_variant => {
+ // 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)
+ }
+ }
+}
+
+pub(crate) fn register_help(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of arguments
+ let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
+
+ // Check if there are exactly two elements
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `EntryType, StructName`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the two elements
+ let entry_type_expr = &input_parsed[0];
+ let struct_name_expr = &input_parsed[1];
+
+ // Convert expressions to TypePath and Ident
+ let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
+ Ok(ident) => ident,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Register the help request mapping
+ let help_entry = build_help_entry(&struct_name, &entry_type);
+ let entry_str = help_entry.to_string();
+
+ // Check if entry was already pre-inserted by `#[help]` attribute
+ let mut helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap();
+ if helps.contains(&entry_str) {
+ // Already registered by `#[help]`, no duplicate check needed
+ return quote! {}.into();
+ }
+
+ // Check for duplicate variant (different struct, same type)
+ let variant_name = entry_type.path.segments.last().unwrap().ident.to_string();
+ if let Err(err) =
+ crate::check_duplicate_variant(&helps, &entry_str, &variant_name, "help", entry_type.span())
+ {
+ return err.into();
+ }
+
+ helps.insert(entry_str);
+
+ quote! {}.into()
+}
diff --git a/mingling_macros/src/attr/program_setup.rs b/mingling_macros/src/attr/program_setup.rs
new file mode 100644
index 0000000..dee5a1c
--- /dev/null
+++ b/mingling_macros/src/attr/program_setup.rs
@@ -0,0 +1,116 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, parse_macro_input};
+
+/// Extracts the program parameter from function arguments
+fn extract_program_param(sig: &Signature) -> syn::Result<(Pat, Type)> {
+ // The function should have exactly one parameter
+ if sig.inputs.len() != 1 {
+ return Err(syn::Error::new(
+ sig.inputs.span(),
+ "Setup function must have exactly one parameter",
+ ));
+ }
+
+ let arg = &sig.inputs[0];
+ match arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ // Extract the pattern (parameter name)
+ let param_pat = (**pat).clone();
+ // Extract the type as-is
+ let param_type = (**ty).clone();
+ Ok((param_pat, param_type))
+ }
+ FnArg::Receiver(_) => Err(syn::Error::new(
+ arg.span(),
+ "Setup function cannot have self parameter",
+ )),
+ }
+}
+
+/// Extracts and validates the return type
+fn extract_return_type(sig: &Signature) -> syn::Result<()> {
+ // Setup functions should return () or have no return type
+ match &sig.output {
+ ReturnType::Type(_, ty) => {
+ // Check if it's ()
+ match &**ty {
+ Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()),
+ _ => Err(syn::Error::new(
+ ty.span(),
+ "Setup function must return () or have no return type",
+ )),
+ }
+ }
+ ReturnType::Default => Ok(()),
+ }
+}
+
+pub(crate) fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // #[program_setup] takes no arguments; always use the default program path
+ let _ = attr;
+ let program_path = crate::default_program_path();
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Setup function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the program parameter
+ let (program_param, program_type) = match extract_program_param(&input_fn.sig) {
+ Ok(info) => info,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Validate return type
+ if let Err(e) = extract_return_type(&input_fn.sig) {
+ return e.to_compile_error().into();
+ }
+
+ // Get the function body
+ let fn_body = &input_fn.block;
+
+ // Get function attributes (excluding the setup attribute)
+ let mut fn_attrs = input_fn.attrs.clone();
+
+ // Remove any #[program_setup(...)] attributes to avoid infinite recursion
+ fn_attrs.retain(|attr| !attr.path().is_ident("setup"));
+
+ // Get function visibility
+ let vis = &input_fn.vis;
+
+ // Get function name
+ let fn_name = &input_fn.sig.ident;
+
+ // Generate struct name from function name using pascal_case
+ let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string());
+ let struct_name = Ident::new(&pascal_case_name, fn_name.span());
+
+ // Generate the struct and implementation
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #vis struct #struct_name;
+
+ impl ::mingling::setup::ProgramSetup<#program_path> for #struct_name {
+ fn setup(self, program: &mut ::mingling::Program<#program_path>) {
+ // Call the original function with the actual Program type
+ #fn_name(program);
+ }
+ }
+
+ // Keep the original function for internal use
+ #(#fn_attrs)*
+ #vis fn #fn_name(#program_param: #program_type) {
+ #fn_body
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/attr/renderer.rs b/mingling_macros/src/attr/renderer.rs
new file mode 100644
index 0000000..828dc00
--- /dev/null
+++ b/mingling_macros/src/attr/renderer.rs
@@ -0,0 +1,274 @@
+use proc_macro::TokenStream;
+use quote::{ToTokens, quote};
+use syn::spanned::Spanned;
+use syn::{ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
+
+use crate::get_global_set;
+use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
+
+/// 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) => Some(quote! { #ty }),
+ ReturnType::Default => None,
+ }
+}
+
+#[allow(clippy::too_many_lines)]
+pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // #[renderer] takes no arguments; always use the default program path
+ let _ = attr;
+ let program_path = crate::default_program_path();
+ let program_type = &program_path;
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Renderer function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the previous type and resource injection params
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ Ok(info) => info,
+ Err(e) => 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();
+
+ // Get function attributes (excluding the renderer attribute)
+ let mut fn_attrs = input_fn.attrs.clone();
+
+ // Remove any #[renderer(...)] attributes to avoid infinite recursion
+ fn_attrs.retain(|attr| !attr.path().is_ident("renderer"));
+
+ // Get function visibility
+ let vis = &input_fn.vis;
+
+ // Get function name
+ let fn_name = &input_fn.sig.ident;
+
+ // Generate struct name from function name using pascal_case
+ let internal_name = format!(
+ "__internal_renderer_{}",
+ just_fmt::snake_case!(fn_name.to_string())
+ );
+ let struct_name = syn::Ident::new(&internal_name, fn_name.span());
+
+ let has_resources = !resources.is_empty();
+ let has_mut_resources = resources.iter().any(|r| r.is_mut);
+
+ // Generate resource bindings for immutable resources
+ let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
+ let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
+
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if has_mut_resources {
+ let mut wrapped = fn_call;
+ for res in mut_resources.iter().rev() {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ wrapped = quote! {
+ ::mingling::this::<#program_type>()
+ .modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
+ wrapped
+ } else {
+ fn_call
+ };
+
+ // Build the Renderer::render body with resource injection.
+ // The trait method injects resources and calls the original function.
+ let render_fn_body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #inner_call
+ }
+ } else {
+ quote! { #inner_call }
+ };
+
+ // 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 = user_return_type.clone().unwrap_or(quote! { () });
+
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ #vis struct #struct_name;
+
+ ::mingling::macros::register_renderer!(#previous_type, #struct_name);
+
+ impl ::mingling::Renderer for #struct_name {
+ type Previous = #previous_type;
+
+ fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult {
+ let __renderer_result = { #render_fn_body };
+ ::std::convert::Into::into(__renderer_result)
+ }
+ }
+
+ // Keep the original function unchanged
+ #(#fn_attrs)*
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
+ }
+ };
+
+ expanded.into()
+}
+
+/// Builds the renderer entry for the global renderers list
+pub(crate) fn build_renderer_entry(
+ struct_name: &syn::Ident,
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
+ let enum_variant = &previous_type.path.segments.last().unwrap().ident;
+ quote! {
+ #struct_name => #enum_variant,
+ }
+}
+
+/// Builds the renderer existence check entry
+pub(crate) fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
+ let enum_variant = &previous_type.path.segments.last().unwrap().ident;
+ quote! {
+ Self::#enum_variant => true,
+ }
+}
+
+/// Builds the structural renderer entry
+#[cfg(feature = "structural_renderer")]
+pub(crate) fn build_structural_renderer_entry(
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
+ let enum_variant = &previous_type.path.segments.last().unwrap().ident;
+ quote! {
+ Self::#enum_variant => {
+ // SAFETY: Only types that match will enter this branch for forced conversion,
+ // and `AnyOutput::new` ensures the type implements serde::Serialize
+ let raw = unsafe { any.restore::<#previous_type>().unwrap_unchecked() };
+ let mut __renderer_inner_result = ::mingling::RenderResult::default();
+ ::mingling::StructuralRenderer::render(&raw, setting, &mut __renderer_inner_result)?;
+ Ok(__renderer_inner_result)
+ }
+ }
+}
+
+pub(crate) fn register_renderer(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of arguments
+ let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
+
+ // Check that there are exactly two elements
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `PreviousType, StructName`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the two elements
+ let previous_type_expr = &input_parsed[0];
+ let struct_name_expr = &input_parsed[1];
+
+ // Convert expressions to TypePath and Ident
+ let previous_type = match syn::parse2::<TypePath>(previous_type_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
+ Ok(ident) => ident,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Register the renderer in the global list
+ let renderer_entry = build_renderer_entry(&struct_name, &previous_type);
+ let renderer_exist_entry = build_renderer_exist_entry(&previous_type);
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderer_entry = build_structural_renderer_entry(&previous_type);
+
+ let renderer_entry_str = renderer_entry.to_string();
+ let renderer_exist_entry_str = renderer_exist_entry.to_string();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderer_entry_str = structural_renderer_entry.to_string();
+
+ // Check for duplicate variant before acquiring other locks
+ let variant_name = previous_type
+ .path
+ .segments
+ .last()
+ .unwrap()
+ .ident
+ .to_string();
+ {
+ let renderers = get_global_set(&crate::RENDERERS).lock().unwrap();
+ if let Err(err) = crate::check_duplicate_variant(
+ &renderers,
+ &renderer_entry_str,
+ &variant_name,
+ "renderer",
+ previous_type.span(),
+ ) {
+ return err.into();
+ }
+ } // renderers lock released here
+
+ let mut renderers = get_global_set(&crate::RENDERERS).lock().unwrap();
+ let mut renderer_exist = get_global_set(&crate::RENDERERS_EXIST).lock().unwrap();
+
+ #[cfg(feature = "structural_renderer")]
+ let mut structural_renderers = get_global_set(&crate::STRUCTURAL_RENDERERS).lock().unwrap();
+
+ renderers.insert(renderer_entry_str);
+ renderer_exist.insert(renderer_exist_entry_str);
+
+ // Only register structural renderer if the type is in STRUCTURED_TYPES
+ #[cfg(feature = "structural_renderer")]
+ {
+ let is_structured = get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .contains(&variant_name);
+ if is_structured {
+ structural_renderers.insert(structural_renderer_entry_str);
+ }
+ }
+
+ quote! {}.into()
+}