diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-22 20:41:00 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-22 20:41:00 +0800 |
| commit | 5169b9a462a7a3854b8320c8d9e78985a34c5f15 (patch) | |
| tree | b0c029bb7dbf4e0cb39794f43c3b3166d539e24b | |
| parent | d7c9ad94113cca2f782666e37a0aa4fb7b8d7d86 (diff) | |
Support resource injection in #[help] and #[completion
| -rw-r--r-- | CHANGELOG.md | 20 | ||||
| -rw-r--r-- | mingling_macros/src/completion.rs | 150 | ||||
| -rw-r--r-- | mingling_macros/src/help.rs | 101 |
3 files changed, 207 insertions, 64 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0f5b5..5384b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,6 +195,26 @@ group!(std::io::Error); This macro is only available with the `extra_macros` feature. +11. **\[macros\]** `#[help]` and `#[completion]` now support resource injection parameters, consistent with `#[chain]` and `#[renderer]`. Specific changes: + +- `#[help]`: Removed the restriction of "must have exactly one parameter". The first parameter still serves as the entry type, while subsequent parameters are treated as resource injections. The internal implementation was changed from a nested `help_wrapper` function to an inline body (consistent with the renderer), making resource variables visible within the scope. +- `#[completion]`: Removed the restriction of "must have exactly one parameter". The first parameter `ctx: &ShellContext` is used for the `Completion::comp` trait method signature, while subsequent parameters are treated as resource injections. Within the `comp` method body, resource bindings are injected via `::mingling::this::<P>().res_or_default::<T>()` and `modify_res`. + +```rust +#[help] +fn help_my_entry(prev: EntryMyEntry, res: &ResA) { + r_println!("res: {:?}", *res); +} + +#[completion(EntryMyEntry)] +fn comp_my_entry(ctx: &ShellContext, res: &mut ResA) -> Suggest { + // res is injected from the program's global resources + suggest! {} +} +``` + +For mutable resources (`&mut T`), both macros use `Program::modify_res` (with constraint `Return: Default`) instead of `#[chain]`'s dedicated `__modify_res_and_return_route` (with constraint `Return: Into<ChainProcess>`), because the return types of help/completion are `()` and `Suggest` respectively. + #### **BREAKING CHANGES** (API CHANGES): --- diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/completion.rs index 720ac49..452c418 100644 --- a/mingling_macros/src/completion.rs +++ b/mingling_macros/src/completion.rs @@ -1,11 +1,12 @@ +use crate::res_injection::{ResourceInjection, generate_immut_resource_bindings}; use proc_macro::TokenStream; use quote::quote; -use syn::{Ident, ItemFn, TypePath, parse_macro_input}; +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 (e.g., HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)]) - + // 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( @@ -24,8 +25,6 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Validate the function is not async if input_fn.sig.asyncness.is_some() { - use syn::spanned::Spanned; - return syn::Error::new(input_fn.sig.span(), "Completion function cannot be async") .to_compile_error() .into(); @@ -36,22 +35,44 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let inputs = &sig.inputs; let output = &sig.output; - // Check that the function has exactly one parameter - if inputs.len() != 1 { - use syn::spanned::Spanned; - + // Must have at least one parameter ctx + if inputs.is_empty() { return syn::Error::new( inputs.span(), - "Completion function must have exactly one parameter", + "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) + // Get function attributes excluding the completion attribute let mut fn_attrs = input_fn.attrs.clone(); fn_attrs.retain(|attr| !attr.path().is_ident("completion")); @@ -68,7 +89,43 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { ); 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)] @@ -78,8 +135,8 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { impl ::mingling::Completion for #struct_name { type Previous = #previous_type_path; - fn comp(#inputs) #output { - #fn_body + fn comp(#ctx_pat: &::mingling::ShellContext) #output { + #comp_body } } @@ -100,3 +157,70 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { expanded.into() } + +/// Extract resource injection parameters from function arguments (skipping the first N params). +fn extract_resources_from_args( + sig: &syn::Signature, + skip: usize, +) -> syn::Result<Vec<ResourceInjection>> { + let mut resources = Vec::new(); + for arg in sig.inputs.iter().skip(skip) { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + let var_name = match &**pat { + Pat::Ident(pat_ident) => pat_ident.ident.clone(), + _ => { + return Err(syn::Error::new( + pat.span(), + "Resource injection parameter must be a simple identifier", + )); + } + }; + + let full_type = *(*ty).clone(); + + let (inner_type, is_ref, is_mut) = match &full_type { + Type::Reference(ref_type) => match &*ref_type.elem { + Type::Path(type_path) => { + let is_mut = ref_type.mutability.is_some(); + (type_path.clone(), true, is_mut) + } + _ => { + return Err(syn::Error::new( + ty.span(), + "Reference resource type must be a type path", + )); + } + }, + Type::Path(_) => { + return Err(syn::Error::new( + ty.span(), + "Resource injection parameter must be a reference (`&T` or `&mut T`)", + )); + } + _ => { + return Err(syn::Error::new( + ty.span(), + "Resource injection type must be a type path or reference", + )); + } + }; + + resources.push(ResourceInjection { + var_name, + full_type, + inner_type, + is_ref, + is_mut, + }); + } + FnArg::Receiver(_) => { + return Err(syn::Error::new( + arg.span(), + "Resource injection parameter cannot be self", + )); + } + } + } + Ok(resources) +} diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs index e9e91cf..e904b16 100644 --- a/mingling_macros/src/help.rs +++ b/mingling_macros/src/help.rs @@ -1,44 +1,10 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{ - FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input, -}; +use syn::{Ident, ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input}; use crate::get_global_set; - -/// Extracts the previous type and parameter name from function arguments -fn extract_previous_info(sig: &Signature) -> syn::Result<(Pat, TypePath)> { - // The function should have exactly one parameter - if sig.inputs.len() != 1 { - return Err(syn::Error::new( - sig.inputs.span(), - "Help function must have exactly one parameter (the entry type)", - )); - } - - // First and only parameter is the entry type - 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 - match &**ty { - Type::Path(type_path) => Ok((param_pat, type_path.clone())), - _ => Err(syn::Error::new( - ty.span(), - "Parameter type must be a type path", - )), - } - } - FnArg::Receiver(_) => Err(syn::Error::new( - arg.span(), - "Help function cannot have self parameter", - )), - } -} +use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; /// Validates the return type is () or empty fn validate_return_type(sig: &Signature) -> syn::Result<()> { @@ -65,8 +31,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream { .into(); } - // Extract the entry type and parameter name from function arguments - let (prev_param, entry_type) = match extract_previous_info(&input_fn.sig) { + // 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(), }; @@ -78,8 +44,9 @@ pub fn help_attr(item: TokenStream) -> TokenStream { // Get the function body let fn_body = &input_fn.block; + let fn_body_stmts = &fn_body.stmts; - // Get function attributes (excluding the help attribute) + // Get function attributes excluding the help attribute let mut fn_attrs = input_fn.attrs.clone(); fn_attrs.retain(|attr| !attr.path().is_ident("help")); @@ -89,13 +56,52 @@ pub fn help_attr(item: TokenStream) -> TokenStream { // Get function name let fn_name = &input_fn.sig.ident; - // Generate internal name using snake_case for the chain macro + // Get original inputs to keep the original function + + let original_inputs = input_fn.sig.inputs.clone(); + + // 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(); @@ -115,23 +121,16 @@ pub fn help_attr(item: TokenStream) -> TokenStream { type Entry = #entry_type; fn render_help(#prev_param: Self::Entry, __renderer_inner_result: &mut ::mingling::RenderResult) { - // Create a local wrapper function that includes `__renderer_inner_result` parameter - // This allows r_println! to access `__renderer_inner_result` - #[allow(non_snake_case)] - fn help_wrapper(#prev_param: #entry_type, __renderer_inner_result: &mut ::mingling::RenderResult) { - #fn_body - } - - // Call the wrapper function - help_wrapper(#prev_param, __renderer_inner_result); + #help_render_body } } ::mingling::macros::register_help!(#entry_type, #struct_name); - // Keep the original function for internal use (without `__renderer_inner_result` parameter) + // Keep the original function for internal use with original params without __renderer_inner_result + #(#fn_attrs)* - #vis fn #fn_name(#prev_param: #entry_type) { + #vis fn #fn_name(#original_inputs) { let mut dummy_r = ::mingling::RenderResult::default(); let __renderer_inner_result = &mut dummy_r; #fn_body |
