mingling_macros 0.1.9

Macros of the mingling library
Documentation
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.
pub(crate) fn extract_args_info(
    sig: &Signature,
) -> syn::Result<(Pat, TypePath, Vec<ResourceInjection>)> {
    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) => {
                    // Check that the type is a single-segment type (no `::`)
                    if type_path.path.segments.len() > 1 {
                        return Err(syn::Error::new(
                            type_path.span(),
                            format!(
                                "The type `{}` must be a simple single-segment type, \
                                 e.g. `Empty` instead of `other::Empty`. \
                                 Qualified paths with `::` are not allowed here.",
                                quote! { #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<Item = &'a ResourceInjection>,
    program_type: &proc_macro2::TokenStream,
) -> Vec<proc_macro2::TokenStream> {
    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()
}

/// Wraps the function body in nested `__modify_res_and_return_route` closures for
/// each mutable resource parameter. 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,
) -> proc_macro2::TokenStream {
    let mut wrapped = quote! {
        #(#fn_body_stmts)*
    };

    for res in mut_resources.iter() {
        let var_name = &res.var_name;
        let inner_type = &res.inner_type;
        wrapped = quote! {
            ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| {
                #wrapped
            }).into()
        };
    }

    wrapped
}