aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/res_injection.rs
blob: f2952ccf35a297592e9e99cd5517b10c8fa56ee2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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<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) => (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()
}

/// 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 nested `__modify_res_and_return_route` closures for
/// each mutable resource parameter (sync version).
///
/// 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 {
        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
}

/// 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
    }
}