From 1f583f625a7d541dc7d47a8136b31d29a5ed5441 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 20 Jul 2026 00:59:17 +0800 Subject: chore(macros): reorganize module hierarchy in mingling_macros Restructure flat module layout into logical directories (attr/, derive/, func/, systems/, extensions/, utils.rs). Tighten internal function visibility to pub(crate). All public API signatures remain unchanged. --- mingling_macros/src/attr.rs | 9 + mingling_macros/src/attr/chain.rs | 347 +++++++++++ mingling_macros/src/attr/completion.rs | 239 ++++++++ mingling_macros/src/attr/dispatcher_clap.rs | 241 ++++++++ mingling_macros/src/attr/help.rs | 214 +++++++ mingling_macros/src/attr/program_setup.rs | 116 ++++ mingling_macros/src/attr/renderer.rs | 253 ++++++++ mingling_macros/src/chain.rs | 344 ----------- mingling_macros/src/completion.rs | 239 -------- mingling_macros/src/derive.rs | 2 + mingling_macros/src/derive/enum_tag.rs | 168 +++++ mingling_macros/src/derive/grouped.rs | 79 +++ mingling_macros/src/dispatch_tree_gen.rs | 181 ------ mingling_macros/src/dispatcher.rs | 266 -------- mingling_macros/src/dispatcher_clap.rs | 241 -------- mingling_macros/src/entry.rs | 70 --- mingling_macros/src/enum_tag.rs | 168 ----- mingling_macros/src/func.rs | 12 + mingling_macros/src/func/dispatcher.rs | 266 ++++++++ mingling_macros/src/func/entry.rs | 70 +++ mingling_macros/src/func/gen_program.rs | 602 ++++++++++++++++++ mingling_macros/src/func/group.rs | 147 +++++ mingling_macros/src/func/node.rs | 59 ++ mingling_macros/src/func/pack.rs | 149 +++++ mingling_macros/src/func/pack_err.rs | 209 +++++++ mingling_macros/src/func/suggest.rs | 93 +++ mingling_macros/src/group_impl.rs | 147 ----- mingling_macros/src/grouped.rs | 79 --- mingling_macros/src/help.rs | 214 ------- mingling_macros/src/lib.rs | 750 ++--------------------- mingling_macros/src/node.rs | 59 -- mingling_macros/src/pack.rs | 149 ----- mingling_macros/src/pack_err.rs | 209 ------- mingling_macros/src/program_setup.rs | 116 ---- mingling_macros/src/renderer.rs | 251 -------- mingling_macros/src/res_injection.rs | 261 -------- mingling_macros/src/structural_data.rs | 336 ---------- mingling_macros/src/suggest.rs | 93 --- mingling_macros/src/systems.rs | 5 + mingling_macros/src/systems/dispatch_tree_gen.rs | 181 ++++++ mingling_macros/src/systems/res_injection.rs | 261 ++++++++ mingling_macros/src/systems/structural_data.rs | 336 ++++++++++ mingling_macros/src/utils.rs | 1 + 43 files changed, 4101 insertions(+), 4131 deletions(-) create mode 100644 mingling_macros/src/attr.rs create mode 100644 mingling_macros/src/attr/chain.rs create mode 100644 mingling_macros/src/attr/completion.rs create mode 100644 mingling_macros/src/attr/dispatcher_clap.rs create mode 100644 mingling_macros/src/attr/help.rs create mode 100644 mingling_macros/src/attr/program_setup.rs create mode 100644 mingling_macros/src/attr/renderer.rs delete mode 100644 mingling_macros/src/chain.rs delete mode 100644 mingling_macros/src/completion.rs create mode 100644 mingling_macros/src/derive.rs create mode 100644 mingling_macros/src/derive/enum_tag.rs create mode 100644 mingling_macros/src/derive/grouped.rs delete mode 100644 mingling_macros/src/dispatch_tree_gen.rs delete mode 100644 mingling_macros/src/dispatcher.rs delete mode 100644 mingling_macros/src/dispatcher_clap.rs delete mode 100644 mingling_macros/src/entry.rs delete mode 100644 mingling_macros/src/enum_tag.rs create mode 100644 mingling_macros/src/func.rs create mode 100644 mingling_macros/src/func/dispatcher.rs create mode 100644 mingling_macros/src/func/entry.rs create mode 100644 mingling_macros/src/func/gen_program.rs create mode 100644 mingling_macros/src/func/group.rs create mode 100644 mingling_macros/src/func/node.rs create mode 100644 mingling_macros/src/func/pack.rs create mode 100644 mingling_macros/src/func/pack_err.rs create mode 100644 mingling_macros/src/func/suggest.rs delete mode 100644 mingling_macros/src/group_impl.rs delete mode 100644 mingling_macros/src/grouped.rs delete mode 100644 mingling_macros/src/help.rs delete mode 100644 mingling_macros/src/node.rs delete mode 100644 mingling_macros/src/pack.rs delete mode 100644 mingling_macros/src/pack_err.rs delete mode 100644 mingling_macros/src/program_setup.rs delete mode 100644 mingling_macros/src/renderer.rs delete mode 100644 mingling_macros/src/res_injection.rs delete mode 100644 mingling_macros/src/structural_data.rs delete mode 100644 mingling_macros/src/suggest.rs create mode 100644 mingling_macros/src/systems.rs create mode 100644 mingling_macros/src/systems/dispatch_tree_gen.rs create mode 100644 mingling_macros/src/systems/res_injection.rs create mode 100644 mingling_macros/src/systems/structural_data.rs create mode 100644 mingling_macros/src/utils.rs (limited to 'mingling_macros/src') diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs new file mode 100644 index 0000000..e4cd826 --- /dev/null +++ b/mingling_macros/src/attr.rs @@ -0,0 +1,9 @@ +pub(crate) mod chain; +#[cfg(feature = "comp")] +pub(crate) mod completion; +#[cfg(feature = "clap")] +pub(crate) mod dispatcher_clap; +pub(crate) mod help; +#[cfg(feature = "extra_macros")] +pub(crate) mod program_setup; +pub(crate) mod renderer; diff --git a/mingling_macros/src/attr/chain.rs b/mingling_macros/src/attr/chain.rs new file mode 100644 index 0000000..120e65d --- /dev/null +++ b/mingling_macros/src/attr/chain.rs @@ -0,0 +1,347 @@ +#![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. +/// +/// The user's function body is inlined directly, and its result is converted +/// via `.into()` to `ChainProcess`. +#[allow(unused_variables)] +fn generate_proc_fn( + has_resources: bool, + resources: &[ResourceInjection], + program_type: &proc_macro2::TokenStream, + previous_type: &TypePath, + prev_param: &Pat, + 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 wrapped_body = if is_async_fn && !mut_resources.is_empty() { + wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type) + } else { + wrap_body_with_mut_resources(fn_body_stmts, &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; + > + ::to_chain(crate::ResultEmpty) + } + } else { + quote! { + #wrapped_body; + > + ::to_chain(crate::ResultEmpty) + } + }; + quote! { #body_with_ending } + } else { + let body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #wrapped_body + } + } else { + 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 for T` + // - `-> PackType`: `Into` from pack!/derive + quote! { + let __chain_result = { #body }; + <#origin_return_type as ::std::convert::Into< + ::mingling::ChainProcess<#program_type> + >>::into(__chain_result) + } + }; + + #[cfg(feature = "async")] + { + quote! { + async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { + #proc_body + } + } + } + + #[cfg(not(feature = "async"))] + { + quote! { + fn proc(#prev_param: #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 (prev_param, 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 fn_body = &input_fn.block; + 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(); + + // 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_body.stmts, + #[cfg(feature = "async")] + is_async_fn, + #[cfg(not(feature = "async"))] + false, + is_unit_return, + &origin_return_type, + ); + + // 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 }; + 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::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::(previous_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::(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..e917d7d --- /dev/null +++ b/mingling_macros/src/attr/completion.rs @@ -0,0 +1,239 @@ +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_pat, _ctx_type) = match first_arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + let param_pat = (**pat).clone(); + (param_pat, (**ty).clone()) + } + FnArg::Receiver(_) => { + return syn::Error::new( + first_arg.span(), + "Completion function cannot have self parameter", + ) + .to_compile_error() + .into(); + } + }; + + // 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; + let fn_body_stmts = &fn_body.stmts; + + // 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 comp method body with resource injection + // Use modify_res for mutable resources same pattern as renderer.rs + let wrapped_body = if mut_resources.is_empty() { + quote! { #(#fn_body_stmts)* } + } else { + let mut wrapped = quote! { #(#fn_body_stmts)* }; + 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)* + #wrapped_body + } + } else { + quote! { #(#fn_body_stmts)* } + }; + + // 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(#ctx_pat: &::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> { + 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..6083a52 --- /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, + /// `help = true` (bool only) + help_enabled: bool, +} + +impl Parse for ClapOptions { + fn parse(input: ParseStream) -> syn::Result { + let mut error_struct = None; + let mut help_enabled = false; + + while !input.is_empty() { + // Parse leading comma + input.parse::()?; + + // Allow trailing comma + if input.is_empty() { + break; + } + + let key: Ident = input.parse()?; + input.parse::()?; + + 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 { + // Format: "cmd", Disp, ... + let command_name: LitStr = input.parse()?; + input.parse::()?; + let dispatcher_struct: Ident = input.parse()?; + + let options = if input.is_empty() { + ClapOptions { + error_struct: None, + help_enabled: false, + } + } else { + input.parse::()? + }; + + 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 #struct_name::default().to_chain(); + } + match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) { + Ok(parsed) => parsed.to_chain(), + Err(e) => { + return #error_struct::new(format!("{}", e.render().ansi())).to_render() + }, + } + } + } else { + quote! { + if ::mingling::this::<#program_path>().user_context.help { + return #struct_name::default().to_chain(); + } + let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args) + .unwrap_or_else(|e| e.exit()); + parsed.to_chain() + } + }; + + // 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, + ) -> ::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::>(); + + #begin_body + } + + fn clone_dispatcher( + &self, + ) -> Box> { + 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..6defae4 --- /dev/null +++ b/mingling_macros/src/attr/help.rs @@ -0,0 +1,214 @@ +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +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}; + +/// Extracts the user's return type, returning `None` for no return type. +fn extract_user_return_type(sig: &Signature) -> Option { + 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, parameter name, and resource injection params + let (prev_param, 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 render_help body with resource injection + // Use modify_res for mutable resources same pattern as renderer.rs + + let wrapped_body = if mut_resources.is_empty() { + quote! { #(#fn_body_stmts)* } + } else { + let mut wrapped = quote! { #(#fn_body_stmts)* }; + 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)* + #wrapped_body + } + } else { + quote! { #(#fn_body_stmts)* } + }; + + // 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(#prev_param: 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 + #[allow(dead_code)] + #(#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::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::(entry_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::(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..c7cbb0b --- /dev/null +++ b/mingling_macros/src/attr/renderer.rs @@ -0,0 +1,253 @@ +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +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}; + +/// Extracts the user's return type, returning `None` for no return type. +fn extract_user_return_type(sig: &Signature) -> Option { + 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, parameter name, and resource injection params + let (prev_param, 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 = 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(); + + let inner_body_with_resources = if has_mut_resources { + let mut wrapped = quote! { #(#fn_body_stmts)* }; + 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 { + quote! { #(#fn_body_stmts)* } + }; + + // Build the Renderer::render body with resource injection + // The user's body now directly creates and returns a RenderResult. + let render_fn_body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #inner_body_with_resources + } + } else { + quote! { #inner_body_with_resources } + }; + + // 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(#prev_param: Self::Previous) -> ::mingling::RenderResult { + 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)* + } + }; + + 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::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::(previous_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::(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() +} diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/chain.rs deleted file mode 100644 index ef31854..0000000 --- a/mingling_macros/src/chain.rs +++ /dev/null @@ -1,344 +0,0 @@ -#![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. -/// -/// The user's function body is inlined directly, and its result is converted -/// via `.into()` to `ChainProcess`. -#[allow(unused_variables)] -fn generate_proc_fn( - has_resources: bool, - resources: &[ResourceInjection], - program_type: &proc_macro2::TokenStream, - previous_type: &TypePath, - prev_param: &Pat, - 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 wrapped_body = if is_async_fn && !mut_resources.is_empty() { - wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type) - } else { - wrap_body_with_mut_resources(fn_body_stmts, &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; - > - ::to_chain(crate::ResultEmpty) - } - } else { - quote! { - #wrapped_body; - > - ::to_chain(crate::ResultEmpty) - } - }; - quote! { #body_with_ending } - } else { - let body = if has_resources { - quote! { - #(#immut_resource_stmts)* - #wrapped_body - } - } else { - 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 for T` - // - `-> PackType`: `Into` from pack!/derive - quote! { - let __chain_result = { #body }; - <#origin_return_type as ::std::convert::Into< - ::mingling::ChainProcess<#program_type> - >>::into(__chain_result) - } - }; - - #[cfg(feature = "async")] - { - quote! { - async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { - #proc_body - } - } - } - - #[cfg(not(feature = "async"))] - { - quote! { - fn proc(#prev_param: #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 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 (prev_param, 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 fn_body = &input_fn.block; - 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(); - - // 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_body.stmts, - #[cfg(feature = "async")] - is_async_fn, - #[cfg(not(feature = "async"))] - false, - is_unit_return, - &origin_return_type, - ); - - // 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 }; - 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 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 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 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::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::(previous_type_expr.to_token_stream()) { - Ok(ty) => ty, - Err(e) => return e.to_compile_error().into(), - }; - - let struct_name = match syn::parse2::(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/completion.rs b/mingling_macros/src/completion.rs deleted file mode 100644 index ae01462..0000000 --- a/mingling_macros/src/completion.rs +++ /dev/null @@ -1,239 +0,0 @@ -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 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_pat, _ctx_type) = match first_arg { - FnArg::Typed(PatType { pat, ty, .. }) => { - let param_pat = (**pat).clone(); - (param_pat, (**ty).clone()) - } - FnArg::Receiver(_) => { - return syn::Error::new( - first_arg.span(), - "Completion function cannot have self parameter", - ) - .to_compile_error() - .into(); - } - }; - - // 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; - let fn_body_stmts = &fn_body.stmts; - - // 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 comp method body with resource injection - // Use modify_res for mutable resources same pattern as renderer.rs - let wrapped_body = if mut_resources.is_empty() { - quote! { #(#fn_body_stmts)* } - } else { - let mut wrapped = quote! { #(#fn_body_stmts)* }; - 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)* - #wrapped_body - } - } else { - quote! { #(#fn_body_stmts)* } - }; - - // 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(#ctx_pat: &::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> { - 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/derive.rs b/mingling_macros/src/derive.rs new file mode 100644 index 0000000..ffa405b --- /dev/null +++ b/mingling_macros/src/derive.rs @@ -0,0 +1,2 @@ +pub(crate) mod enum_tag; +pub(crate) mod grouped; diff --git a/mingling_macros/src/derive/enum_tag.rs b/mingling_macros/src/derive/enum_tag.rs new file mode 100644 index 0000000..a7f71f0 --- /dev/null +++ b/mingling_macros/src/derive/enum_tag.rs @@ -0,0 +1,168 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Variant, parse_macro_input, +}; + +pub(crate) fn derive_enum_tag(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match derive_enum_tag_impl(input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + +/// Implementation of the `EnumTag` derive macro +fn derive_enum_tag_impl(input: DeriveInput) -> Result { + let enum_name = &input.ident; + let generics = &input.generics; + + // Extract enum data + let data = match input.data { + Data::Enum(data_enum) => data_enum, + Data::Struct(_) => { + return Err(Error::new_spanned( + enum_name, + "EnumTag can only be derived for enums, not structs", + )); + } + Data::Union(_) => { + return Err(Error::new_spanned( + enum_name, + "EnumTag can only be derived for enums, not unions", + )); + } + }; + + // Process each variant + let mut variant_info = Vec::new(); + let mut match_arms = Vec::new(); + let mut build_match_arms = Vec::new(); + + for variant in data.variants { + process_variant( + &variant, + enum_name, + &mut variant_info, + &mut match_arms, + &mut build_match_arms, + )?; + } + + // Generate the implementation + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics ::mingling::EnumTag for #enum_name #ty_generics #where_clause { + fn enum_info(&self) -> (&'static str, &'static str) { + match self { + #(#match_arms)* + } + } + + fn build_enum(name: String) -> Option + where + Self: Sized + { + match name.as_str() { + #(#build_match_arms)* + _ => None, + } + } + + fn enums() -> &'static [(&'static str, &'static str)] { + &[#(#variant_info),*] + } + } + }; + + Ok(expanded) +} + +/// Process a single enum variant +fn process_variant( + variant: &Variant, + enum_name: &Ident, + variant_info: &mut Vec, + match_arms: &mut Vec, + build_match_arms: &mut Vec, +) -> Result<()> { + let variant_name = variant.ident.clone(); + + // Check if variant has fields + match &variant.fields { + Fields::Unit => { + // Good, unit variant + } + Fields::Named(_) | Fields::Unnamed(_) => { + return Err(Error::new_spanned( + variant, + format!( + "EnumTag cannot be derived for enum variant `{variant_name}` with fields. Only unit variants are supported." + ), + )); + } + } + + // Extract description from #[enum_desc] attribute + let description = extract_description(&variant.attrs)?; + + // Extract rename from #[enum_rename] attribute + let rename = extract_rename(&variant.attrs)?; + + // Generate tokens for this variant + let variant_name_str = variant_name.to_string(); + let display_name = rename.unwrap_or_else(|| variant_name_str.clone()); + let description_str = description.unwrap_or_default(); + + variant_info.push(quote! { + (#display_name, #description_str) + }); + + match_arms.push(quote! { + #enum_name::#variant_name => (#display_name, #description_str), + }); + + build_match_arms.push(quote! { + #display_name => Some(#enum_name::#variant_name), + }); + + Ok(()) +} + +/// Extract description from #[`enum_desc`] attribute +fn extract_description(attrs: &[Attribute]) -> Result> { + for attr in attrs { + if attr.path().is_ident("enum_desc") { + return match attr.parse_args::() { + Ok(lit_str) => Ok(Some(lit_str.value())), + Err(_) => Err(Error::new_spanned( + attr, + "#[enum_desc] attribute must be in the form `#[enum_desc(\"description\")]`", + )), + }; + } + } + + // If no #[enum_desc] attribute, return None + Ok(None) +} + +/// Extract rename from #[`enum_rename`] attribute +fn extract_rename(attrs: &[Attribute]) -> Result> { + for attr in attrs { + if attr.path().is_ident("enum_rename") { + return match attr.parse_args::() { + Ok(lit_str) => Ok(Some(lit_str.value())), + Err(_) => Err(Error::new_spanned( + attr, + "#[enum_rename] attribute must be in the form `#[enum_rename(\"new_name\")]`", + )), + }; + } + } + + // If no #[enum_rename] attribute, return None + Ok(None) +} diff --git a/mingling_macros/src/derive/grouped.rs b/mingling_macros/src/derive/grouped.rs new file mode 100644 index 0000000..307aab6 --- /dev/null +++ b/mingling_macros/src/derive/grouped.rs @@ -0,0 +1,79 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Ident, parse_macro_input}; + +pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream { + // Parse the input struct/enum + let input = parse_macro_input!(input as DeriveInput); + let struct_name = input.ident; + + let group_ident: proc_macro2::TokenStream = crate::default_program_path(); + + let any_output_convert_impls = + proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident)); + + // Generate the Grouped trait implementation + let expanded = quote! { + ::mingling::macros::register_type!(#struct_name); + + impl ::mingling::Grouped<#group_ident> for #struct_name { + fn member_id() -> #group_ident { + #group_ident::#struct_name + } + } + + #any_output_convert_impls + }; + + expanded.into() +} + +#[cfg(feature = "structural_renderer")] +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(); + + let group_ident: proc_macro2::TokenStream = crate::default_program_path(); + + let any_output_convert_impls = + proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident)); + + // Generate both Serialize and Grouped implementations + let expanded = quote! { + #[derive(serde::Serialize)] + #input_parsed + + ::mingling::macros::register_type!(#struct_name); + + impl ::mingling::Grouped<#group_ident> for #struct_name { + fn member_id() -> #group_ident { + #group_ident::#struct_name + } + } + + #any_output_convert_impls + }; + + expanded.into() +} + +fn build_any_output_convert_impls( + struct_name: &Ident, + group_ident: &proc_macro2::TokenStream, +) -> TokenStream { + quote! { + impl ::std::convert::Into<::mingling::AnyOutput<#group_ident>> for #struct_name { + fn into(self) -> ::mingling::AnyOutput<#group_ident> { + ::mingling::AnyOutput::new(self) + } + } + + impl ::std::convert::Into<::mingling::ChainProcess<#group_ident>> for #struct_name { + fn into(self) -> ::mingling::ChainProcess<#group_ident> { + ::mingling::AnyOutput::new(self).route_chain() + } + } + } + .into() +} diff --git a/mingling_macros/src/dispatch_tree_gen.rs b/mingling_macros/src/dispatch_tree_gen.rs deleted file mode 100644 index b66e2f4..0000000 --- a/mingling_macros/src/dispatch_tree_gen.rs +++ /dev/null @@ -1,181 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; - -use just_fmt::snake_case; -use proc_macro2::TokenStream; -use quote::quote; - -use crate::resolve_type; - -/// Generate the `get_nodes()` function body for a ProgramCollect impl. -/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths. -pub fn gen_get_nodes( - entries: &[(String, String, String)], - pathf_map: &HashMap, -) -> TokenStream { - let mut node_entries = Vec::new(); - - for (node_name, _disp_type, _entry_name) in entries { - let static_name_str = format!("__internal_dispatcher_{}", snake_case!(node_name)); - let resolved = resolve_type(&static_name_str, pathf_map); - let node_display_name = node_name.replace('.', " "); - let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site()); - - node_entries.push(quote! { - (#node_display_lit.to_string(), & #resolved) - }); - } - - quote! { - fn get_nodes() -> Vec<(String, &'static (dyn ::mingling::Dispatcher + Send + Sync))> { - vec![ - #(#node_entries),* - ] - } - } -} - -/// Generate the `dispatch_args_trie()` function body for a ProgramCollect impl. -/// -/// Builds a hardcoded match tree: at each depth, group nodes by character. -/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match. -/// -/// If `pathf_map` is non-empty, resolves dispatcher types using full paths. -pub fn gen_dispatch_args_trie( - entries: &[(String, String, String)], - pathf_map: &HashMap, -) -> TokenStream { - // Prepare (display_name, disp_type) pairs. - // display_name = node_name.replace('.', " ") - let nodes: Vec<(String, String)> = entries - .iter() - .map(|(name, disp, _)| (name.replace('.', " "), disp.clone())) - .collect(); - - let dispatch_body = build_dispatch_body(&nodes, 0, pathf_map); - - quote! { - fn dispatch_args_trie( - raw: &[String], - ) -> Result<::mingling::AnyOutput, ::mingling::error::ProgramInternalExecuteError> - { - let raw_string = format!("{} ", raw.join(" ")); - let raw_str = raw_string.as_str(); - let mut raw_chars = raw_str.chars(); - #dispatch_body - } - } -} - -/// Recursively build the trie match body. -/// -/// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far. -/// `depth`: The character index currently being matched. -/// `pathf_map`: optional mapping from type name to full path for resolving dispatchers. -fn build_dispatch_body( - nodes: &[(String, String)], - depth: usize, - pathf_map: &HashMap, -) -> TokenStream { - if nodes.is_empty() { - return quote! { - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); - }; - } - - // Group by character at `depth` - let mut groups: BTreeMap> = BTreeMap::new(); - let mut exact_nodes: Vec<(String, String)> = Vec::new(); - - for (name, disp_type) in nodes { - if let Some(ch) = name.chars().nth(depth) { - groups - .entry(ch) - .or_default() - .push((name.clone(), disp_type.clone())); - } else { - exact_nodes.push((name.clone(), disp_type.clone())); - } - } - - // Build a dispatch arm for a single node via `starts_with` - let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream { - let name_space = format!("{} ", name); - let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site()); - let disp_resolved = resolve_type(disp_type, pathf_map); - let prefix_word_count = name.split_whitespace().count(); - quote! { - if raw_str.starts_with(#name_lit) { - let prefix_len = #prefix_word_count; - let trimmed_args: Vec = raw.iter().skip(prefix_len).cloned().collect(); - let __cp = <#disp_resolved as ::mingling::Dispatcher>::begin( - &#disp_resolved::default(), - trimmed_args, - ); - return match __cp { - ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0), - ::mingling::ChainProcess::Err(chain_process_error) => { - Err(chain_process_error.into()) - } - }; - } - } - }; - - let mut arms = Vec::new(); - - for (&ch, sub_nodes) in &groups { - let ch_char = ch; - - if sub_nodes.len() == 1 { - let (name, disp_type) = &sub_nodes[0]; - let arm = make_starts_with_arm(name, disp_type); - arms.push(quote! { - Some(#ch_char) => { - #arm - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); - } - }); - } else { - let sub_body = build_dispatch_body(sub_nodes, depth + 1, pathf_map); - arms.push(quote! { - Some(#ch_char) => { - #sub_body - } - }); - } - } - - let exact_checks: Vec = exact_nodes - .iter() - .map(|(name, disp_type)| make_starts_with_arm(name, disp_type)) - .collect(); - - if !exact_checks.is_empty() && !groups.is_empty() { - let match_body = quote! { - match raw_chars.nth(0) { - #(#arms)* - _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())), - } - }; - quote! { - #(#exact_checks)* - #match_body - } - } else if !exact_checks.is_empty() { - quote! { - #(#exact_checks)* - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); - } - } else if arms.is_empty() { - quote! { - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); - } - } else { - quote! { - match raw_chars.nth(0) { - #(#arms)* - _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())), - } - } - } -} diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/dispatcher.rs deleted file mode 100644 index 3698ede..0000000 --- a/mingling_macros/src/dispatcher.rs +++ /dev/null @@ -1,266 +0,0 @@ -#[cfg(feature = "dispatch_tree")] -use just_fmt::snake_case; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, LitStr, Result as SynResult, Token}; - -#[cfg(feature = "dispatch_tree")] -use crate::COMPILE_TIME_DISPATCHERS; - -enum DispatcherChainInput { - Default { - cmd_attrs: Vec, - entry_attrs: Vec, - command_name: syn::LitStr, - command_struct: Ident, - pack: Ident, - }, - #[cfg(feature = "extra_macros")] - Auto { - cmd_attrs: Vec, - command_name: syn::LitStr, - }, -} - -impl Parse for DispatcherChainInput { - fn parse(input: ParseStream) -> SynResult { - // Collect outer attributes for the CMD struct - let cmd_attrs = input.call(Attribute::parse_outer)?; - - if input.peek(syn::LitStr) { - // Parse the command name string first - let command_name: LitStr = input.parse()?; - - // Check if this is the abbreviated form: just "command_name" without ", CMD => Entry" - if input.is_empty() { - #[cfg(feature = "extra_macros")] - { - return Ok(DispatcherChainInput::Auto { - cmd_attrs, - command_name, - }); - } - #[cfg(not(feature = "extra_macros"))] - { - return Err(syn::Error::new( - command_name.span(), - "expected `, CommandStruct => EntryStruct` after command name", - )); - } - } - - // Default format: "command_name", CommandStruct => ChainStruct - input.parse::()?; - let command_struct = input.parse()?; - input.parse::]>()?; - let entry_attrs = input.call(Attribute::parse_outer)?; - let pack = input.parse()?; - - Ok(DispatcherChainInput::Default { - cmd_attrs, - entry_attrs, - command_name, - command_struct, - pack, - }) - } else { - Err(input.lookahead1().error()) - } - } -} - -// NOTICE: The token stream generation patterns in `dispatcher_chain` and `dispatcher_render` -// are nearly identical and could benefit from refactoring into common helper functions. - -#[allow(clippy::too_many_lines)] -pub fn dispatcher(input: TokenStream) -> TokenStream { - // Parse the input - let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); - - #[cfg(not(feature = "extra_macros"))] - let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { - DispatcherChainInput::Default { - cmd_attrs, - entry_attrs, - command_name, - command_struct, - pack, - } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), - }; - - #[cfg(feature = "extra_macros")] - let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { - DispatcherChainInput::Default { - cmd_attrs, - entry_attrs, - command_name, - command_struct, - pack, - } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), - DispatcherChainInput::Auto { - cmd_attrs, - command_name, - } => { - let command_name_str = command_name.value(); - let pascal = dotted_to_pascal_case(&command_name_str); - let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span()); - let pack = Ident::new(&format!("Entry{pascal}"), command_name.span()); - (command_name, command_struct, pack, cmd_attrs, Vec::new()) - } - }; - - let command_name_str = command_name.value(); - - let comp_entry = get_comp_entry(&pack); - - let dispatch_tree_entry = get_dispatch_tree_entry(&command_name_str, &command_struct, &pack); - - let program_type = crate::default_program_path(); - - let expanded = quote! { - #(#cmd_attrs)* - #[derive(Debug, Default)] - pub struct #command_struct; - - ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec); - - #comp_entry - #dispatch_tree_entry - - impl ::mingling::Dispatcher<#program_type> for #command_struct { - fn node(&self) -> ::mingling::Node { - ::mingling::macros::node!(#command_name_str) - } - fn begin(&self, args: Vec) -> ::mingling::ChainProcess<#program_type> { - use ::mingling::Grouped; - #pack::new(args).to_chain() - } - fn clone_dispatcher(&self) -> Box> { - Box::new(#command_struct) - } - } - }; - - expanded.into() -} - -#[cfg(feature = "comp")] -fn get_comp_entry(entry_name: &Ident) -> TokenStream2 { - let comp_entry = quote! { - impl ::mingling::CompletionEntry for #entry_name { - fn get_input(self) -> Vec { - self.inner.clone() - } - } - }; - comp_entry -} - -#[cfg(not(feature = "comp"))] -fn get_comp_entry(_entry_name: &Ident) -> TokenStream2 { - quote! {} -} - -#[cfg(feature = "dispatch_tree")] -fn get_dispatch_tree_entry( - command_name_str: &str, - command_struct: &Ident, - entry_name: &Ident, -) -> TokenStream2 { - let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site()); - quote! { - ::mingling::macros::register_dispatcher!(#node_name_lit, #command_struct, #entry_name); - } -} - -#[cfg(not(feature = "dispatch_tree"))] -fn get_dispatch_tree_entry( - _command_name_str: &str, - _command_struct: &Ident, - _entry_name: &Ident, -) -> TokenStream2 { - quote! {} -} - -#[cfg(feature = "dispatch_tree")] -/// Input format: ("node.name", DispatcherType, EntryName) -struct RegisterDispatcherInput { - node_name: syn::LitStr, - dispatcher_type: Ident, - entry_name: Ident, -} - -#[cfg(feature = "dispatch_tree")] -impl Parse for RegisterDispatcherInput { - fn parse(input: ParseStream) -> SynResult { - let node_name = input.parse()?; - input.parse::()?; - let dispatcher_type = input.parse()?; - input.parse::()?; - let entry_name = input.parse()?; - Ok(RegisterDispatcherInput { - node_name, - dispatcher_type, - entry_name, - }) - } -} - -#[cfg(feature = "dispatch_tree")] -pub fn register_dispatcher(input: TokenStream) -> TokenStream { - let RegisterDispatcherInput { - node_name, - dispatcher_type, - entry_name, - } = 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_ident = Ident::new(&static_name, proc_macro2::Span::call_site()); - - // Register node info in the global collection at compile time - // Format: "node.name:DispatcherType:EntryName" - crate::get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .insert(format!( - "{}:{}:{}", - node_name_str, dispatcher_type, entry_name - )); - - let expanded = quote! { - #[doc(hidden)] - #[allow(nonstandard_style)] - pub static #static_ident: #dispatcher_type = #dispatcher_type; - }; - - expanded.into() -} - -#[cfg(not(feature = "dispatch_tree"))] -pub fn register_dispatcher(_input: TokenStream) -> TokenStream { - quote! {}.into() -} - -/// Converts a dotted command name (e.g. "remote.add") to `PascalCase` (e.g. "`RemoteAdd`"). -/// -/// Each segment is split by `.`, the first character of each segment is uppercased, -/// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax -/// (when `Command => Entry` is omitted) to auto-derive struct names. -#[cfg(feature = "extra_macros")] -fn dotted_to_pascal_case(s: &str) -> String { - s.split('.') - .map(|segment| { - let mut chars = segment.chars(); - match chars.next() { - None => String::new(), - Some(c) => c.to_uppercase().to_string() + chars.as_str(), - } - }) - .collect() -} diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/dispatcher_clap.rs deleted file mode 100644 index 0945e31..0000000 --- a/mingling_macros/src/dispatcher_clap.rs +++ /dev/null @@ -1,241 +0,0 @@ -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, - /// `help = true` (bool only) - help_enabled: bool, -} - -impl Parse for ClapOptions { - fn parse(input: ParseStream) -> syn::Result { - let mut error_struct = None; - let mut help_enabled = false; - - while !input.is_empty() { - // Parse leading comma - input.parse::()?; - - // Allow trailing comma - if input.is_empty() { - break; - } - - let key: Ident = input.parse()?; - input.parse::()?; - - 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 { - // Format: "cmd", Disp, ... - let command_name: LitStr = input.parse()?; - input.parse::()?; - let dispatcher_struct: Ident = input.parse()?; - - let options = if input.is_empty() { - ClapOptions { - error_struct: None, - help_enabled: false, - } - } else { - input.parse::()? - }; - - Ok(DispatcherClapInput { - command_name, - dispatcher_struct, - options, - }) - } -} - -#[cfg(feature = "clap")] -pub 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 #struct_name::default().to_chain(); - } - match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) { - Ok(parsed) => parsed.to_chain(), - Err(e) => { - return #error_struct::new(format!("{}", e.render().ansi())).to_render() - }, - } - } - } else { - quote! { - if ::mingling::this::<#program_path>().user_context.help { - return #struct_name::default().to_chain(); - } - let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args) - .unwrap_or_else(|e| e.exit()); - parsed.to_chain() - } - }; - - // 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 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 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, - ) -> ::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::>(); - - #begin_body - } - - fn clone_dispatcher( - &self, - ) -> Box> { - 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/entry.rs b/mingling_macros/src/entry.rs deleted file mode 100644 index 2ac5d6b..0000000 --- a/mingling_macros/src/entry.rs +++ /dev/null @@ -1,70 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{Ident, LitStr, parse_macro_input}; - -enum EntryInput { - Typed { ident: Ident, strings: Vec }, - Untyped { strings: Vec }, -} - -impl syn::parse::Parse for EntryInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - // entry!(EntryType, ["a", "b", "c"]) - // entry!["a", "b", "c"] — comes in as just ["a", "b", "c"] - if input.peek(Ident) && input.peek2(syn::Token![,]) { - // entry!(EntryType, ["a", "b", "c"]) - let ident: Ident = input.parse()?; - let _comma: syn::Token![,] = input.parse()?; - let content; - syn::bracketed!(content in input); - let strings = parse_strings(&content)?; - Ok(EntryInput::Typed { ident, strings }) - } else { - // entry!["a", "b", "c"] — bare bracket content - let strings = parse_strings(input)?; - Ok(EntryInput::Untyped { strings }) - } - } -} - -fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result> { - let mut strings = Vec::new(); - while !input.is_empty() { - let s: LitStr = input.parse()?; - strings.push(s.value()); - if input.peek(syn::Token![,]) { - let _comma: syn::Token![,] = input.parse()?; - } - } - Ok(strings) -} - -pub fn entry(input: TokenStream) -> TokenStream { - let parsed = parse_macro_input!(input as EntryInput); - - let strings = match &parsed { - EntryInput::Typed { strings, .. } | EntryInput::Untyped { strings } => strings, - }; - let string_exprs = strings - .iter() - .map(|s| { - let lit = syn::LitStr::new(s, proc_macro2::Span::call_site()); - quote! { #lit.to_string() } - }) - .collect::>(); - - let expanded = match parsed { - EntryInput::Typed { ident, .. } => { - quote! { - #ident::new(vec![#(#string_exprs),*]) - } - } - EntryInput::Untyped { .. } => { - quote! { - vec![#(#string_exprs),*].into() - } - } - }; - - expanded.into() -} diff --git a/mingling_macros/src/enum_tag.rs b/mingling_macros/src/enum_tag.rs deleted file mode 100644 index 6277b69..0000000 --- a/mingling_macros/src/enum_tag.rs +++ /dev/null @@ -1,168 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Variant, parse_macro_input, -}; - -pub fn derive_enum_tag(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - match derive_enum_tag_impl(input) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into(), - } -} - -/// Implementation of the `EnumTag` derive macro -fn derive_enum_tag_impl(input: DeriveInput) -> Result { - let enum_name = &input.ident; - let generics = &input.generics; - - // Extract enum data - let data = match input.data { - Data::Enum(data_enum) => data_enum, - Data::Struct(_) => { - return Err(Error::new_spanned( - enum_name, - "EnumTag can only be derived for enums, not structs", - )); - } - Data::Union(_) => { - return Err(Error::new_spanned( - enum_name, - "EnumTag can only be derived for enums, not unions", - )); - } - }; - - // Process each variant - let mut variant_info = Vec::new(); - let mut match_arms = Vec::new(); - let mut build_match_arms = Vec::new(); - - for variant in data.variants { - process_variant( - &variant, - enum_name, - &mut variant_info, - &mut match_arms, - &mut build_match_arms, - )?; - } - - // Generate the implementation - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let expanded = quote! { - impl #impl_generics ::mingling::EnumTag for #enum_name #ty_generics #where_clause { - fn enum_info(&self) -> (&'static str, &'static str) { - match self { - #(#match_arms)* - } - } - - fn build_enum(name: String) -> Option - where - Self: Sized - { - match name.as_str() { - #(#build_match_arms)* - _ => None, - } - } - - fn enums() -> &'static [(&'static str, &'static str)] { - &[#(#variant_info),*] - } - } - }; - - Ok(expanded) -} - -/// Process a single enum variant -fn process_variant( - variant: &Variant, - enum_name: &Ident, - variant_info: &mut Vec, - match_arms: &mut Vec, - build_match_arms: &mut Vec, -) -> Result<()> { - let variant_name = variant.ident.clone(); - - // Check if variant has fields - match &variant.fields { - Fields::Unit => { - // Good, unit variant - } - Fields::Named(_) | Fields::Unnamed(_) => { - return Err(Error::new_spanned( - variant, - format!( - "EnumTag cannot be derived for enum variant `{variant_name}` with fields. Only unit variants are supported." - ), - )); - } - } - - // Extract description from #[enum_desc] attribute - let description = extract_description(&variant.attrs)?; - - // Extract rename from #[enum_rename] attribute - let rename = extract_rename(&variant.attrs)?; - - // Generate tokens for this variant - let variant_name_str = variant_name.to_string(); - let display_name = rename.unwrap_or_else(|| variant_name_str.clone()); - let description_str = description.unwrap_or_default(); - - variant_info.push(quote! { - (#display_name, #description_str) - }); - - match_arms.push(quote! { - #enum_name::#variant_name => (#display_name, #description_str), - }); - - build_match_arms.push(quote! { - #display_name => Some(#enum_name::#variant_name), - }); - - Ok(()) -} - -/// Extract description from #[`enum_desc`] attribute -fn extract_description(attrs: &[Attribute]) -> Result> { - for attr in attrs { - if attr.path().is_ident("enum_desc") { - return match attr.parse_args::() { - Ok(lit_str) => Ok(Some(lit_str.value())), - Err(_) => Err(Error::new_spanned( - attr, - "#[enum_desc] attribute must be in the form `#[enum_desc(\"description\")]`", - )), - }; - } - } - - // If no #[enum_desc] attribute, return None - Ok(None) -} - -/// Extract rename from #[`enum_rename`] attribute -fn extract_rename(attrs: &[Attribute]) -> Result> { - for attr in attrs { - if attr.path().is_ident("enum_rename") { - return match attr.parse_args::() { - Ok(lit_str) => Ok(Some(lit_str.value())), - Err(_) => Err(Error::new_spanned( - attr, - "#[enum_rename] attribute must be in the form `#[enum_rename(\"new_name\")]`", - )), - }; - } - } - - // If no #[enum_rename] attribute, return None - Ok(None) -} diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs new file mode 100644 index 0000000..720b20a --- /dev/null +++ b/mingling_macros/src/func.rs @@ -0,0 +1,12 @@ +pub(crate) mod dispatcher; +#[cfg(feature = "extra_macros")] +pub(crate) mod entry; +pub(crate) mod gen_program; +#[cfg(feature = "extra_macros")] +pub(crate) mod group; +pub(crate) mod node; +pub(crate) mod pack; +#[cfg(feature = "extra_macros")] +pub(crate) mod pack_err; +#[cfg(feature = "comp")] +pub(crate) mod suggest; diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs new file mode 100644 index 0000000..808df75 --- /dev/null +++ b/mingling_macros/src/func/dispatcher.rs @@ -0,0 +1,266 @@ +#[cfg(feature = "dispatch_tree")] +use just_fmt::snake_case; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, LitStr, Result as SynResult, Token}; + +#[cfg(feature = "dispatch_tree")] +use crate::COMPILE_TIME_DISPATCHERS; + +enum DispatcherChainInput { + Default { + cmd_attrs: Vec, + entry_attrs: Vec, + command_name: syn::LitStr, + command_struct: Ident, + pack: Ident, + }, + #[cfg(feature = "extra_macros")] + Auto { + cmd_attrs: Vec, + command_name: syn::LitStr, + }, +} + +impl Parse for DispatcherChainInput { + fn parse(input: ParseStream) -> SynResult { + // Collect outer attributes for the CMD struct + let cmd_attrs = input.call(Attribute::parse_outer)?; + + if input.peek(syn::LitStr) { + // Parse the command name string first + let command_name: LitStr = input.parse()?; + + // Check if this is the abbreviated form: just "command_name" without ", CMD => Entry" + if input.is_empty() { + #[cfg(feature = "extra_macros")] + { + return Ok(DispatcherChainInput::Auto { + cmd_attrs, + command_name, + }); + } + #[cfg(not(feature = "extra_macros"))] + { + return Err(syn::Error::new( + command_name.span(), + "expected `, CommandStruct => EntryStruct` after command name", + )); + } + } + + // Default format: "command_name", CommandStruct => ChainStruct + input.parse::()?; + let command_struct = input.parse()?; + input.parse::]>()?; + let entry_attrs = input.call(Attribute::parse_outer)?; + let pack = input.parse()?; + + Ok(DispatcherChainInput::Default { + cmd_attrs, + entry_attrs, + command_name, + command_struct, + pack, + }) + } else { + Err(input.lookahead1().error()) + } + } +} + +// NOTICE: The token stream generation patterns in `dispatcher_chain` and `dispatcher_render` +// are nearly identical and could benefit from refactoring into common helper functions. + +#[allow(clippy::too_many_lines)] +pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { + // Parse the input + let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); + + #[cfg(not(feature = "extra_macros"))] + let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { + DispatcherChainInput::Default { + cmd_attrs, + entry_attrs, + command_name, + command_struct, + pack, + } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), + }; + + #[cfg(feature = "extra_macros")] + let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { + DispatcherChainInput::Default { + cmd_attrs, + entry_attrs, + command_name, + command_struct, + pack, + } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), + DispatcherChainInput::Auto { + cmd_attrs, + command_name, + } => { + let command_name_str = command_name.value(); + let pascal = dotted_to_pascal_case(&command_name_str); + let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span()); + let pack = Ident::new(&format!("Entry{pascal}"), command_name.span()); + (command_name, command_struct, pack, cmd_attrs, Vec::new()) + } + }; + + let command_name_str = command_name.value(); + + let comp_entry = get_comp_entry(&pack); + + let dispatch_tree_entry = get_dispatch_tree_entry(&command_name_str, &command_struct, &pack); + + let program_type = crate::default_program_path(); + + let expanded = quote! { + #(#cmd_attrs)* + #[derive(Debug, Default)] + pub struct #command_struct; + + ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec); + + #comp_entry + #dispatch_tree_entry + + impl ::mingling::Dispatcher<#program_type> for #command_struct { + fn node(&self) -> ::mingling::Node { + ::mingling::macros::node!(#command_name_str) + } + fn begin(&self, args: Vec) -> ::mingling::ChainProcess<#program_type> { + use ::mingling::Grouped; + #pack::new(args).to_chain() + } + fn clone_dispatcher(&self) -> Box> { + Box::new(#command_struct) + } + } + }; + + expanded.into() +} + +#[cfg(feature = "comp")] +fn get_comp_entry(entry_name: &Ident) -> TokenStream2 { + let comp_entry = quote! { + impl ::mingling::CompletionEntry for #entry_name { + fn get_input(self) -> Vec { + self.inner.clone() + } + } + }; + comp_entry +} + +#[cfg(not(feature = "comp"))] +fn get_comp_entry(_entry_name: &Ident) -> TokenStream2 { + quote! {} +} + +#[cfg(feature = "dispatch_tree")] +fn get_dispatch_tree_entry( + command_name_str: &str, + command_struct: &Ident, + entry_name: &Ident, +) -> TokenStream2 { + let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site()); + quote! { + ::mingling::macros::register_dispatcher!(#node_name_lit, #command_struct, #entry_name); + } +} + +#[cfg(not(feature = "dispatch_tree"))] +fn get_dispatch_tree_entry( + _command_name_str: &str, + _command_struct: &Ident, + _entry_name: &Ident, +) -> TokenStream2 { + quote! {} +} + +#[cfg(feature = "dispatch_tree")] +/// Input format: ("node.name", DispatcherType, EntryName) +struct RegisterDispatcherInput { + node_name: syn::LitStr, + dispatcher_type: Ident, + entry_name: Ident, +} + +#[cfg(feature = "dispatch_tree")] +impl Parse for RegisterDispatcherInput { + fn parse(input: ParseStream) -> SynResult { + let node_name = input.parse()?; + input.parse::()?; + let dispatcher_type = input.parse()?; + input.parse::()?; + let entry_name = input.parse()?; + Ok(RegisterDispatcherInput { + node_name, + dispatcher_type, + entry_name, + }) + } +} + +#[cfg(feature = "dispatch_tree")] +pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { + let RegisterDispatcherInput { + node_name, + dispatcher_type, + entry_name, + } = 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_ident = Ident::new(&static_name, proc_macro2::Span::call_site()); + + // Register node info in the global collection at compile time + // Format: "node.name:DispatcherType:EntryName" + crate::get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .insert(format!( + "{}:{}:{}", + node_name_str, dispatcher_type, entry_name + )); + + let expanded = quote! { + #[doc(hidden)] + #[allow(nonstandard_style)] + pub static #static_ident: #dispatcher_type = #dispatcher_type; + }; + + expanded.into() +} + +#[cfg(not(feature = "dispatch_tree"))] +pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream { + quote! {}.into() +} + +/// Converts a dotted command name (e.g. "remote.add") to `PascalCase` (e.g. "`RemoteAdd`"). +/// +/// Each segment is split by `.`, the first character of each segment is uppercased, +/// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax +/// (when `Command => Entry` is omitted) to auto-derive struct names. +#[cfg(feature = "extra_macros")] +fn dotted_to_pascal_case(s: &str) -> String { + s.split('.') + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().to_string() + chars.as_str(), + } + }) + .collect() +} diff --git a/mingling_macros/src/func/entry.rs b/mingling_macros/src/func/entry.rs new file mode 100644 index 0000000..35209e5 --- /dev/null +++ b/mingling_macros/src/func/entry.rs @@ -0,0 +1,70 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, LitStr, parse_macro_input}; + +enum EntryInput { + Typed { ident: Ident, strings: Vec }, + Untyped { strings: Vec }, +} + +impl syn::parse::Parse for EntryInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // entry!(EntryType, ["a", "b", "c"]) + // entry!["a", "b", "c"] — comes in as just ["a", "b", "c"] + if input.peek(Ident) && input.peek2(syn::Token![,]) { + // entry!(EntryType, ["a", "b", "c"]) + let ident: Ident = input.parse()?; + let _comma: syn::Token![,] = input.parse()?; + let content; + syn::bracketed!(content in input); + let strings = parse_strings(&content)?; + Ok(EntryInput::Typed { ident, strings }) + } else { + // entry!["a", "b", "c"] — bare bracket content + let strings = parse_strings(input)?; + Ok(EntryInput::Untyped { strings }) + } + } +} + +fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result> { + let mut strings = Vec::new(); + while !input.is_empty() { + let s: LitStr = input.parse()?; + strings.push(s.value()); + if input.peek(syn::Token![,]) { + let _comma: syn::Token![,] = input.parse()?; + } + } + Ok(strings) +} + +pub(crate) fn entry(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as EntryInput); + + let strings = match &parsed { + EntryInput::Typed { strings, .. } | EntryInput::Untyped { strings } => strings, + }; + let string_exprs = strings + .iter() + .map(|s| { + let lit = syn::LitStr::new(s, proc_macro2::Span::call_site()); + quote! { #lit.to_string() } + }) + .collect::>(); + + let expanded = match parsed { + EntryInput::Typed { ident, .. } => { + quote! { + #ident::new(vec![#(#string_exprs),*]) + } + } + EntryInput::Untyped { .. } => { + quote! { + vec![#(#string_exprs),*].into() + } + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs new file mode 100644 index 0000000..7128a9a --- /dev/null +++ b/mingling_macros/src/func/gen_program.rs @@ -0,0 +1,602 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +use crate::CHAINS; +use crate::CHAINS_EXIST; +#[cfg(feature = "dispatch_tree")] +use crate::COMPILE_TIME_DISPATCHERS; +#[cfg(feature = "comp")] +use crate::COMPLETIONS; +use crate::HELP_REQUESTS; +use crate::PACKED_TYPES; +use crate::RENDERERS; +use crate::RENDERERS_EXIST; +#[cfg(feature = "structural_renderer")] +use crate::STRUCTURAL_RENDERERS; +use crate::attr::{chain, renderer}; +use crate::get_global_set; +#[cfg(feature = "dispatch_tree")] +use crate::systems::dispatch_tree_gen; + +#[cfg(feature = "async")] +const ASYNC_ENABLED: bool = true; +#[cfg(not(feature = "async"))] +const ASYNC_ENABLED: bool = false; + +/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. +fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { + let s = entry.to_string(); + let arrow_idx = s + .find("=>") + .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); + let struct_str = s[..arrow_idx].trim(); + let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); + let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); + let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); + (struct_ident, variant_ident) +} + +/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. +/// Always compiled; returns empty map when pathf feature is not enabled. +fn load_pathf_map() -> std::collections::HashMap { + if !cfg!(feature = "pathf") { + return std::collections::HashMap::new(); + } + let out_dir = std::env::var("OUT_DIR").ok(); + let crate_name = std::env::var("CARGO_PKG_NAME").ok(); + match (out_dir, crate_name) { + (Some(dir), Some(name)) => { + let path = std::path::Path::new(&dir).join(&name).join("type_using.rs"); + match std::fs::read_to_string(&path) { + Ok(content) => content + .lines() + .filter_map(|line| { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("use ") { + let path = rest.strip_suffix(';').unwrap_or(rest); + if let Some((_mod, type_name)) = path.rsplit_once("::") { + return Some((type_name.to_string(), path.to_string())); + } + } + None + }) + .collect(), + Err(_) => std::collections::HashMap::new(), + } + } + _ => std::collections::HashMap::new(), + } +} + +/// Resolves a type name to its full path token stream using the pathf mapping. +pub(crate) fn resolve_type( + name: &str, + map: &std::collections::HashMap, +) -> proc_macro2::TokenStream { + if let Some(full_path) = map.get(name) { + syn::parse_str::(full_path).unwrap_or_else(|_| { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + }) + } else { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + } +} + +pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "comp")] + let comp_gen = quote! { + ::mingling::macros::program_comp_gen!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_gen = quote! {}; + + TokenStream::from(quote! { + /// Alias for the current program type `crate::ThisProgram` + pub type Next = ::mingling::ChainProcess; + + impl ::mingling::Routable for ::mingling::ChainProcess + { + fn to_chain(self) -> ::mingling::ChainProcess { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) + } + other => other, + } + } + + fn to_render(self) -> ::mingling::ChainProcess { + 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!(); + }) +} + +#[cfg(feature = "comp")] +pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "async")] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub async fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::(&ctx); + crate::CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(not(feature = "async"))] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::(&ctx); + crate::CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(feature = "dispatch_tree")] + let internal_dispatcher_comp = quote! { + use __internal_completion_mod::__internal_dispatcher_comp; + }; + + #[cfg(not(feature = "dispatch_tree"))] + let internal_dispatcher_comp = quote! {}; + + let comp_dispatcher = quote! { + #[doc(hidden)] + mod __internal_completion_mod { + use ::mingling::Grouped; + ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); + ::mingling::macros::pack!( + CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) + ); + } + #internal_dispatcher_comp + use __internal_completion_mod::CompletionContext; + use __internal_completion_mod::CompletionSuggest; + pub use __internal_completion_mod::CMDCompletion; + + #fn_exec_comp + + ::mingling::macros::register_type!(CompletionContext); + + #[allow(unused)] + #[doc(hidden)] + #[::mingling::macros::renderer] + pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { + let result = ::mingling::RenderResult::default(); + let (ctx, suggest) = prev.inner; + ::mingling::CompletionHelper::render_suggest::(ctx, suggest); + result + } + }; + + TokenStream::from(comp_dispatcher) +} + +pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream { + let type_ident = parse_macro_input!(input as syn::Ident); + let entry_str = type_ident.to_string(); + + get_global_set(&PACKED_TYPES) + .lock() + .unwrap() + .insert(entry_str); + + TokenStream::new() +} + +pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream { + chain::register_chain(input) +} + +pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream { + renderer::register_renderer(input) +} + +pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "structural_renderer")] + let pack_empty = quote! { + #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + #[cfg(not(feature = "structural_renderer"))] + let pack_empty = quote! { + #[derive(::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + let expanded = quote! { + ::mingling::macros::pack!(ErrorRendererNotFound = String); + ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec); + #pack_empty + }; + TokenStream::from(expanded) +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { + let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); + + let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); + + let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); + let chains = get_global_set(&CHAINS).lock().unwrap().clone(); + let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); + let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); + + #[cfg(feature = "structural_renderer")] + let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clone(); + + #[cfg(feature = "comp")] + let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); + + let packed_types: Vec = packed_types + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let renderer_tokens: Vec = renderers + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let chain_tokens: Vec = chains + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let renderer_exist_tokens: Vec = renderer_exist + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let chain_exist_tokens: Vec = chain_exist + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let pathf_map: std::collections::HashMap = if cfg!(feature = "pathf") { + load_pathf_map() + } else { + std::collections::HashMap::new() + }; + + let pathf_uses: Vec = if cfg!(feature = "pathf") { + pathf_map + .values() + .map(|path| format!("use {};", path).parse().unwrap_or_default()) + .collect() + } else { + Vec::new() + }; + + #[cfg(feature = "structural_renderer")] + let structural_renderer_tokens: Vec = structural_renderers + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + #[cfg(feature = "structural_renderer")] + let structural_render = quote! { + fn structural_render( + any: ::mingling::AnyOutput, + setting: &::mingling::StructuralRendererSetting, + ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#structural_renderer_tokens)* + _ => { + // Non-structural types: render ResultEmpty (which implements + // StructuralData + Serialize) instead of producing nothing. + let mut r = ::mingling::RenderResult::default(); + ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; + Ok(r) + } + } + } + }; + + #[cfg(not(feature = "structural_renderer"))] + let structural_render = quote! {}; + + #[cfg(feature = "dispatch_tree")] + let compile_time_dispatchers: Vec = get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clone() + .iter() + .cloned() + .collect(); + + #[cfg(feature = "dispatch_tree")] + let dispatch_tree_nodes = { + let entries: Vec<(String, String, String)> = compile_time_dispatchers + .iter() + .filter_map(|entry| { + let parts: Vec<&str> = entry.split(':').collect(); + if parts.len() == 3 { + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + )) + } else { + None + } + }) + .collect(); + + let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); + let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); + + quote! { + #get_nodes_fn + #dispatch_trie_fn + } + }; + + #[cfg(not(feature = "dispatch_tree"))] + let dispatch_tree_nodes = quote! {}; + + #[cfg(feature = "comp")] + let completion_tokens: Vec = completions + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + #[cfg(feature = "comp")] + let comp = quote! { + fn do_comp(any: &::mingling::AnyOutput, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#completion_tokens)* + _ => ::mingling::Suggest::FileCompletion, + } + } + }; + + #[cfg(not(feature = "comp"))] + let comp = quote! {}; + + // Build render function arms from stored entries + let render_fn = + if renderer_tokens.is_empty() { + quote! { + fn render(_any: ::mingling::AnyOutput) -> ::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); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + <#resolved_struct as ::mingling::Renderer>::render(value) + } + } + }).collect(); + quote! { + fn render(any: ::mingling::AnyOutput) -> ::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| { + 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); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + let fut = async { <#resolved_struct as ::mingling::Chain>::proc(value).await }; + ::std::boxed::Box::pin(fut) + } + } + }).collect(); + + let chain_arms_sync: Vec<_> = chain_tokens + .iter() + .map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + <#resolved_struct as ::mingling::Chain>::proc(value) + } + } + }) + .collect(); + + let do_chain_fn = if chain_tokens.is_empty() { + quote! { + fn do_chain(_any: ::mingling::AnyOutput) -> ::mingling::ChainProcess { + ::core::panic!("No chain found for type id") + } + } + } else if ASYNC_ENABLED { + quote! { + fn do_chain( + any: ::mingling::AnyOutput, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::std::marker::Send>> { + match any.member_id { + #(#chain_arms_async)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + } else { + quote! { + fn do_chain( + any: ::mingling::AnyOutput, + ) -> ::mingling::ChainProcess { + match any.member_id { + #(#chain_arms_sync)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + }; + + let help_tokens: Vec = get_global_set(&HELP_REQUESTS) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let num_variants = packed_types.len(); + let repr_type = if u8::try_from(num_variants).is_ok() { + quote! { u8 } + } else if u16::try_from(num_variants).is_ok() { + quote! { u16 } + } else if u32::try_from(num_variants).is_ok() { + quote! { u32 } + } else { + quote! { u128 } + }; + + let expanded = quote! { + #[derive(Debug, PartialEq, Eq, Clone)] + #[repr(#repr_type)] + #[allow(nonstandard_style)] + pub enum #name { + #(#packed_types),* + } + + impl ::std::fmt::Display for #name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* + } + } + } + + impl ::mingling::ProgramCollect for #name { + type Enum = #name; + type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type ErrorRendererNotFound = ErrorRendererNotFound; + type ResultEmpty = ResultEmpty; + fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput { + ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) + } + fn build_dispatcher_not_found(args: Vec) -> ::mingling::AnyOutput { + ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) + } + fn build_empty_result() -> ::mingling::AnyOutput { + ::mingling::AnyOutput::new(ResultEmpty) + } + #render_fn + #do_chain_fn + fn render_help(any: ::mingling::AnyOutput) -> ::mingling::RenderResult { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#help_tokens)* + _ => ::mingling::RenderResult::default(), + } + } + fn has_renderer(any: &::mingling::AnyOutput) -> bool { + match any.member_id { + #(#renderer_exist_tokens)* + _ => false + } + } + fn has_chain(any: &::mingling::AnyOutput) -> bool { + match any.member_id { + #(#chain_exist_tokens)* + _ => false + } + } + #dispatch_tree_nodes + #structural_render + #comp + } + + impl #name { + /// Creates a new `Program<#name>` instance with default configuration. + pub fn new() -> ::mingling::Program<#name> { + ::mingling::Program::new() + } + + /// Returns a static reference to the global `Program<#name>` singleton. + pub fn this() -> &'static ::mingling::Program<#name> { + &::mingling::this::<#name>() + } + } + }; + + // Clear all global registries to prevent stale state in Rust Analyzer + get_global_set(&PACKED_TYPES).lock().unwrap().clear(); + get_global_set(&CHAINS).lock().unwrap().clear(); + get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); + get_global_set(&RENDERERS).lock().unwrap().clear(); + get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); + get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); + #[cfg(feature = "comp")] + get_global_set(&COMPLETIONS).lock().unwrap().clear(); + #[cfg(feature = "dispatch_tree")] + get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clear(); + #[cfg(feature = "structural_renderer")] + get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clear(); + + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/func/group.rs b/mingling_macros/src/func/group.rs new file mode 100644 index 0000000..b865913 --- /dev/null +++ b/mingling_macros/src/func/group.rs @@ -0,0 +1,147 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Result as SynResult, TypePath}; + +/// Input for the `group!` macro +/// +/// # Syntax +/// +/// ```rust,ignore +/// /// Only a type path — uses default `crate::ThisProgram` as program +/// group!(std::io::Error); +/// group!(ParseIntError); +/// +/// /// With an alias — creates a `pub type Alias = Path;` and uses `Alias` as variant name +/// group!(IoError = std::io::Error); +/// ``` +enum GroupInput { + /// `group!(TypePath)` — variant name is the last path segment + Plain(TypePath), + + /// `group!(Alias = TypePath)` — variant name is `Alias`, also generates `pub type Alias = TypePath;` + Aliased { alias: Ident, type_path: TypePath }, +} + +impl Parse for GroupInput { + fn parse(input: ParseStream) -> SynResult { + // Peek ahead: if the second token is `=`, parse as aliased form + let fork = input.fork(); + let _first: Ident = fork.parse()?; + if fork.peek(syn::Token![=]) { + // Consume the ident and `=` from the real input + let alias: Ident = input.parse()?; + let _eq: syn::Token![=] = input.parse()?; + let type_path: TypePath = input.parse()?; + Ok(GroupInput::Aliased { alias, type_path }) + } else { + let type_path: TypePath = input.parse()?; + Ok(GroupInput::Plain(type_path)) + } + } +} + +/// Convert a type path into a valid module name segment +/// +/// e.g. `std::io::Error` -> `internal_group_std_io_error` +fn module_name_from_type(type_path: &TypePath) -> Ident { + let segments: Vec = type_path + .path + .segments + .iter() + .map(|seg| seg.ident.to_string().to_lowercase()) + .collect(); + Ident::new( + &format!("internal_group_{}", segments.join("_")), + proc_macro2::Span::call_site(), + ) +} + +/// Get the last segment name of a type path (the simple type name) +/// +/// e.g. `std::io::Error` -> `Error` +fn type_simple_name(type_path: &TypePath) -> Ident { + type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone() +} + +/// Generate the `use` token for the type path inside the generated module. +/// +/// - Multi-segment path (e.g. `std::num::ParseIntError`): `use std::num::ParseIntError;` +/// - Single-segment path (e.g. `ParseIntError`): `use super::ParseIntError;` +fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream { + if type_path.path.segments.len() > 1 { + // Full path: use it directly + quote! { + #[allow(unused_imports)] + use #type_path; + } + } else { + // Single ident: import from parent scope + let ident = type_simple_name(type_path); + quote! { + #[allow(unused_imports)] + use super::#ident; + } + } +} + +pub(crate) fn group_macro(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as GroupInput); + + let is_aliased = matches!(input, GroupInput::Aliased { .. }); + + let (type_path, type_name, alias_stmt) = match input { + GroupInput::Plain(type_path) => { + let type_name = type_simple_name(&type_path); + (type_path, type_name, quote! {}) + } + GroupInput::Aliased { alias, type_path } => { + let type_name = alias.clone(); + let alias_stmt = quote! { + pub type #alias = #type_path; + }; + (type_path, type_name, alias_stmt) + } + }; + + let program_path = crate::default_program_path(); + + // Create a unique module name from the type path (use alias name for aliased form) + let module_name = module_name_from_type(&type_path); + // Generate the appropriate `use` statement for the type + let type_use = gen_type_use(&type_path); + + // For aliased form, also import the alias from parent scope + let alias_use = if is_aliased { + quote! { use super::#type_name; } + } else { + quote! {} + }; + + // Generate the module with the Grouped implementation + let expanded = quote! { + #alias_stmt + #[allow(non_camel_case_types)] + mod #module_name { + use #program_path as __MinglingProgram; + #type_use + #alias_use + + impl ::mingling::Grouped<__MinglingProgram> for #type_name { + fn member_id() -> __MinglingProgram { + __MinglingProgram::#type_name + } + } + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func/node.rs b/mingling_macros/src/func/node.rs new file mode 100644 index 0000000..1b944a1 --- /dev/null +++ b/mingling_macros/src/func/node.rs @@ -0,0 +1,59 @@ +use just_fmt::kebab_case; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{LitStr, Result as SynResult}; + +/// Parses a string literal input for the node macro +struct NodeInput { + path: LitStr, +} + +impl Parse for NodeInput { + fn parse(input: ParseStream) -> SynResult { + Ok(NodeInput { + path: input.parse()?, + }) + } +} + +pub(crate) fn node(input: TokenStream) -> TokenStream { + // Parse the input as a string literal + let input_parsed = syn::parse_macro_input!(input as NodeInput); + let path_str = input_parsed.path.value(); + + // If the input string is empty, return an empty Node + if path_str.is_empty() { + return quote! { + mingling::Node::default() + } + .into(); + } + + // Split the path by dots + let parts: Vec = path_str + .split('.') + .map(|s| { + if s.starts_with('_') { + s.to_string() + } else { + kebab_case!(s).to_string() + } + }) + .collect(); + + // Build the expression starting from Node::default() + let mut expr: TokenStream2 = quote! { + mingling::Node::default() + }; + + // Add .join() calls for each part of the path + for part in parts { + expr = quote! { + #expr.join(#part) + }; + } + + expr.into() +} diff --git a/mingling_macros/src/func/pack.rs b/mingling_macros/src/func/pack.rs new file mode 100644 index 0000000..a1a7e6b --- /dev/null +++ b/mingling_macros/src/func/pack.rs @@ -0,0 +1,149 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, Result as SynResult, Token, Type}; + +struct PackInput { + attrs: Vec, + type_name: Ident, + inner_type: Type, +} + +impl Parse for PackInput { + fn parse(input: ParseStream) -> SynResult { + let attrs = input.call(Attribute::parse_outer)?; + let type_name: Ident = input.parse()?; + input.parse::()?; + let inner_type: Type = input.parse()?; + + Ok(PackInput { + attrs, + type_name, + inner_type, + }) + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn pack(input: TokenStream) -> TokenStream { + let pack_input = syn::parse_macro_input!(input as PackInput); + + let group_name = crate::default_program_path(); + let type_name = pack_input.type_name; + let inner_type = pack_input.inner_type; + let attrs = pack_input.attrs; + + // Generate the struct definition + // Note: No longer derives Serialize under structural_renderer. + // Use pack_structual! for structured output support. + let struct_def = quote! { + #(#attrs)* + pub struct #type_name { + pub(crate) inner: #inner_type, + } + }; + + // Generate the new() method + let new_impl = quote! { + impl #type_name { + /// Creates a new instance of the wrapper type + pub fn new(inner: #inner_type) -> Self { + Self { inner } + } + } + }; + + // Generate From and Into implementations + let from_into_impl = quote! { + impl From<#inner_type> for #type_name { + fn from(inner: #inner_type) -> Self { + Self::new(inner) + } + } + + impl From<#type_name> for #inner_type { + fn from(wrapper: #type_name) -> #inner_type { + wrapper.inner + } + } + }; + + // Generate AsRef and AsMut implementations + let as_ref_impl = quote! { + impl ::std::convert::AsRef<#inner_type> for #type_name { + fn as_ref(&self) -> &#inner_type { + &self.inner + } + } + + impl ::std::convert::AsMut<#inner_type> for #type_name { + fn as_mut(&mut self) -> &mut #inner_type { + &mut self.inner + } + } + }; + + // Generate Deref and DerefMut implementations + let deref_impl = quote! { + impl ::std::ops::Deref for #type_name { + type Target = #inner_type; + + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl ::std::ops::DerefMut for #type_name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + }; + + // Check if the inner type implements Default by generating conditional code + let default_impl = quote! { + impl ::std::default::Default for #type_name + where + #inner_type: ::std::default::Default, + { + fn default() -> Self { + Self::new(::std::default::Default::default()) + } + } + }; + + let register_impl = quote! { + ::mingling::macros::register_type!(#type_name); + }; + + let expanded = quote! { + #struct_def + + #new_impl + #from_into_impl + #as_ref_impl + #deref_impl + #default_impl + #register_impl + + impl Into> for #type_name { + fn into(self) -> mingling::AnyOutput<#group_name> { + mingling::AnyOutput::new(self) + } + } + + impl Into> for #type_name { + fn into(self) -> mingling::ChainProcess<#group_name> { + mingling::AnyOutput::new(self).route_chain() + } + } + + impl ::mingling::Grouped<#group_name> for #type_name { + fn member_id() -> #group_name { + #group_name::#type_name + } + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs new file mode 100644 index 0000000..36e550a --- /dev/null +++ b/mingling_macros/src/func/pack_err.rs @@ -0,0 +1,209 @@ +use just_fmt::snake_case; +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, Token, Type, parse_macro_input}; + +enum PackErrInput { + /// pack_err!(ErrorNotFound) + Simple { type_name: Ident }, + /// pack_err!(ErrorNotDir = PathBuf) + Typed { + type_name: Ident, + inner_type: Box, + }, +} + +impl syn::parse::Parse for PackErrInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let type_name: Ident = input.parse()?; + + if input.peek(Token![=]) { + input.parse::()?; + let inner_type: Type = input.parse()?; + Ok(PackErrInput::Typed { + type_name, + inner_type: Box::new(inner_type), + }) + } else { + Ok(PackErrInput::Simple { type_name }) + } + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn pack_err(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as PackErrInput); + + match parsed { + PackErrInput::Simple { type_name } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + // Note: No longer derives Serialize under structural_renderer. + // Use pack_err_structural for structured output support. + let derive = quote! { + #[derive(::mingling::Grouped)] + }; + + let expanded = quote! { + #derive + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + } + + impl ::std::default::Default for #type_name { + fn default() -> Self { + Self { + name: #snake_name.into(), + } + } + } + + ::mingling::macros::register_type!(#type_name); + }; + + expanded.into() + } + PackErrInput::Typed { + type_name, + inner_type, + } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + // Note: No longer derives Serialize under structural_renderer. + // Use pack_err_structural for structured output support. + let derive = quote! { + #[derive(::mingling::Grouped)] + }; + + let expanded = quote! { + #derive + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + /// Additional context info for this error. + pub info: #inner_type, + } + + impl #type_name { + /// Creates a new error with the given info. + /// The `name` field is automatically set to the snake_case of the struct name. + pub fn new(info: #inner_type) -> Self { + Self { + name: #snake_name.into(), + info, + } + } + } + + ::mingling::macros::register_type!(#type_name); + }; + + expanded.into() + } + } +} + +/// `pack_err_structural!` — like `pack_err!` but also marks the type as +/// supporting structured output via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// pack_err_structural!(ErrorNotFound); +/// pack_err_structural!(ErrorNotDir = PathBuf); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// pack_err!(ErrorNotFound); +/// impl ::mingling::__private::StructuralDataSealed for ErrorNotFound {} +/// impl ::mingling::__private::StructuralData for ErrorNotFound {} +/// ``` +#[cfg(feature = "structural_renderer")] +pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as PackErrInput); + + let type_name = match &parsed { + PackErrInput::Simple { type_name } => type_name.clone(), + PackErrInput::Typed { type_name, .. } => type_name.clone(), + }; + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + crate::get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + let structural_data = quote! { + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + }; + + // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed) + match parsed { + PackErrInput::Simple { type_name } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + let expanded = quote! { + #[derive(::mingling::Grouped, ::serde::Serialize)] + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + } + + impl ::std::default::Default for #type_name { + fn default() -> Self { + Self { + name: #snake_name.into(), + } + } + } + + ::mingling::macros::register_type!(#type_name); + + #structural_data + }; + + expanded.into() + } + PackErrInput::Typed { + type_name, + inner_type, + } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + let expanded = quote! { + #[derive(::mingling::Grouped, ::serde::Serialize)] + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + /// Additional context info for this error. + pub info: #inner_type, + } + + impl #type_name { + /// Creates a new error with the given info. + /// The `name` field is automatically set to the snake_case of the struct name. + pub fn new(info: #inner_type) -> Self { + Self { + name: #snake_name.into(), + info, + } + } + } + + ::mingling::macros::register_type!(#type_name); + + #structural_data + }; + + expanded.into() + } + } +} diff --git a/mingling_macros/src/func/suggest.rs b/mingling_macros/src/func/suggest.rs new file mode 100644 index 0000000..0f2026f --- /dev/null +++ b/mingling_macros/src/func/suggest.rs @@ -0,0 +1,93 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Expr, LitStr, Token, parse_macro_input}; + +struct SuggestInput { + items: Punctuated, +} + +enum SuggestItem { + WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something" + Simple(LitStr), // "-I" +} + +impl Parse for SuggestInput { + fn parse(input: ParseStream) -> syn::Result { + let items = Punctuated::parse_terminated(input)?; + Ok(SuggestInput { items }) + } +} + +impl Parse for SuggestItem { + fn parse(input: ParseStream) -> syn::Result { + let key: LitStr = input.parse()?; + + if input.peek(Token![:]) { + let _colon: Token![:] = input.parse()?; + let value: Expr = input.parse()?; + Ok(SuggestItem::WithDesc(Box::new((key, value)))) + } else { + Ok(SuggestItem::Simple(key)) + } + } +} + +#[cfg(feature = "comp")] +pub(crate) fn suggest(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as SuggestInput); + + let mut items = Vec::new(); + + for item in input.items { + match item { + SuggestItem::WithDesc(boxed) => { + let (key, value) = *boxed; + items.push(quote! { + ::mingling::SuggestItem::new_with_desc(#key.to_string(), #value.to_string()) + }); + } + SuggestItem::Simple(key) => { + items.push(quote! { + ::mingling::SuggestItem::new(#key.to_string()) + }); + } + } + } + + let expanded = if items.is_empty() { + quote! { + ::mingling::Suggest::new() + } + } else { + quote! {{ + let mut suggest = ::mingling::Suggest::new(); + #(suggest.insert(#items);)* + suggest + }} + }; + + expanded.into() +} + +pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream { + let enum_type = parse_macro_input!(input as syn::Type); + + let expanded = quote! {{ + let mut enum_suggest = ::mingling::Suggest::new(); + for (name, desc) in <#enum_type>::enums() { + if desc.is_empty() { + enum_suggest.insert(::mingling::SuggestItem::new(name.to_string())); + } else { + enum_suggest.insert(::mingling::SuggestItem::new_with_desc( + name.to_string(), + desc.to_string(), + )); + } + } + enum_suggest + }}; + + expanded.into() +} diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/group_impl.rs deleted file mode 100644 index cac2734..0000000 --- a/mingling_macros/src/group_impl.rs +++ /dev/null @@ -1,147 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Ident, Result as SynResult, TypePath}; - -/// Input for the `group!` macro -/// -/// # Syntax -/// -/// ```rust,ignore -/// /// Only a type path — uses default `crate::ThisProgram` as program -/// group!(std::io::Error); -/// group!(ParseIntError); -/// -/// /// With an alias — creates a `pub type Alias = Path;` and uses `Alias` as variant name -/// group!(IoError = std::io::Error); -/// ``` -enum GroupInput { - /// `group!(TypePath)` — variant name is the last path segment - Plain(TypePath), - - /// `group!(Alias = TypePath)` — variant name is `Alias`, also generates `pub type Alias = TypePath;` - Aliased { alias: Ident, type_path: TypePath }, -} - -impl Parse for GroupInput { - fn parse(input: ParseStream) -> SynResult { - // Peek ahead: if the second token is `=`, parse as aliased form - let fork = input.fork(); - let _first: Ident = fork.parse()?; - if fork.peek(syn::Token![=]) { - // Consume the ident and `=` from the real input - let alias: Ident = input.parse()?; - let _eq: syn::Token![=] = input.parse()?; - let type_path: TypePath = input.parse()?; - Ok(GroupInput::Aliased { alias, type_path }) - } else { - let type_path: TypePath = input.parse()?; - Ok(GroupInput::Plain(type_path)) - } - } -} - -/// Convert a type path into a valid module name segment -/// -/// e.g. `std::io::Error` -> `internal_group_std_io_error` -fn module_name_from_type(type_path: &TypePath) -> Ident { - let segments: Vec = type_path - .path - .segments - .iter() - .map(|seg| seg.ident.to_string().to_lowercase()) - .collect(); - Ident::new( - &format!("internal_group_{}", segments.join("_")), - proc_macro2::Span::call_site(), - ) -} - -/// Get the last segment name of a type path (the simple type name) -/// -/// e.g. `std::io::Error` -> `Error` -fn type_simple_name(type_path: &TypePath) -> Ident { - type_path - .path - .segments - .last() - .expect("TypePath must have at least one segment") - .ident - .clone() -} - -/// Generate the `use` token for the type path inside the generated module. -/// -/// - Multi-segment path (e.g. `std::num::ParseIntError`): `use std::num::ParseIntError;` -/// - Single-segment path (e.g. `ParseIntError`): `use super::ParseIntError;` -fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream { - if type_path.path.segments.len() > 1 { - // Full path: use it directly - quote! { - #[allow(unused_imports)] - use #type_path; - } - } else { - // Single ident: import from parent scope - let ident = type_simple_name(type_path); - quote! { - #[allow(unused_imports)] - use super::#ident; - } - } -} - -pub fn group_macro(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as GroupInput); - - let is_aliased = matches!(input, GroupInput::Aliased { .. }); - - let (type_path, type_name, alias_stmt) = match input { - GroupInput::Plain(type_path) => { - let type_name = type_simple_name(&type_path); - (type_path, type_name, quote! {}) - } - GroupInput::Aliased { alias, type_path } => { - let type_name = alias.clone(); - let alias_stmt = quote! { - pub type #alias = #type_path; - }; - (type_path, type_name, alias_stmt) - } - }; - - let program_path = crate::default_program_path(); - - // Create a unique module name from the type path (use alias name for aliased form) - let module_name = module_name_from_type(&type_path); - // Generate the appropriate `use` statement for the type - let type_use = gen_type_use(&type_path); - - // For aliased form, also import the alias from parent scope - let alias_use = if is_aliased { - quote! { use super::#type_name; } - } else { - quote! {} - }; - - // Generate the module with the Grouped implementation - let expanded = quote! { - #alias_stmt - #[allow(non_camel_case_types)] - mod #module_name { - use #program_path as __MinglingProgram; - #type_use - #alias_use - - impl ::mingling::Grouped<__MinglingProgram> for #type_name { - fn member_id() -> __MinglingProgram { - __MinglingProgram::#type_name - } - } - - ::mingling::macros::register_type!(#type_name); - } - }; - - expanded.into() -} diff --git a/mingling_macros/src/grouped.rs b/mingling_macros/src/grouped.rs deleted file mode 100644 index 9014c37..0000000 --- a/mingling_macros/src/grouped.rs +++ /dev/null @@ -1,79 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, Ident, parse_macro_input}; - -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; - - let group_ident: proc_macro2::TokenStream = crate::default_program_path(); - - let any_output_convert_impls = - proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident)); - - // Generate the Grouped trait implementation - let expanded = quote! { - ::mingling::macros::register_type!(#struct_name); - - impl ::mingling::Grouped<#group_ident> for #struct_name { - fn member_id() -> #group_ident { - #group_ident::#struct_name - } - } - - #any_output_convert_impls - }; - - expanded.into() -} - -#[cfg(feature = "structural_renderer")] -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(); - - let group_ident: proc_macro2::TokenStream = crate::default_program_path(); - - let any_output_convert_impls = - proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident)); - - // Generate both Serialize and Grouped implementations - let expanded = quote! { - #[derive(serde::Serialize)] - #input_parsed - - ::mingling::macros::register_type!(#struct_name); - - impl ::mingling::Grouped<#group_ident> for #struct_name { - fn member_id() -> #group_ident { - #group_ident::#struct_name - } - } - - #any_output_convert_impls - }; - - expanded.into() -} - -fn build_any_output_convert_impls( - struct_name: &Ident, - group_ident: &proc_macro2::TokenStream, -) -> TokenStream { - quote! { - impl ::std::convert::Into<::mingling::AnyOutput<#group_ident>> for #struct_name { - fn into(self) -> ::mingling::AnyOutput<#group_ident> { - ::mingling::AnyOutput::new(self) - } - } - - impl ::std::convert::Into<::mingling::ChainProcess<#group_ident>> for #struct_name { - fn into(self) -> ::mingling::ChainProcess<#group_ident> { - ::mingling::AnyOutput::new(self).route_chain() - } - } - } - .into() -} diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs deleted file mode 100644 index 6e0d12e..0000000 --- a/mingling_macros/src/help.rs +++ /dev/null @@ -1,214 +0,0 @@ -use proc_macro::TokenStream; -use quote::{ToTokens, quote}; -use syn::spanned::Spanned; -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}; - -/// Extracts the user's return type, returning `None` for no return type. -fn extract_user_return_type(sig: &Signature) -> Option { - match &sig.output { - ReturnType::Type(_, ty) => Some(quote! { #ty }), - ReturnType::Default => None, - } -} - -pub 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, parameter name, and resource injection params - let (prev_param, 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 render_help body with resource injection - // Use modify_res for mutable resources same pattern as renderer.rs - - let wrapped_body = if mut_resources.is_empty() { - quote! { #(#fn_body_stmts)* } - } else { - let mut wrapped = quote! { #(#fn_body_stmts)* }; - 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)* - #wrapped_body - } - } else { - quote! { #(#fn_body_stmts)* } - }; - - // 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(#prev_param: 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 - #[allow(dead_code)] - #(#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 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::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::(entry_type_expr.to_token_stream()) { - Ok(ty) => ty, - Err(e) => return e.to_compile_error().into(), - }; - - let struct_name = match syn::parse2::(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/lib.rs b/mingling_macros/src/lib.rs index c48d14b..3743df8 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -141,44 +141,48 @@ //! } //! ``` -use proc_macro::TokenStream; +#[cfg(feature = "extra_macros")] use quote::quote; + +#[cfg(feature = "extra_macros")] +use syn::parse_macro_input; + +use proc_macro::TokenStream; use std::collections::BTreeSet; use std::sync::Mutex; use std::sync::OnceLock; -use syn::parse_macro_input; -mod chain; +mod attr; +mod derive; +mod func; +mod systems; + +mod extensions; +mod utils; + +// Bring all sub-modules into scope at the old paths so that existing +// references (e.g. `chain::chain_attr`, `renderer::renderer_attr`) +// continue to work without any `use`-path changes. #[cfg(feature = "comp")] -mod completion; -#[cfg(feature = "dispatch_tree")] -mod dispatch_tree_gen; -mod dispatcher; +use attr::completion; #[cfg(feature = "clap")] -mod dispatcher_clap; +use attr::dispatcher_clap; #[cfg(feature = "extra_macros")] -mod entry; -mod enum_tag; - -/// Extension point mechanism for attribute macros -/// (`#[chain(routeify, ...)]`, `#[renderer(routeify, ...)]`, etc.) -mod extensions; +use attr::program_setup; +use attr::{chain, help, renderer}; +use derive::{enum_tag, grouped}; #[cfg(feature = "extra_macros")] -mod group_impl; -mod grouped; -mod help; -mod node; -mod pack; +use func::entry; #[cfg(feature = "extra_macros")] -mod pack_err; +pub(crate) use func::group as group_impl; #[cfg(feature = "extra_macros")] -mod program_setup; -mod renderer; -mod res_injection; -#[cfg(feature = "structural_renderer")] -mod structural_data; +use func::pack_err; #[cfg(feature = "comp")] -mod suggest; +use func::suggest; +use func::{dispatcher, node, pack}; +use systems::res_injection; +#[cfg(feature = "structural_renderer")] +pub(crate) use systems::structural_data; pub(crate) fn default_program_path() -> proc_macro2::TokenStream { quote::quote! { crate::ThisProgram } @@ -266,44 +270,6 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { entry.contains(&format!(":: {variant_name} =>")) } -/// 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 `Grouped` implementation and registering -/// the type's simple name as an enum variant. -/// -/// # Syntax -/// -/// ```rust,ignore -/// group!(std::io::Error); -/// group!(ParseIntError); -/// ``` -/// -/// The type is registered under the default program (`crate::ThisProgram`). -/// -/// # How it works -/// -/// The macro generates a module containing: -/// - A `use` import for the program path and the outside-type -/// - An `impl Grouped` 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(Grouped)]` or `pack!`. -/// -/// # Example -/// -/// ```rust,ignore -/// use mingling::macros::group; -/// -/// // Register std::io::Error as a group member -/// group!(std::io::Error); -/// ``` -/// -/// After expansion, the type can be used in chains and renderers like any -/// `#[derive(Grouped)]` type. -/// -/// This macro is only available with the `extra_macros` feature. #[cfg(feature = "extra_macros")] #[proc_macro] pub fn group(input: TokenStream) -> TokenStream { @@ -1459,135 +1425,14 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { /// gen_program!(); /// ``` #[proc_macro] -pub fn gen_program(_input: TokenStream) -> TokenStream { - #[cfg(feature = "comp")] - let comp_gen = quote! { - ::mingling::macros::program_comp_gen!(); - }; - - #[cfg(not(feature = "comp"))] - let comp_gen = quote! {}; - - TokenStream::from(quote! { - /// Alias for the current program type `crate::ThisProgram` - pub type Next = ::mingling::ChainProcess; - - impl ::mingling::Routable for ::mingling::ChainProcess - { - fn to_chain(self) -> ::mingling::ChainProcess { - match self { - ::mingling::ChainProcess::Ok((any, _)) => { - ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) - } - other => other, - } - } - - fn to_render(self) -> ::mingling::ChainProcess { - 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!(); - }) +pub fn gen_program(input: TokenStream) -> TokenStream { + func::gen_program::gen_program_impl(input) } -/// Internal macro used by `gen_program!` to generate completion infrastructure. -/// -/// **This macro is only available with the `comp` feature.** -/// -/// This is an internal macro and should not be called directly by user code. -/// It generates a completion dispatcher, the `CompletionContext` type, and -/// the execution/render logic for shell completion. -/// -/// The generated module `__completion_gen` contains: -/// - A `__comp` dispatcher that routes completion requests -/// - A `__exec_completion` chain that processes `CompletionContext` into `CompletionSuggest` -/// - A `__render_completion` renderer that outputs completion suggestions -#[proc_macro] #[cfg(feature = "comp")] -pub fn program_comp_gen(_input: TokenStream) -> TokenStream { - #[cfg(feature = "async")] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub async fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Grouped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(not(feature = "async"))] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Grouped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(feature = "dispatch_tree")] - let internal_dispatcher_comp = quote! { - use __internal_completion_mod::__internal_dispatcher_comp; - }; - - #[cfg(not(feature = "dispatch_tree"))] - let internal_dispatcher_comp = quote! {}; - - let comp_dispatcher = quote! { - #[doc(hidden)] - mod __internal_completion_mod { - use ::mingling::Grouped; - ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); - ::mingling::macros::pack!( - CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) - ); - } - #internal_dispatcher_comp - use __internal_completion_mod::CompletionContext; - use __internal_completion_mod::CompletionSuggest; - pub use __internal_completion_mod::CMDCompletion; - - #fn_exec_comp - - ::mingling::macros::register_type!(CompletionContext); - - #[allow(unused)] - #[doc(hidden)] - #[::mingling::macros::renderer] - pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { - let result = ::mingling::RenderResult::default(); - let (ctx, suggest) = prev.inner; - ::mingling::CompletionHelper::render_suggest::(ctx, suggest); - result - } - }; - - TokenStream::from(comp_dispatcher) +#[proc_macro] +pub fn program_comp_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_comp_gen_impl(input) } /// Registers a type into the global packed types registry for inclusion in @@ -1612,108 +1457,22 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { /// Panics if the global `PACKED_TYPES` mutex is poisoned. #[proc_macro] pub fn register_type(input: TokenStream) -> TokenStream { - let type_ident = parse_macro_input!(input as syn::Ident); - let entry_str = type_ident.to_string(); - - get_global_set(&PACKED_TYPES) - .lock() - .unwrap() - .insert(entry_str); - - TokenStream::new() + func::gen_program::register_type_impl(input) } -/// Registers a chain mapping from a previous type to a chain struct. -/// -/// This macro is called internally by `#[chain]`(macro.chain.html) and is -/// generally not needed in user code. It inserts entries into the global -/// `CHAINS` and `CHAINS_EXIST` registries. -/// -/// # Syntax -/// -/// ```rust,ignore -/// register_chain!(PreviousType, ChainStruct); -/// ``` -/// -/// The `PreviousType` is the input type of the chain step, and `ChainStruct` -/// is the generated struct that implements the `Chain` trait. #[proc_macro] pub fn register_chain(input: TokenStream) -> TokenStream { - chain::register_chain(input) + func::gen_program::register_chain_impl(input) } -/// Registers a renderer mapping from a type to a renderer struct. -/// -/// This macro is called internally by `#[renderer]`(macro.renderer.html) and is -/// generally not needed in user code. It inserts entries into the global -/// `RENDERERS`, `RENDERERS_EXIST` and (with `structural_renderer` feature) -/// `STRUCTURAL_RENDERERS` registries. -/// -/// # Syntax -/// -/// ```rust,ignore -/// register_renderer!(PreviousType, RendererStruct); -/// ``` -/// -/// The `PreviousType` is the input type of the renderer, and `RendererStruct` -/// is the generated struct that implements the `Renderer` trait. #[proc_macro] pub fn register_renderer(input: TokenStream) -> TokenStream { - renderer::register_renderer(input) + func::gen_program::register_renderer_impl(input) } -/// Internal macro used by `gen_program!` to generate fallback types. -/// -/// This macro generates the fallback wrapper types that are essential -/// for error handling in the Mingling pipeline: -/// -/// - **`ErrorRendererNotFound`** — Wraps a `String` (the name of the missing renderer). -/// Used when no matching renderer is found for a given output type. -/// - **`ErrorDispatcherNotFound`** — Wraps `Vec` (the unrecognized command args). -/// Used when no matching dispatcher is found for user input. -/// - **`ResultEmpty`** — Wraps `()` (the unit type). -/// Used when the chain returns an empty result. -/// -/// Users can (and should) write `#[renderer]` functions for these types -/// to provide meaningful error messages. -/// -/// This macro is called automatically by `gen_program!` and should not -/// be called directly by user code. -/// -/// # Syntax -/// -/// ```rust,ignore -/// // Called internally by gen_program!: -/// program_fallback_gen!(); -/// ``` -/// -/// # Generated code equivalent -/// -/// ```rust,ignore -/// pack!(ErrorRendererNotFound = String); -/// pack!(ErrorDispatcherNotFound = Vec); -/// pack!(ResultEmpty = ()); -/// ``` #[proc_macro] -pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { - #[cfg(feature = "structural_renderer")] - let pack_empty = quote! { - #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] - pub struct ResultEmpty; - }; - - #[cfg(not(feature = "structural_renderer"))] - let pack_empty = quote! { - #[derive(::mingling::Grouped, Default)] - pub struct ResultEmpty; - }; - - let expanded = quote! { - ::mingling::macros::pack!(ErrorRendererNotFound = String); - ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec); - #pack_empty - }; - TokenStream::from(expanded) +pub fn program_fallback_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_fallback_gen_impl(input) } /// Internal macro used by `gen_program!` to generate the final program enum @@ -1765,434 +1524,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { /// pub fn new() -> Program { Program::new() } /// } /// ``` -/// -/// # Panics -/// -// Feature detection: baked into the proc-macro binary at compile time -#[cfg(feature = "async")] -const ASYNC_ENABLED: bool = true; -#[cfg(not(feature = "async"))] -const ASYNC_ENABLED: bool = false; - -/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. -fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { - let s = entry.to_string(); - let arrow_idx = s - .find("=>") - .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); - let struct_str = s[..arrow_idx].trim(); - let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); - let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); - let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); - (struct_ident, variant_ident) -} - -/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. -/// Always compiled; returns empty map when pathf feature is not enabled. -fn load_pathf_map() -> std::collections::HashMap { - if !cfg!(feature = "pathf") { - return std::collections::HashMap::new(); - } - let out_dir = std::env::var("OUT_DIR").ok(); - let crate_name = std::env::var("CARGO_PKG_NAME").ok(); - match (out_dir, crate_name) { - (Some(dir), Some(name)) => { - let path = std::path::Path::new(&dir).join(&name).join("type_using.rs"); - match std::fs::read_to_string(&path) { - Ok(content) => content - .lines() - .filter_map(|line| { - let line = line.trim(); - if let Some(rest) = line.strip_prefix("use ") { - let path = rest.strip_suffix(';').unwrap_or(rest); - if let Some((_mod, type_name)) = path.rsplit_once("::") { - return Some((type_name.to_string(), path.to_string())); - } - } - None - }) - .collect(), - Err(_) => std::collections::HashMap::new(), - } - } - _ => std::collections::HashMap::new(), - } -} - -/// Resolves a type name to its full path token stream using the pathf mapping. -pub(crate) fn resolve_type( - name: &str, - map: &std::collections::HashMap, -) -> proc_macro2::TokenStream { - if let Some(full_path) = map.get(name) { - syn::parse_str::(full_path).unwrap_or_else(|_| { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - }) - } else { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - } -} - -/// Panics if any of the global registries (`PACKED_TYPES`, `RENDERERS`, `CHAINS`, etc.) -/// are poisoned. #[proc_macro] -#[allow(clippy::too_many_lines)] -pub fn program_final_gen(_input: TokenStream) -> TokenStream { - let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); - - let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); - - let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); - let chains = get_global_set(&CHAINS).lock().unwrap().clone(); - let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); - let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); - - #[cfg(feature = "structural_renderer")] - let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clone(); - - #[cfg(feature = "comp")] - let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); - - let packed_types: Vec = packed_types - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - let renderer_tokens: Vec = renderers - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - let chain_tokens: Vec = chains - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - let renderer_exist_tokens: Vec = renderer_exist - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - let chain_exist_tokens: Vec = chain_exist - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - let pathf_map: std::collections::HashMap = if cfg!(feature = "pathf") { - load_pathf_map() - } else { - std::collections::HashMap::new() - }; - - let pathf_uses: Vec = if cfg!(feature = "pathf") { - pathf_map - .values() - .map(|path| format!("use {};", path).parse().unwrap_or_default()) - .collect() - } else { - Vec::new() - }; - - #[cfg(feature = "structural_renderer")] - let structural_renderer_tokens: Vec = structural_renderers - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - #[cfg(feature = "structural_renderer")] - let structural_render = quote! { - fn structural_render( - any: ::mingling::AnyOutput, - setting: &::mingling::StructuralRendererSetting, - ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#structural_renderer_tokens)* - _ => { - // Non-structural types: render ResultEmpty (which implements - // StructuralData + Serialize) instead of producing nothing. - let mut r = ::mingling::RenderResult::default(); - ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; - Ok(r) - } - } - } - }; - - #[cfg(not(feature = "structural_renderer"))] - let structural_render = quote! {}; - - #[cfg(feature = "dispatch_tree")] - let compile_time_dispatchers: Vec = get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clone() - .iter() - .cloned() - .collect(); - - #[cfg(feature = "dispatch_tree")] - let dispatch_tree_nodes = { - let entries: Vec<(String, String, String)> = compile_time_dispatchers - .iter() - .filter_map(|entry| { - let parts: Vec<&str> = entry.split(':').collect(); - if parts.len() == 3 { - Some(( - parts[0].to_string(), - parts[1].to_string(), - parts[2].to_string(), - )) - } else { - None - } - }) - .collect(); - - let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); - let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); - - quote! { - #get_nodes_fn - #dispatch_trie_fn - } - }; - - #[cfg(not(feature = "dispatch_tree"))] - let dispatch_tree_nodes = quote! {}; - - #[cfg(feature = "comp")] - let completion_tokens: Vec = completions - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - #[cfg(feature = "comp")] - let comp = quote! { - fn do_comp(any: &::mingling::AnyOutput, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#completion_tokens)* - _ => ::mingling::Suggest::FileCompletion, - } - } - }; - - #[cfg(not(feature = "comp"))] - let comp = quote! {}; - - // Build render function arms from stored entries - let render_fn = - if renderer_tokens.is_empty() { - quote! { - fn render(_any: ::mingling::AnyOutput) -> ::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); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Renderer>::render(value) - } - } - }).collect(); - quote! { - fn render(any: ::mingling::AnyOutput) -> ::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| { - 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); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - let fut = async { <#resolved_struct as ::mingling::Chain>::proc(value).await }; - ::std::boxed::Box::pin(fut) - } - } - }).collect(); - - let chain_arms_sync: Vec<_> = chain_tokens - .iter() - .map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Chain>::proc(value) - } - } - }) - .collect(); - - let do_chain_fn = if chain_tokens.is_empty() { - quote! { - fn do_chain(_any: ::mingling::AnyOutput) -> ::mingling::ChainProcess { - ::core::panic!("No chain found for type id") - } - } - } else if ASYNC_ENABLED { - quote! { - fn do_chain( - any: ::mingling::AnyOutput, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::std::marker::Send>> { - match any.member_id { - #(#chain_arms_async)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), - } - } - } - } else { - quote! { - fn do_chain( - any: ::mingling::AnyOutput, - ) -> ::mingling::ChainProcess { - match any.member_id { - #(#chain_arms_sync)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), - } - } - } - }; - - let help_tokens: Vec = get_global_set(&HELP_REQUESTS) - .lock() - .unwrap() - .clone() - .iter() - .map(|s| syn::parse_str::(s).unwrap()) - .collect(); - - let num_variants = packed_types.len(); - let repr_type = if u8::try_from(num_variants).is_ok() { - quote! { u8 } - } else if u16::try_from(num_variants).is_ok() { - quote! { u16 } - } else if u32::try_from(num_variants).is_ok() { - quote! { u32 } - } else { - quote! { u128 } - }; - - let expanded = quote! { - #[derive(Debug, PartialEq, Eq, Clone)] - #[repr(#repr_type)] - #[allow(nonstandard_style)] - pub enum #name { - #(#packed_types),* - } - - impl ::std::fmt::Display for #name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match self { - #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* - } - } - } - - impl ::mingling::ProgramCollect for #name { - type Enum = #name; - type ErrorDispatcherNotFound = ErrorDispatcherNotFound; - type ErrorRendererNotFound = ErrorRendererNotFound; - type ResultEmpty = ResultEmpty; - fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput { - ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) - } - fn build_dispatcher_not_found(args: Vec) -> ::mingling::AnyOutput { - ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) - } - fn build_empty_result() -> ::mingling::AnyOutput { - ::mingling::AnyOutput::new(ResultEmpty) - } - #render_fn - #do_chain_fn - fn render_help(any: ::mingling::AnyOutput) -> ::mingling::RenderResult { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#help_tokens)* - _ => ::mingling::RenderResult::default(), - } - } - fn has_renderer(any: &::mingling::AnyOutput) -> bool { - match any.member_id { - #(#renderer_exist_tokens)* - _ => false - } - } - fn has_chain(any: &::mingling::AnyOutput) -> bool { - match any.member_id { - #(#chain_exist_tokens)* - _ => false - } - } - #dispatch_tree_nodes - #structural_render - #comp - } - - impl #name { - /// Creates a new `Program<#name>` instance with default configuration. - pub fn new() -> ::mingling::Program<#name> { - ::mingling::Program::new() - } - - /// Returns a static reference to the global `Program<#name>` singleton. - pub fn this() -> &'static ::mingling::Program<#name> { - &::mingling::this::<#name>() - } - } - }; - - // Clear all global registries to prevent stale state in Rust Analyzer - get_global_set(&PACKED_TYPES).lock().unwrap().clear(); - get_global_set(&CHAINS).lock().unwrap().clear(); - get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); - get_global_set(&RENDERERS).lock().unwrap().clear(); - get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); - get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); - #[cfg(feature = "comp")] - get_global_set(&COMPLETIONS).lock().unwrap().clear(); - #[cfg(feature = "dispatch_tree")] - get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clear(); - #[cfg(feature = "structural_renderer")] - get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clear(); - - TokenStream::from(expanded) +pub fn program_final_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_final_gen_impl(input) } /// Builds a `Suggest` instance with inline suggestion items. diff --git a/mingling_macros/src/node.rs b/mingling_macros/src/node.rs deleted file mode 100644 index b3a61c6..0000000 --- a/mingling_macros/src/node.rs +++ /dev/null @@ -1,59 +0,0 @@ -use just_fmt::kebab_case; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{LitStr, Result as SynResult}; - -/// Parses a string literal input for the node macro -struct NodeInput { - path: LitStr, -} - -impl Parse for NodeInput { - fn parse(input: ParseStream) -> SynResult { - Ok(NodeInput { - path: input.parse()?, - }) - } -} - -pub fn node(input: TokenStream) -> TokenStream { - // Parse the input as a string literal - let input_parsed = syn::parse_macro_input!(input as NodeInput); - let path_str = input_parsed.path.value(); - - // If the input string is empty, return an empty Node - if path_str.is_empty() { - return quote! { - mingling::Node::default() - } - .into(); - } - - // Split the path by dots - let parts: Vec = path_str - .split('.') - .map(|s| { - if s.starts_with('_') { - s.to_string() - } else { - kebab_case!(s).to_string() - } - }) - .collect(); - - // Build the expression starting from Node::default() - let mut expr: TokenStream2 = quote! { - mingling::Node::default() - }; - - // Add .join() calls for each part of the path - for part in parts { - expr = quote! { - #expr.join(#part) - }; - } - - expr.into() -} diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/pack.rs deleted file mode 100644 index 0af1b4c..0000000 --- a/mingling_macros/src/pack.rs +++ /dev/null @@ -1,149 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, Result as SynResult, Token, Type}; - -struct PackInput { - attrs: Vec, - type_name: Ident, - inner_type: Type, -} - -impl Parse for PackInput { - fn parse(input: ParseStream) -> SynResult { - let attrs = input.call(Attribute::parse_outer)?; - let type_name: Ident = input.parse()?; - input.parse::()?; - let inner_type: Type = input.parse()?; - - Ok(PackInput { - attrs, - type_name, - inner_type, - }) - } -} - -#[allow(clippy::too_many_lines)] -pub fn pack(input: TokenStream) -> TokenStream { - let pack_input = syn::parse_macro_input!(input as PackInput); - - let group_name = crate::default_program_path(); - let type_name = pack_input.type_name; - let inner_type = pack_input.inner_type; - let attrs = pack_input.attrs; - - // Generate the struct definition - // Note: No longer derives Serialize under structural_renderer. - // Use pack_structual! for structured output support. - let struct_def = quote! { - #(#attrs)* - pub struct #type_name { - pub(crate) inner: #inner_type, - } - }; - - // Generate the new() method - let new_impl = quote! { - impl #type_name { - /// Creates a new instance of the wrapper type - pub fn new(inner: #inner_type) -> Self { - Self { inner } - } - } - }; - - // Generate From and Into implementations - let from_into_impl = quote! { - impl From<#inner_type> for #type_name { - fn from(inner: #inner_type) -> Self { - Self::new(inner) - } - } - - impl From<#type_name> for #inner_type { - fn from(wrapper: #type_name) -> #inner_type { - wrapper.inner - } - } - }; - - // Generate AsRef and AsMut implementations - let as_ref_impl = quote! { - impl ::std::convert::AsRef<#inner_type> for #type_name { - fn as_ref(&self) -> &#inner_type { - &self.inner - } - } - - impl ::std::convert::AsMut<#inner_type> for #type_name { - fn as_mut(&mut self) -> &mut #inner_type { - &mut self.inner - } - } - }; - - // Generate Deref and DerefMut implementations - let deref_impl = quote! { - impl ::std::ops::Deref for #type_name { - type Target = #inner_type; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl ::std::ops::DerefMut for #type_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - }; - - // Check if the inner type implements Default by generating conditional code - let default_impl = quote! { - impl ::std::default::Default for #type_name - where - #inner_type: ::std::default::Default, - { - fn default() -> Self { - Self::new(::std::default::Default::default()) - } - } - }; - - let register_impl = quote! { - ::mingling::macros::register_type!(#type_name); - }; - - let expanded = quote! { - #struct_def - - #new_impl - #from_into_impl - #as_ref_impl - #deref_impl - #default_impl - #register_impl - - impl Into> for #type_name { - fn into(self) -> mingling::AnyOutput<#group_name> { - mingling::AnyOutput::new(self) - } - } - - impl Into> for #type_name { - fn into(self) -> mingling::ChainProcess<#group_name> { - mingling::AnyOutput::new(self).route_chain() - } - } - - impl ::mingling::Grouped<#group_name> for #type_name { - fn member_id() -> #group_name { - #group_name::#type_name - } - } - }; - - expanded.into() -} diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/pack_err.rs deleted file mode 100644 index a747aa9..0000000 --- a/mingling_macros/src/pack_err.rs +++ /dev/null @@ -1,209 +0,0 @@ -use just_fmt::snake_case; -use proc_macro::TokenStream; -use quote::quote; -use syn::{Ident, Token, Type, parse_macro_input}; - -enum PackErrInput { - /// pack_err!(ErrorNotFound) - Simple { type_name: Ident }, - /// pack_err!(ErrorNotDir = PathBuf) - Typed { - type_name: Ident, - inner_type: Box, - }, -} - -impl syn::parse::Parse for PackErrInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let type_name: Ident = input.parse()?; - - if input.peek(Token![=]) { - input.parse::()?; - let inner_type: Type = input.parse()?; - Ok(PackErrInput::Typed { - type_name, - inner_type: Box::new(inner_type), - }) - } else { - Ok(PackErrInput::Simple { type_name }) - } - } -} - -#[allow(clippy::too_many_lines)] -pub fn pack_err(input: TokenStream) -> TokenStream { - let parsed = parse_macro_input!(input as PackErrInput); - - match parsed { - PackErrInput::Simple { type_name } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - // Note: No longer derives Serialize under structural_renderer. - // Use pack_err_structural for structured output support. - let derive = quote! { - #[derive(::mingling::Grouped)] - }; - - let expanded = quote! { - #derive - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - } - - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - name: #snake_name.into(), - } - } - } - - ::mingling::macros::register_type!(#type_name); - }; - - expanded.into() - } - PackErrInput::Typed { - type_name, - inner_type, - } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - // Note: No longer derives Serialize under structural_renderer. - // Use pack_err_structural for structured output support. - let derive = quote! { - #[derive(::mingling::Grouped)] - }; - - let expanded = quote! { - #derive - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - /// Additional context info for this error. - pub info: #inner_type, - } - - impl #type_name { - /// Creates a new error with the given info. - /// The `name` field is automatically set to the snake_case of the struct name. - pub fn new(info: #inner_type) -> Self { - Self { - name: #snake_name.into(), - info, - } - } - } - - ::mingling::macros::register_type!(#type_name); - }; - - expanded.into() - } - } -} - -/// `pack_err_structural!` — like `pack_err!` but also marks the type as -/// supporting structured output via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// pack_err_structural!(ErrorNotFound); -/// pack_err_structural!(ErrorNotDir = PathBuf); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// pack_err!(ErrorNotFound); -/// impl ::mingling::__private::StructuralDataSealed for ErrorNotFound {} -/// impl ::mingling::__private::StructuralData for ErrorNotFound {} -/// ``` -#[cfg(feature = "structural_renderer")] -pub fn pack_err_structural(input: TokenStream) -> TokenStream { - let parsed = parse_macro_input!(input as PackErrInput); - - let type_name = match &parsed { - PackErrInput::Simple { type_name } => type_name.clone(), - PackErrInput::Typed { type_name, .. } => type_name.clone(), - }; - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - crate::get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - let structural_data = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} - }; - - // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed) - match parsed { - PackErrInput::Simple { type_name } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - let expanded = quote! { - #[derive(::mingling::Grouped, ::serde::Serialize)] - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - } - - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - name: #snake_name.into(), - } - } - } - - ::mingling::macros::register_type!(#type_name); - - #structural_data - }; - - expanded.into() - } - PackErrInput::Typed { - type_name, - inner_type, - } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - let expanded = quote! { - #[derive(::mingling::Grouped, ::serde::Serialize)] - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - /// Additional context info for this error. - pub info: #inner_type, - } - - impl #type_name { - /// Creates a new error with the given info. - /// The `name` field is automatically set to the snake_case of the struct name. - pub fn new(info: #inner_type) -> Self { - Self { - name: #snake_name.into(), - info, - } - } - } - - ::mingling::macros::register_type!(#type_name); - - #structural_data - }; - - expanded.into() - } - } -} diff --git a/mingling_macros/src/program_setup.rs b/mingling_macros/src/program_setup.rs deleted file mode 100644 index 7fd9d16..0000000 --- a/mingling_macros/src/program_setup.rs +++ /dev/null @@ -1,116 +0,0 @@ -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 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/renderer.rs b/mingling_macros/src/renderer.rs deleted file mode 100644 index d124ec9..0000000 --- a/mingling_macros/src/renderer.rs +++ /dev/null @@ -1,251 +0,0 @@ -use proc_macro::TokenStream; -use quote::{ToTokens, quote}; -use syn::spanned::Spanned; -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}; - -/// Extracts the user's return type, returning `None` for no return type. -fn extract_user_return_type(sig: &Signature) -> Option { - match &sig.output { - ReturnType::Type(_, ty) => Some(quote! { #ty }), - ReturnType::Default => None, - } -} - -#[allow(clippy::too_many_lines)] -pub 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, parameter name, and resource injection params - let (prev_param, 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 = 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(); - - let inner_body_with_resources = if has_mut_resources { - let mut wrapped = quote! { #(#fn_body_stmts)* }; - 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 { - quote! { #(#fn_body_stmts)* } - }; - - // Build the Renderer::render body with resource injection - // The user's body now directly creates and returns a RenderResult. - let render_fn_body = if has_resources { - quote! { - #(#immut_resource_stmts)* - #inner_body_with_resources - } - } else { - quote! { #inner_body_with_resources } - }; - - // 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(#prev_param: Self::Previous) -> ::mingling::RenderResult { - 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)* - } - }; - - expanded.into() -} - -/// Builds the renderer entry for the global renderers list -pub 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 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 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 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::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::(previous_type_expr.to_token_stream()) { - Ok(ty) => ty, - Err(e) => return e.to_compile_error().into(), - }; - - let struct_name = match syn::parse2::(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() -} diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/res_injection.rs deleted file mode 100644 index 606b9a6..0000000 --- a/mingling_macros/src/res_injection.rs +++ /dev/null @@ -1,261 +0,0 @@ -use quote::quote; -use syn::spanned::Spanned; -use syn::{FnArg, Ident, Pat, PatType, Signature, Type, TypePath}; - -/// Extracted information about a resource injection parameter -pub(crate) struct ResourceInjection { - pub(crate) var_name: Ident, - pub(crate) full_type: Type, - pub(crate) inner_type: TypePath, - pub(crate) is_ref: bool, - pub(crate) is_mut: bool, -} - -/// Extracts the previous type and parameter name from function arguments, -/// and collects resource injection parameters from the 2nd argument onward. -#[allow(clippy::too_many_lines)] -pub(crate) fn extract_args_info( - sig: &Signature, -) -> syn::Result<(Pat, TypePath, Vec)> { - if sig.inputs.is_empty() { - return Err(syn::Error::new( - sig.span(), - "Function must have at least one parameter", - )); - } - - // First parameter: required, the previous type (must be owned, not a reference) - let first_arg = &sig.inputs[0]; - let (prev_param, previous_type) = match first_arg { - FnArg::Typed(PatType { pat, ty, .. }) => { - let param_pat = (**pat).clone(); - match &**ty { - Type::Path(type_path) => (param_pat, type_path.clone()), - Type::Reference(_) => { - return Err(syn::Error::new( - ty.span(), - "The first parameter (previous type) must be taken by move, \ - not by reference. \ - Use `prev: SomeEntry` instead of `prev: &SomeEntry`.", - )); - } - _ => { - return Err(syn::Error::new( - ty.span(), - "First parameter type must be a type path", - )); - } - } - } - FnArg::Receiver(_) => { - return Err(syn::Error::new( - first_arg.span(), - "Function cannot have self parameter", - )); - } - }; - - // 2nd to Nth parameters: optional, for resource injection - let mut resources = Vec::new(); - for arg in sig.inputs.iter().skip(1) { - match arg { - FnArg::Typed(PatType { pat, ty, .. }) => { - // Extract the variable name – must be a simple identifier - 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 (e.g., `age: &Age`)", - )); - } - }; - - let full_type = *(*ty).clone(); - - // Try to extract inner type for reference patterns like `&Age` -> `Age` - // and `&mut Age` -> `Age` - 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 (e.g., `age: &Age`)", - )); - } - }, - Type::Path(_) => { - return Err(syn::Error::new( - ty.span(), - "Resource injection parameter must be a reference (`&T` or `&mut T`), \ - not an owned value. Use `age: &Age` instead of `age: Age`.", - )); - } - _ => { - return Err(syn::Error::new( - ty.span(), - "Resource injection type must be a type path or reference to one \ - (e.g., `age: Age` or `age: &Age`)", - )); - } - }; - - 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((prev_param, previous_type, resources)) -} - -/// Generates `let` binding statements for immutable resource injection parameters. -/// -/// Each immutable reference parameter gets a `_binding` variable that holds the -/// `res_or_default` result, then a shadowing `let` that borrows from it via `.as_ref()`. -pub(crate) fn generate_immut_resource_bindings<'a>( - resources: impl Iterator, - program_type: &proc_macro2::TokenStream, -) -> Vec { - resources - .filter(|r| !r.is_mut) - .map(|res| { - let var_binding_name = syn::Ident::new( - &format!("__{}_binding", &res.var_name.to_string()), - res.var_name.span(), - ); - let var_name = &res.var_name; - let full_type = &res.full_type; - let inner_type = &res.inner_type; - if res.is_ref { - quote! { - let #var_binding_name = ::mingling::this::<#program_type>() - .res_or_default::<#inner_type>(); - let #var_name: #full_type = #var_binding_name.as_ref(); - } - } else { - quote! { - let #var_name: #full_type = ::mingling::this::<#program_type>() - .res_or_default::<#full_type>(); - } - } - }) - .collect() -} - -/// 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()) -} - -/// 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`). -/// -/// The innermost closure gets the original body, and each mutable parameter wraps -/// outward from last to first. -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)* - }; - - for res in mut_resources { - let var_name = &res.var_name; - let inner_type = &res.inner_type; - 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 -} - -/// Generates code for mutable resource injection in async `#[chain]` functions. -/// -/// Instead of wrapping in a closure (which causes lifetime issues with async), -/// this generates a three-part pattern: -/// -/// 1. **Extract** each mutable resource from the global store into an owned local. -/// 2. **Body block** — the user's body with `&mut` references scoped to a block. -/// 3. **Store back** each modified resource after the body block completes. -/// -/// This avoids holding a `&mut` reference across `.await` points — the borrow -/// ends when the body block closes. -pub(crate) fn wrap_body_with_mut_resources_async( - fn_body_stmts: &[syn::Stmt], - mut_resources: &[&ResourceInjection], - program_type: &proc_macro2::TokenStream, -) -> proc_macro2::TokenStream { - // 1. Generate extract statements and body-block `let` bindings - let mut extract_stmts = Vec::new(); - let mut body_lets = Vec::new(); - - for res in mut_resources { - let var_name = &res.var_name; - let inner_type = &res.inner_type; - let binding_name = mut_res_binding_name(var_name); - - extract_stmts.push(quote! { - let mut #binding_name = ::mingling::this::<#program_type>() - .__extract_res_mut::<#inner_type>(); - }); - - body_lets.push(quote! { - let #var_name: &mut #inner_type = &mut #binding_name; - }); - } - - // 2. Store-back statements (reverse order is fine, resources are independent) - let mut store_stmts = Vec::new(); - for res in mut_resources { - let var_name = &res.var_name; - let inner_type = &res.inner_type; - let binding_name = mut_res_binding_name(var_name); - - store_stmts.push(quote! { - ::mingling::this::<#program_type>() - .__store_res::<#inner_type>(#binding_name); - }); - } - - quote! { - #(#extract_stmts)* - let __chain_result = { - #(#body_lets)* - #(#fn_body_stmts)* - }; - #(#store_stmts)* - __chain_result - } -} diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/structural_data.rs deleted file mode 100644 index 74bcf09..0000000 --- a/mingling_macros/src/structural_data.rs +++ /dev/null @@ -1,336 +0,0 @@ -#![allow(dead_code)] - -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, Ident, TypePath, parse_macro_input}; - -use crate::get_global_set; - -/// Derive macro for `StructuralData`. -/// -/// This marks a type as eligible for structured output (JSON / YAML / TOML / RON). -/// The type must also implement `serde::Serialize` — the generated `impl StructuralData` -/// will fail to compile if `Serialize` is not in scope or implemented. -/// -/// Also registers the type name in the global `STRUCTURED_TYPES` registry so that -/// the `structural_render` match arm is generated by `gen_program!()`. -pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let type_name = input.ident; - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - // Generate BOTH the sealed impl AND the StructuralData impl. - // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]), - // so the only way to get StructuralData is through this derive macro. - let expanded = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} - }; - - expanded.into() -} - -/// `pack_structural!` — like `pack!` but also marks the type as supporting -/// structured output via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// pack_structural!(Info = (String, i32)); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// pack!(Info = (String, i32)); -/// impl ::mingling::StructuralData for Info {} -/// ``` -pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { - // Parse same input format as `pack!` - let input_parsed = syn::parse_macro_input!(input as PackStructuralInput); - let type_name = input_parsed.type_name; - let inner_type = input_parsed.inner_type; - let attrs = input_parsed.attrs; - let program_path = crate::default_program_path(); - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - // Struct definition (with Serialize derive, same as pack! under structural_renderer) - #[cfg(not(feature = "structural_renderer"))] - let struct_def = quote! { - #(#attrs)* - pub struct #type_name { - pub inner: #inner_type, - } - }; - - #[cfg(feature = "structural_renderer")] - let struct_def = quote! { - #(#attrs)* - #[derive(serde::Serialize)] - pub struct #type_name { - pub inner: #inner_type, - } - }; - - // Helper impls (same as pack!) - let new_impl = quote! { - impl #type_name { - pub fn new(inner: #inner_type) -> Self { - Self { inner } - } - } - }; - - let from_into_impl = quote! { - impl From<#inner_type> for #type_name { - fn from(inner: #inner_type) -> Self { - Self::new(inner) - } - } - impl From<#type_name> for #inner_type { - fn from(wrapper: #type_name) -> #inner_type { - wrapper.inner - } - } - }; - - let as_ref_impl = quote! { - impl ::std::convert::AsRef<#inner_type> for #type_name { - fn as_ref(&self) -> &#inner_type { - &self.inner - } - } - impl ::std::convert::AsMut<#inner_type> for #type_name { - fn as_mut(&mut self) -> &mut #inner_type { - &mut self.inner - } - } - }; - - let deref_impl = quote! { - impl ::std::ops::Deref for #type_name { - type Target = #inner_type; - fn deref(&self) -> &Self::Target { - &self.inner - } - } - impl ::std::ops::DerefMut for #type_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - }; - - let default_impl = quote! { - impl ::std::default::Default for #type_name - where - #inner_type: ::std::default::Default, - { - fn default() -> Self { - Self::new(::std::default::Default::default()) - } - } - }; - - let register_impl = quote! { - ::mingling::macros::register_type!(#type_name); - }; - - // StructuralData impl + sealed + registration - let structural_impl = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} - }; - - let expanded = quote! { - #struct_def - - #new_impl - #from_into_impl - #as_ref_impl - #deref_impl - #default_impl - #register_impl - #structural_impl - - impl Into<::mingling::AnyOutput<#program_path>> for #type_name { - fn into(self) -> ::mingling::AnyOutput<#program_path> { - ::mingling::AnyOutput::new(self) - } - } - - impl Into<::mingling::ChainProcess<#program_path>> for #type_name { - fn into(self) -> ::mingling::ChainProcess<#program_path> { - ::mingling::AnyOutput::new(self).route_chain() - } - } - - impl ::mingling::Grouped<#program_path> for #type_name { - fn member_id() -> #program_path { - #program_path::#type_name - } - } - }; - - expanded.into() -} - -/// Input for `pack_structural!` — same format as `pack!`. -struct PackStructuralInput { - attrs: Vec, - type_name: Ident, - inner_type: syn::Type, -} - -impl syn::parse::Parse for PackStructuralInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let attrs = input.call(syn::Attribute::parse_outer)?; - let type_name: Ident = input.parse()?; - input.parse::()?; - let inner_type: syn::Type = input.parse()?; - Ok(PackStructuralInput { - attrs, - type_name, - inner_type, - }) - } -} - -/// `group_structural!` — like `group!` but also marks the type as supporting -/// structured output via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// group_structural!(Info = (String, i32)); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// group!(Info = (String, i32)); -/// 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); - - let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. }); - - let (type_path, type_name, alias_stmt) = match &input_parsed { - GroupStructuralInput::Plain(type_path) => { - let name = type_path - .path - .segments - .last() - .expect("TypePath must have at least one segment") - .ident - .clone(); - (type_path.clone(), name, quote! {}) - } - GroupStructuralInput::Aliased { alias, type_path } => { - let alias_stmt = quote! { - pub(crate) type #alias = #type_path; - }; - (type_path.clone(), alias.clone(), alias_stmt) - } - }; - - let type_name_str = type_name.to_string(); - - // Register in STRUCTURED_TYPES - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - let program_path = crate::default_program_path(); - - // Generate unique module name - let segments: Vec = type_path - .path - .segments - .iter() - .map(|seg| seg.ident.to_string().to_lowercase()) - .collect(); - let module_name = Ident::new( - &format!("internal_group_{}", segments.join("_")), - proc_macro2::Span::call_site(), - ); - - // 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_path - .path - .segments - .last() - .expect("TypePath must have at least one segment") - .ident - .clone(); - quote! { #[allow(unused_imports)] use super::#ident; } - }; - - let alias_use = if is_aliased { - quote! { use super::#type_name; } - } else { - quote! {} - }; - - let expanded = quote! { - #alias_stmt - #[allow(non_camel_case_types)] - mod #module_name { - use #program_path as __MinglingProgram; - #type_use - #alias_use - - impl ::mingling::Grouped<__MinglingProgram> for #type_name { - fn member_id() -> __MinglingProgram { - __MinglingProgram::#type_name - } - } - - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} - - ::mingling::macros::register_type!(#type_name); - } - }; - - expanded.into() -} - -/// Input for `group_structural!` — same format as `group!`. -enum GroupStructuralInput { - Plain(TypePath), - Aliased { alias: Ident, type_path: TypePath }, -} - -impl syn::parse::Parse for GroupStructuralInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let fork = input.fork(); - let _first: Ident = fork.parse()?; - if fork.peek(syn::Token![=]) { - let alias: Ident = input.parse()?; - let _eq: syn::Token![=] = input.parse()?; - let type_path: TypePath = input.parse()?; - Ok(GroupStructuralInput::Aliased { alias, type_path }) - } else { - let type_path: TypePath = input.parse()?; - Ok(GroupStructuralInput::Plain(type_path)) - } - } -} diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/suggest.rs deleted file mode 100644 index d3ab446..0000000 --- a/mingling_macros/src/suggest.rs +++ /dev/null @@ -1,93 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::{Expr, LitStr, Token, parse_macro_input}; - -struct SuggestInput { - items: Punctuated, -} - -enum SuggestItem { - WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something" - Simple(LitStr), // "-I" -} - -impl Parse for SuggestInput { - fn parse(input: ParseStream) -> syn::Result { - let items = Punctuated::parse_terminated(input)?; - Ok(SuggestInput { items }) - } -} - -impl Parse for SuggestItem { - fn parse(input: ParseStream) -> syn::Result { - let key: LitStr = input.parse()?; - - if input.peek(Token![:]) { - let _colon: Token![:] = input.parse()?; - let value: Expr = input.parse()?; - Ok(SuggestItem::WithDesc(Box::new((key, value)))) - } else { - Ok(SuggestItem::Simple(key)) - } - } -} - -#[cfg(feature = "comp")] -pub fn suggest(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as SuggestInput); - - let mut items = Vec::new(); - - for item in input.items { - match item { - SuggestItem::WithDesc(boxed) => { - let (key, value) = *boxed; - items.push(quote! { - ::mingling::SuggestItem::new_with_desc(#key.to_string(), #value.to_string()) - }); - } - SuggestItem::Simple(key) => { - items.push(quote! { - ::mingling::SuggestItem::new(#key.to_string()) - }); - } - } - } - - let expanded = if items.is_empty() { - quote! { - ::mingling::Suggest::new() - } - } else { - quote! {{ - let mut suggest = ::mingling::Suggest::new(); - #(suggest.insert(#items);)* - suggest - }} - }; - - expanded.into() -} - -pub fn suggest_enum(input: TokenStream) -> TokenStream { - let enum_type = parse_macro_input!(input as syn::Type); - - let expanded = quote! {{ - let mut enum_suggest = ::mingling::Suggest::new(); - for (name, desc) in <#enum_type>::enums() { - if desc.is_empty() { - enum_suggest.insert(::mingling::SuggestItem::new(name.to_string())); - } else { - enum_suggest.insert(::mingling::SuggestItem::new_with_desc( - name.to_string(), - desc.to_string(), - )); - } - } - enum_suggest - }}; - - expanded.into() -} diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs new file mode 100644 index 0000000..53e58c5 --- /dev/null +++ b/mingling_macros/src/systems.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "dispatch_tree")] +pub(crate) mod dispatch_tree_gen; +pub(crate) mod res_injection; +#[cfg(feature = "structural_renderer")] +pub(crate) mod structural_data; diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs new file mode 100644 index 0000000..7383421 --- /dev/null +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -0,0 +1,181 @@ +use std::collections::{BTreeMap, HashMap}; + +use just_fmt::snake_case; +use proc_macro2::TokenStream; +use quote::quote; + +use crate::func::gen_program::resolve_type; + +/// Generate the `get_nodes()` function body for a ProgramCollect impl. +/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths. +pub(crate) fn gen_get_nodes( + entries: &[(String, String, String)], + pathf_map: &HashMap, +) -> TokenStream { + let mut node_entries = Vec::new(); + + for (node_name, _disp_type, _entry_name) in entries { + let static_name_str = format!("__internal_dispatcher_{}", snake_case!(node_name)); + let resolved = resolve_type(&static_name_str, pathf_map); + let node_display_name = node_name.replace('.', " "); + let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site()); + + node_entries.push(quote! { + (#node_display_lit.to_string(), & #resolved) + }); + } + + quote! { + fn get_nodes() -> Vec<(String, &'static (dyn ::mingling::Dispatcher + Send + Sync))> { + vec![ + #(#node_entries),* + ] + } + } +} + +/// Generate the `dispatch_args_trie()` function body for a ProgramCollect impl. +/// +/// Builds a hardcoded match tree: at each depth, group nodes by character. +/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match. +/// +/// If `pathf_map` is non-empty, resolves dispatcher types using full paths. +pub(crate) fn gen_dispatch_args_trie( + entries: &[(String, String, String)], + pathf_map: &HashMap, +) -> TokenStream { + // Prepare (display_name, disp_type) pairs. + // display_name = node_name.replace('.', " ") + let nodes: Vec<(String, String)> = entries + .iter() + .map(|(name, disp, _)| (name.replace('.', " "), disp.clone())) + .collect(); + + let dispatch_body = build_dispatch_body(&nodes, 0, pathf_map); + + quote! { + fn dispatch_args_trie( + raw: &[String], + ) -> Result<::mingling::AnyOutput, ::mingling::error::ProgramInternalExecuteError> + { + let raw_string = format!("{} ", raw.join(" ")); + let raw_str = raw_string.as_str(); + let mut raw_chars = raw_str.chars(); + #dispatch_body + } + } +} + +/// Recursively build the trie match body. +/// +/// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far. +/// `depth`: The character index currently being matched. +/// `pathf_map`: optional mapping from type name to full path for resolving dispatchers. +fn build_dispatch_body( + nodes: &[(String, String)], + depth: usize, + pathf_map: &HashMap, +) -> TokenStream { + if nodes.is_empty() { + return quote! { + return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + }; + } + + // Group by character at `depth` + let mut groups: BTreeMap> = BTreeMap::new(); + let mut exact_nodes: Vec<(String, String)> = Vec::new(); + + for (name, disp_type) in nodes { + if let Some(ch) = name.chars().nth(depth) { + groups + .entry(ch) + .or_default() + .push((name.clone(), disp_type.clone())); + } else { + exact_nodes.push((name.clone(), disp_type.clone())); + } + } + + // Build a dispatch arm for a single node via `starts_with` + let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream { + let name_space = format!("{} ", name); + let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site()); + let disp_resolved = resolve_type(disp_type, pathf_map); + let prefix_word_count = name.split_whitespace().count(); + quote! { + if raw_str.starts_with(#name_lit) { + let prefix_len = #prefix_word_count; + let trimmed_args: Vec = raw.iter().skip(prefix_len).cloned().collect(); + let __cp = <#disp_resolved as ::mingling::Dispatcher>::begin( + &#disp_resolved::default(), + trimmed_args, + ); + return match __cp { + ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0), + ::mingling::ChainProcess::Err(chain_process_error) => { + Err(chain_process_error.into()) + } + }; + } + } + }; + + let mut arms = Vec::new(); + + for (&ch, sub_nodes) in &groups { + let ch_char = ch; + + if sub_nodes.len() == 1 { + let (name, disp_type) = &sub_nodes[0]; + let arm = make_starts_with_arm(name, disp_type); + arms.push(quote! { + Some(#ch_char) => { + #arm + return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + } + }); + } else { + let sub_body = build_dispatch_body(sub_nodes, depth + 1, pathf_map); + arms.push(quote! { + Some(#ch_char) => { + #sub_body + } + }); + } + } + + let exact_checks: Vec = exact_nodes + .iter() + .map(|(name, disp_type)| make_starts_with_arm(name, disp_type)) + .collect(); + + if !exact_checks.is_empty() && !groups.is_empty() { + let match_body = quote! { + match raw_chars.nth(0) { + #(#arms)* + _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())), + } + }; + quote! { + #(#exact_checks)* + #match_body + } + } else if !exact_checks.is_empty() { + quote! { + #(#exact_checks)* + return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + } + } else if arms.is_empty() { + quote! { + return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + } + } else { + quote! { + match raw_chars.nth(0) { + #(#arms)* + _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())), + } + } + } +} diff --git a/mingling_macros/src/systems/res_injection.rs b/mingling_macros/src/systems/res_injection.rs new file mode 100644 index 0000000..606b9a6 --- /dev/null +++ b/mingling_macros/src/systems/res_injection.rs @@ -0,0 +1,261 @@ +use quote::quote; +use syn::spanned::Spanned; +use syn::{FnArg, Ident, Pat, PatType, Signature, Type, TypePath}; + +/// Extracted information about a resource injection parameter +pub(crate) struct ResourceInjection { + pub(crate) var_name: Ident, + pub(crate) full_type: Type, + pub(crate) inner_type: TypePath, + pub(crate) is_ref: bool, + pub(crate) is_mut: bool, +} + +/// Extracts the previous type and parameter name from function arguments, +/// and collects resource injection parameters from the 2nd argument onward. +#[allow(clippy::too_many_lines)] +pub(crate) fn extract_args_info( + sig: &Signature, +) -> syn::Result<(Pat, TypePath, Vec)> { + if sig.inputs.is_empty() { + return Err(syn::Error::new( + sig.span(), + "Function must have at least one parameter", + )); + } + + // First parameter: required, the previous type (must be owned, not a reference) + let first_arg = &sig.inputs[0]; + let (prev_param, previous_type) = match first_arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + let param_pat = (**pat).clone(); + match &**ty { + Type::Path(type_path) => (param_pat, type_path.clone()), + Type::Reference(_) => { + return Err(syn::Error::new( + ty.span(), + "The first parameter (previous type) must be taken by move, \ + not by reference. \ + Use `prev: SomeEntry` instead of `prev: &SomeEntry`.", + )); + } + _ => { + return Err(syn::Error::new( + ty.span(), + "First parameter type must be a type path", + )); + } + } + } + FnArg::Receiver(_) => { + return Err(syn::Error::new( + first_arg.span(), + "Function cannot have self parameter", + )); + } + }; + + // 2nd to Nth parameters: optional, for resource injection + let mut resources = Vec::new(); + for arg in sig.inputs.iter().skip(1) { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + // Extract the variable name – must be a simple identifier + 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 (e.g., `age: &Age`)", + )); + } + }; + + let full_type = *(*ty).clone(); + + // Try to extract inner type for reference patterns like `&Age` -> `Age` + // and `&mut Age` -> `Age` + 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 (e.g., `age: &Age`)", + )); + } + }, + Type::Path(_) => { + return Err(syn::Error::new( + ty.span(), + "Resource injection parameter must be a reference (`&T` or `&mut T`), \ + not an owned value. Use `age: &Age` instead of `age: Age`.", + )); + } + _ => { + return Err(syn::Error::new( + ty.span(), + "Resource injection type must be a type path or reference to one \ + (e.g., `age: Age` or `age: &Age`)", + )); + } + }; + + 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((prev_param, previous_type, resources)) +} + +/// Generates `let` binding statements for immutable resource injection parameters. +/// +/// Each immutable reference parameter gets a `_binding` variable that holds the +/// `res_or_default` result, then a shadowing `let` that borrows from it via `.as_ref()`. +pub(crate) fn generate_immut_resource_bindings<'a>( + resources: impl Iterator, + program_type: &proc_macro2::TokenStream, +) -> Vec { + resources + .filter(|r| !r.is_mut) + .map(|res| { + let var_binding_name = syn::Ident::new( + &format!("__{}_binding", &res.var_name.to_string()), + res.var_name.span(), + ); + let var_name = &res.var_name; + let full_type = &res.full_type; + let inner_type = &res.inner_type; + if res.is_ref { + quote! { + let #var_binding_name = ::mingling::this::<#program_type>() + .res_or_default::<#inner_type>(); + let #var_name: #full_type = #var_binding_name.as_ref(); + } + } else { + quote! { + let #var_name: #full_type = ::mingling::this::<#program_type>() + .res_or_default::<#full_type>(); + } + } + }) + .collect() +} + +/// 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()) +} + +/// 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`). +/// +/// The innermost closure gets the original body, and each mutable parameter wraps +/// outward from last to first. +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)* + }; + + for res in mut_resources { + let var_name = &res.var_name; + let inner_type = &res.inner_type; + 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 +} + +/// Generates code for mutable resource injection in async `#[chain]` functions. +/// +/// Instead of wrapping in a closure (which causes lifetime issues with async), +/// this generates a three-part pattern: +/// +/// 1. **Extract** each mutable resource from the global store into an owned local. +/// 2. **Body block** — the user's body with `&mut` references scoped to a block. +/// 3. **Store back** each modified resource after the body block completes. +/// +/// This avoids holding a `&mut` reference across `.await` points — the borrow +/// ends when the body block closes. +pub(crate) fn wrap_body_with_mut_resources_async( + fn_body_stmts: &[syn::Stmt], + mut_resources: &[&ResourceInjection], + program_type: &proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + // 1. Generate extract statements and body-block `let` bindings + let mut extract_stmts = Vec::new(); + let mut body_lets = Vec::new(); + + for res in mut_resources { + let var_name = &res.var_name; + let inner_type = &res.inner_type; + let binding_name = mut_res_binding_name(var_name); + + extract_stmts.push(quote! { + let mut #binding_name = ::mingling::this::<#program_type>() + .__extract_res_mut::<#inner_type>(); + }); + + body_lets.push(quote! { + let #var_name: &mut #inner_type = &mut #binding_name; + }); + } + + // 2. Store-back statements (reverse order is fine, resources are independent) + let mut store_stmts = Vec::new(); + for res in mut_resources { + let var_name = &res.var_name; + let inner_type = &res.inner_type; + let binding_name = mut_res_binding_name(var_name); + + store_stmts.push(quote! { + ::mingling::this::<#program_type>() + .__store_res::<#inner_type>(#binding_name); + }); + } + + quote! { + #(#extract_stmts)* + let __chain_result = { + #(#body_lets)* + #(#fn_body_stmts)* + }; + #(#store_stmts)* + __chain_result + } +} diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs new file mode 100644 index 0000000..74bcf09 --- /dev/null +++ b/mingling_macros/src/systems/structural_data.rs @@ -0,0 +1,336 @@ +#![allow(dead_code)] + +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Ident, TypePath, parse_macro_input}; + +use crate::get_global_set; + +/// Derive macro for `StructuralData`. +/// +/// This marks a type as eligible for structured output (JSON / YAML / TOML / RON). +/// The type must also implement `serde::Serialize` — the generated `impl StructuralData` +/// will fail to compile if `Serialize` is not in scope or implemented. +/// +/// Also registers the type name in the global `STRUCTURED_TYPES` registry so that +/// the `structural_render` match arm is generated by `gen_program!()`. +pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let type_name = input.ident; + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + // Generate BOTH the sealed impl AND the StructuralData impl. + // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]), + // so the only way to get StructuralData is through this derive macro. + let expanded = quote! { + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + }; + + expanded.into() +} + +/// `pack_structural!` — like `pack!` but also marks the type as supporting +/// structured output via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// pack_structural!(Info = (String, i32)); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// pack!(Info = (String, i32)); +/// impl ::mingling::StructuralData for Info {} +/// ``` +pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { + // Parse same input format as `pack!` + let input_parsed = syn::parse_macro_input!(input as PackStructuralInput); + let type_name = input_parsed.type_name; + let inner_type = input_parsed.inner_type; + let attrs = input_parsed.attrs; + let program_path = crate::default_program_path(); + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + // Struct definition (with Serialize derive, same as pack! under structural_renderer) + #[cfg(not(feature = "structural_renderer"))] + let struct_def = quote! { + #(#attrs)* + pub struct #type_name { + pub inner: #inner_type, + } + }; + + #[cfg(feature = "structural_renderer")] + let struct_def = quote! { + #(#attrs)* + #[derive(serde::Serialize)] + pub struct #type_name { + pub inner: #inner_type, + } + }; + + // Helper impls (same as pack!) + let new_impl = quote! { + impl #type_name { + pub fn new(inner: #inner_type) -> Self { + Self { inner } + } + } + }; + + let from_into_impl = quote! { + impl From<#inner_type> for #type_name { + fn from(inner: #inner_type) -> Self { + Self::new(inner) + } + } + impl From<#type_name> for #inner_type { + fn from(wrapper: #type_name) -> #inner_type { + wrapper.inner + } + } + }; + + let as_ref_impl = quote! { + impl ::std::convert::AsRef<#inner_type> for #type_name { + fn as_ref(&self) -> &#inner_type { + &self.inner + } + } + impl ::std::convert::AsMut<#inner_type> for #type_name { + fn as_mut(&mut self) -> &mut #inner_type { + &mut self.inner + } + } + }; + + let deref_impl = quote! { + impl ::std::ops::Deref for #type_name { + type Target = #inner_type; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + impl ::std::ops::DerefMut for #type_name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + }; + + let default_impl = quote! { + impl ::std::default::Default for #type_name + where + #inner_type: ::std::default::Default, + { + fn default() -> Self { + Self::new(::std::default::Default::default()) + } + } + }; + + let register_impl = quote! { + ::mingling::macros::register_type!(#type_name); + }; + + // StructuralData impl + sealed + registration + let structural_impl = quote! { + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + }; + + let expanded = quote! { + #struct_def + + #new_impl + #from_into_impl + #as_ref_impl + #deref_impl + #default_impl + #register_impl + #structural_impl + + impl Into<::mingling::AnyOutput<#program_path>> for #type_name { + fn into(self) -> ::mingling::AnyOutput<#program_path> { + ::mingling::AnyOutput::new(self) + } + } + + impl Into<::mingling::ChainProcess<#program_path>> for #type_name { + fn into(self) -> ::mingling::ChainProcess<#program_path> { + ::mingling::AnyOutput::new(self).route_chain() + } + } + + impl ::mingling::Grouped<#program_path> for #type_name { + fn member_id() -> #program_path { + #program_path::#type_name + } + } + }; + + expanded.into() +} + +/// Input for `pack_structural!` — same format as `pack!`. +struct PackStructuralInput { + attrs: Vec, + type_name: Ident, + inner_type: syn::Type, +} + +impl syn::parse::Parse for PackStructuralInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + let type_name: Ident = input.parse()?; + input.parse::()?; + let inner_type: syn::Type = input.parse()?; + Ok(PackStructuralInput { + attrs, + type_name, + inner_type, + }) + } +} + +/// `group_structural!` — like `group!` but also marks the type as supporting +/// structured output via `StructuralData`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// group_structural!(Info = (String, i32)); +/// ``` +/// +/// This is equivalent to: +/// ```rust,ignore +/// group!(Info = (String, i32)); +/// 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); + + let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. }); + + let (type_path, type_name, alias_stmt) = match &input_parsed { + GroupStructuralInput::Plain(type_path) => { + let name = type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone(); + (type_path.clone(), name, quote! {}) + } + GroupStructuralInput::Aliased { alias, type_path } => { + let alias_stmt = quote! { + pub(crate) type #alias = #type_path; + }; + (type_path.clone(), alias.clone(), alias_stmt) + } + }; + + let type_name_str = type_name.to_string(); + + // Register in STRUCTURED_TYPES + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + let program_path = crate::default_program_path(); + + // Generate unique module name + let segments: Vec = type_path + .path + .segments + .iter() + .map(|seg| seg.ident.to_string().to_lowercase()) + .collect(); + let module_name = Ident::new( + &format!("internal_group_{}", segments.join("_")), + proc_macro2::Span::call_site(), + ); + + // 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_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone(); + quote! { #[allow(unused_imports)] use super::#ident; } + }; + + let alias_use = if is_aliased { + quote! { use super::#type_name; } + } else { + quote! {} + }; + + let expanded = quote! { + #alias_stmt + #[allow(non_camel_case_types)] + mod #module_name { + use #program_path as __MinglingProgram; + #type_use + #alias_use + + impl ::mingling::Grouped<__MinglingProgram> for #type_name { + fn member_id() -> __MinglingProgram { + __MinglingProgram::#type_name + } + } + + impl ::mingling::__private::StructuralDataSealed for #type_name {} + impl ::mingling::__private::StructuralData for #type_name {} + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} + +/// Input for `group_structural!` — same format as `group!`. +enum GroupStructuralInput { + Plain(TypePath), + Aliased { alias: Ident, type_path: TypePath }, +} + +impl syn::parse::Parse for GroupStructuralInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let fork = input.fork(); + let _first: Ident = fork.parse()?; + if fork.peek(syn::Token![=]) { + let alias: Ident = input.parse()?; + let _eq: syn::Token![=] = input.parse()?; + let type_path: TypePath = input.parse()?; + Ok(GroupStructuralInput::Aliased { alias, type_path }) + } else { + let type_path: TypePath = input.parse()?; + Ok(GroupStructuralInput::Plain(type_path)) + } + } +} diff --git a/mingling_macros/src/utils.rs b/mingling_macros/src/utils.rs new file mode 100644 index 0000000..49d0e7a --- /dev/null +++ b/mingling_macros/src/utils.rs @@ -0,0 +1 @@ +// Shared utilities for the macro crate. -- cgit