Skip to main content

macros_process_mining/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::meta::ParseNestedMeta;
4use syn::{parse_macro_input, Attribute, FnArg, ItemFn, Lifetime, Pat};
5
6use syn::fold::{self, Fold};
7use syn::{AngleBracketedGenericArguments, GenericArgument, Type, TypeReference};
8
9/// The optional argument added to bindings whose result is stored in the registry, letting the
10/// caller pick the id instead of receiving a generated one.
11const OUTPUT_ID_ARG: &str = "output_id";
12
13/// Name of big data types, which are handled over app state instead of being (de-)serialized
14const BIG_TYPES_NAMES: &[&str] = &[
15    "EventLog",
16    "OCEL",
17    "EventLogActivityProjection",
18    "SlimLinkedOCEL",
19    "IndexLinkedOCEL",
20    "TabularSource",
21];
22
23/// Removes/elide lifetimes and other special cases (i.e., certain generics) from types
24struct LifetimeStripper;
25
26impl Fold for LifetimeStripper {
27    /// Remove/elide lifetimes from type references `&'a T -> &'_ T`
28    fn fold_type_reference(&mut self, mut node: TypeReference) -> TypeReference {
29        // Replace lifetime with placeholder (_)
30        node.lifetime = Some(syn::Lifetime::new("'_", proc_macro2::Span::call_site()));
31        // Recurse
32        fold::fold_type_reference(self, node)
33    }
34
35    /// Remove/elide lifetimes from generic structs `MyStruct<'a, T> -> MyStruct<'_, T>`
36    fn fold_angle_bracketed_generic_arguments(
37        &mut self,
38        mut node: AngleBracketedGenericArguments,
39    ) -> AngleBracketedGenericArguments {
40        // Modify all lifetime arguments
41        node.args = node
42            .args
43            .into_iter()
44            .map(|arg| {
45                if matches!(arg, GenericArgument::Lifetime(_)) {
46                    GenericArgument::Lifetime(Lifetime::new("'_", proc_macro2::Span::call_site()))
47                } else {
48                    arg
49                }
50            })
51            .collect();
52
53        // Recurse
54        fold::fold_angle_bracketed_generic_arguments(self, node)
55    }
56    /// Handle `impl Trait` types specially
57    fn fold_type(&mut self, ty: Type) -> Type {
58        if let Type::ImplTrait(it) = &ty {
59            if it.bounds.len() != 1 {
60                return fold::fold_type(self, ty);
61            }
62            if let Some(syn::TypeParamBound::Trait(really_it)) = it.bounds.first() {
63                let really_it_str = quote::quote!(#really_it).to_string();
64                let ret = match really_it_str.as_str() {
65                    "AsRef < Path >"
66                    | "AsRef < std :: path :: Path >"
67                    | "AsRef < path :: Path >" => {
68                        syn::parse_quote!(std::path::PathBuf)
69                    }
70                    "AsRef < str >" => syn::parse_quote!(String),
71                    "LinkedOCELAccess < 'a >" => syn::parse_quote!(
72                        crate::core::event_data::object_centric::linked_ocel::SlimLinkedOCEL
73                    ),
74                    _ => {
75                        return fold::fold_type(self, ty);
76                    }
77                };
78                return ret;
79            };
80        }
81        fold::fold_type(self, ty)
82    }
83}
84
85/// Strip lifetimes: Helper function to use in your main macro logic
86fn strip_lifetimes(ty: Type) -> Type {
87    let mut stripper = LifetimeStripper;
88    stripper.fold_type(ty)
89}
90
91/// Find the longest matching big type name that is a suffix of the given string.
92///
93/// Using the longest match avoids ambiguity when one type name is a suffix of another
94/// (e.g., "OCEL" is a suffix of "SlimLinkedOCEL").
95fn longest_big_type_match(s: &str) -> Option<String> {
96    BIG_TYPES_NAMES
97        .iter()
98        .filter(|tn| s.ends_with(**tn))
99        .max_by_key(|tn| tn.len())
100        .map(|s| s.to_string())
101}
102
103fn is_big_type_ref(ty: &Type) -> bool {
104    if matches!(ty, Type::Reference(_)) {
105        let ty_str = quote::quote!(#ty).to_string();
106        longest_big_type_match(&ty_str).is_some()
107    } else {
108        false
109    }
110}
111
112fn is_big_type(ty: &Type) -> Option<String> {
113    let ty_str = quote::quote!(#ty).to_string();
114    longest_big_type_match(&ty_str)
115}
116
117/// Check if a type is a mutable reference to a big type (e.g., `&mut SlimLinkedOCEL`)
118/// Returns the big type name if it is.
119fn is_mut_big_type_ref(ty: &Type) -> Option<String> {
120    if let Type::Reference(TypeReference {
121        mutability: Some(_),
122        elem,
123        ..
124    }) = ty
125    {
126        let elem_str = quote::quote!(#elem).to_string();
127        longest_big_type_match(&elem_str)
128    } else {
129        None
130    }
131}
132
133#[derive(Default)]
134struct RegisterBindingAttrs {
135    stringify_error: bool,
136    debug_output: bool,
137    custom_name: Option<String>,
138    returns_handle: bool,
139}
140
141impl RegisterBindingAttrs {
142    fn parse(&mut self, meta: ParseNestedMeta) -> syn::parse::Result<()> {
143        if meta.path.is_ident("debug_output") {
144            self.debug_output = true;
145        } else if meta.path.is_ident("stringify_error") {
146            self.stringify_error = true;
147        } else if meta.path.is_ident("returns_handle") {
148            self.returns_handle = true;
149        } else if meta.path.is_ident("name") {
150            let value: syn::LitStr = meta.value()?.parse()?;
151            self.custom_name = Some(value.value());
152        } else {
153            return Err(meta.error(
154                "unknown #[register_binding] option, expected one of `debug_output`, \
155                 `stringify_error`, `returns_handle`, `name = \"..\"`",
156            ));
157        }
158        Ok(())
159    }
160}
161
162struct ArgOptions {
163    default_value: Option<syn::Expr>,
164    /// `#[bind(state)]`: not a JSON argument but a read-only [`StateRef`] over the registry
165    is_state: bool,
166    /// `#[bind(state_mut)]`: not a JSON argument but a writable [`StateRefMut`] over the
167    /// registry. Unlike `state`, this forces the write lock for the whole call.
168    is_state_mut: bool,
169    /// `#[bind(handle)]`: a reference to a `CustomRegistryValue`, passed as a registry id.
170    is_handle: bool,
171}
172
173fn parse_arg_attributes(attrs: &[Attribute]) -> syn::parse::Result<ArgOptions> {
174    let mut opts = ArgOptions {
175        default_value: None,
176        is_state: false,
177        is_state_mut: false,
178        is_handle: false,
179    };
180    for attr in attrs {
181        if attr.path().is_ident("bind") {
182            attr.parse_nested_meta(|meta| {
183                if meta.path.is_ident("state") {
184                    opts.is_state = true;
185                } else if meta.path.is_ident("state_mut") {
186                    opts.is_state_mut = true;
187                } else if meta.path.is_ident("handle") {
188                    opts.is_handle = true;
189                } else if meta.path.is_ident("default") {
190                    if meta.input.peek(syn::Token![=]) {
191                        let expr: syn::Expr = meta.value()?.parse()?;
192                        opts.default_value = Some(expr);
193                    } else {
194                        opts.default_value = Some(syn::parse_quote!(Default::default()));
195                    }
196                } else {
197                    return Err(meta.error(
198                        "unknown #[bind] option, expected one of `state`, `state_mut`, \
199                         `handle`, `default`, `default = <expr>`",
200                    ));
201                }
202                Ok(())
203            })?;
204        }
205    }
206    Ok(opts)
207}
208
209/// One function argument in the required format for codegen
210struct ArgInfo {
211    name: String,
212    change_from_ref: bool,
213    ty_without_ref: Type,
214    opts: ArgOptions,
215    /// The big type name, for a `&mut` big-type argument
216    mut_big_type_name: Option<String>,
217    /// The referenced type and mutability, for a `#[bind(handle)]` argument
218    handle_elem: Option<(Type, bool)>,
219}
220
221impl ArgInfo {
222    /// Whether this argument needs the write lock
223    /// (like a `&mut` big type)
224    fn is_mut_handle(&self) -> bool {
225        matches!(self.handle_elem, Some((_, true)))
226    }
227}
228
229#[proc_macro_attribute]
230pub fn register_binding(args: TokenStream, item: TokenStream) -> TokenStream {
231    let mut input_fn = parse_macro_input!(item as ItemFn);
232    let fn_ident = &input_fn.sig.ident;
233
234    let mut attrs = RegisterBindingAttrs::default();
235    let attr_parser = syn::meta::parser(|meta| attrs.parse(meta));
236    parse_macro_input!(args with attr_parser);
237
238    let binding_name_str = attrs.custom_name.unwrap_or_else(|| fn_ident.to_string());
239    let wrapper_name = format_ident!("{}_wrapper", fn_ident);
240
241    let docs: Vec<String> = input_fn
242        .attrs
243        .iter()
244        .filter(|attr| attr.path().is_ident("doc"))
245        .filter_map(|attr| match &attr.meta {
246            syn::Meta::NameValue(syn::MetaNameValue {
247                value:
248                    syn::Expr::Lit(syn::ExprLit {
249                        lit: syn::Lit::Str(s),
250                        ..
251                    }),
252                ..
253            }) => Some(s.value()),
254            _ => None,
255        })
256        .flat_map(|s| {
257            s.lines()
258                .map(|l| l.strip_prefix(' ').unwrap_or(l).to_string())
259                .collect::<Vec<_>>()
260        })
261        .collect();
262
263    let args_info: syn::parse::Result<Vec<ArgInfo>> = input_fn
264        .sig
265        .inputs
266        .iter()
267        .map(|arg| match arg {
268            FnArg::Typed(pat_type) => {
269                let pat = &pat_type.pat;
270                let ty = &pat_type.ty;
271
272                let arg_opts = parse_arg_attributes(&pat_type.attrs)?;
273
274                let arg_name =
275                    match &**pat {
276                        Pat::Ident(p) => p.ident.to_string(),
277                        _ => return Err(syn::Error::new_spanned(
278                            pat,
279                            "#[register_binding] needs a plain `name: Type` argument, because the \
280                             name is what a caller sends the argument under.",
281                        )),
282                    };
283
284                let ty_no_life = strip_lifetimes(*ty.clone());
285                let ty_as_str = quote::quote!(#ty_no_life).to_string();
286                // A handle is passed as an id and resolved through `FromContext`, like a big type.
287                let handle_elem = match (&ty_no_life, arg_opts.is_handle) {
288                    (Type::Reference(r), true) => Some((*r.elem.clone(), r.mutability.is_some())),
289                    (_, true) => {
290                        return Err(syn::Error::new_spanned(
291                            ty,
292                            "#[bind(handle)] is only valid on a reference argument \
293                             (`&T` or `&mut T`), where `T: CustomRegistryValue`.",
294                        ))
295                    }
296                    (_, false) => None,
297                };
298                let change_from_ref = matches!(ty_no_life, Type::Reference(_))
299                    && !arg_opts.is_handle
300                    && !(BIG_TYPES_NAMES.iter().any(|tn| ty_as_str.ends_with(tn)));
301                let type_without_ref = match &ty_no_life {
302                    Type::Reference(type_reference) if change_from_ref => {
303                        match &*type_reference.elem {
304                            // `&[T]` -> extract a `Vec<T>`; `&Vec<T>` coerces back to `&[T]` at the
305                            // call site. (Does not apply to `&[&str]`: `Vec<&str>` is not owned.)
306                            Type::Slice(slice) => {
307                                let elem = &slice.elem;
308                                syn::parse_quote!(Vec<#elem>)
309                            }
310                            _ => *type_reference.elem.clone(),
311                        }
312                    }
313                    x => x.clone(),
314                };
315                let mut_big_type_name = if handle_elem.is_some() {
316                    None
317                } else {
318                    is_mut_big_type_ref(&ty_no_life)
319                };
320                Ok(ArgInfo {
321                    name: arg_name,
322                    change_from_ref,
323                    ty_without_ref: type_without_ref,
324                    opts: arg_opts,
325                    mut_big_type_name,
326                    handle_elem,
327                })
328            }
329            FnArg::Receiver(receiver) => Err(syn::Error::new_spanned(
330                receiver,
331                "#[register_binding] is for free functions. A method's `self` has no argument \
332                 name a caller could send it under.",
333            )),
334        })
335        .collect();
336
337    // Strip `#[bind]` before `#input_fn` is emitted on any path, including the error paths below.
338    for input in &mut input_fn.sig.inputs {
339        if let FnArg::Typed(pat_type) = input {
340            pat_type.attrs.retain(|attr| !attr.path().is_ident("bind"));
341        }
342    }
343
344    let args_info = match args_info {
345        Ok(args_info) => args_info,
346        Err(e) => {
347            let err = e.to_compile_error();
348            return TokenStream::from(quote! { #input_fn #err });
349        }
350    };
351
352    // Whether the call needs the registry's write lock rather than its read lock: a `&mut`
353    // big-type argument, a `#[bind(handle)]` on a `&mut` reference, or `#[bind(state_mut)]`
354    // all resolve through the same write-locked `__state_guard` in the execution block below.
355    let needs_write_lock = args_info
356        .iter()
357        .any(|a| a.mut_big_type_name.is_some() || a.is_mut_handle() || a.opts.is_state_mut);
358
359    // A `#[bind(state)]` cannot be combined with anything needing the write lock (conflicting
360    // ownership/mut borrow of the same registry guard).
361    if needs_write_lock && args_info.iter().any(|a| a.opts.is_state) {
362        let err = syn::Error::new_spanned(
363            &input_fn.sig,
364            "#[bind(state)] cannot be combined with a `&mut` big-type, `#[bind(handle)]`, or \
365             `#[bind(state_mut)]` argument. Return the value instead of taking it by `&mut`.",
366        )
367        .to_compile_error();
368        return TokenStream::from(quote! { #input_fn #err });
369    }
370
371    // `#[bind(state_mut)]` already gives full mutable access to every item in the registry, so
372    // it must be the only thing asking for the write lock: combining it with a `&mut` big-type
373    // or `#[bind(handle)]` argument would borrow the same guard mutably twice.
374    let state_mut_count = args_info.iter().filter(|a| a.opts.is_state_mut).count();
375    if state_mut_count > 1 {
376        let err = syn::Error::new_spanned(
377            &input_fn.sig,
378            "#[bind(state_mut)] may only be used once per binding.",
379        )
380        .to_compile_error();
381        return TokenStream::from(quote! { #input_fn #err });
382    }
383    if state_mut_count == 1
384        && args_info
385            .iter()
386            .any(|a| !a.opts.is_state_mut && (a.mut_big_type_name.is_some() || a.is_mut_handle()))
387    {
388        let err = syn::Error::new_spanned(
389            &input_fn.sig,
390            "#[bind(state_mut)] cannot be combined with a `&mut` big-type or `#[bind(handle)]` \
391             argument; it already has full mutable access to the registry, so reach that item \
392             through `state_mut` instead.",
393        )
394        .to_compile_error();
395        return TokenStream::from(quote! { #input_fn #err });
396    }
397
398    if needs_write_lock {
399        if let Some(shared) = args_info.iter().find(|a| {
400            a.mut_big_type_name.is_none()
401                && !a.is_mut_handle()
402                && !a.opts.is_state_mut
403                && (a.handle_elem.is_some() || is_big_type_ref(&a.ty_without_ref))
404        }) {
405            let name = &shared.name;
406            let err = syn::Error::new_spanned(
407                &input_fn.sig,
408                format!(
409                    "#[register_binding] on a function taking a `&mut` big-type, \
410                     `#[bind(handle)]`, or `#[bind(state_mut)]` argument cannot also take \
411                     `{name}` by shared reference. Take it by `&mut` too, or take it by value."
412                ),
413            )
414            .to_compile_error();
415            return TokenStream::from(quote! { #input_fn #err });
416        }
417    }
418
419    // 1. Extraction Logic (for the read-locked path only; a `#[bind(state_mut)]` argument
420    // forces `needs_write_lock`, so this map's default arm never runs for one; it is handled in
421    // the write-locked `execution_block` below instead.)
422    let extractions = args_info.iter().map(|a| {
423        let (name, is_ref, ty_without_ref, opts) = (&a.name, a.change_from_ref, &a.ty_without_ref, &a.opts);
424        if opts.is_state {
425            return quote! { ::process_mining::bindings::StateRef::new(state) };
426        }
427        let maybe_ref = if is_ref {
428            quote! {&}
429        } else {
430            quote! {}
431        };
432        if let Some(default_expr) = &opts.default_value {
433            quote! {
434                #maybe_ref ::process_mining::bindings::extract_param::<#ty_without_ref>(arg_map, #name, state, || Some(#default_expr))?
435            }
436        } else {
437            quote! {
438                #maybe_ref ::process_mining::bindings::extract_param::<#ty_without_ref>(arg_map, #name, state, || None)?
439            }
440        }
441    });
442
443    // 2. Schema Logic
444    let schema_gens = args_info.iter().map(|a| {
445        let (name, ty_without_ref, opts) = (&a.name, &a.ty_without_ref, &a.opts);
446        if opts.is_state || opts.is_state_mut {
447            return quote! {};
448        }
449        if let Some((elem, _)) = &a.handle_elem {
450             quote! {
451                 args_schema.push((#name.to_string(), ::process_mining::__private::serde_json::json!({
452                    "type": "string",
453                    "title": <#elem as ::process_mining::bindings::CustomRegistryValue>::kind_name(),
454                    "x-registry-ref": <#elem as ::process_mining::bindings::CustomRegistryValue>::kind_name(),
455                    "x-widget": "entity-selector"
456                })));
457             }
458        } else if is_big_type_ref(ty_without_ref) {
459             let ty_str = quote::quote!(#ty_without_ref).to_string();
460             let type_name = longest_big_type_match(&ty_str).unwrap();
461             quote! {
462                 args_schema.push((#name.to_string(), ::process_mining::__private::serde_json::json!({
463                    "type": "string",
464                    "title": #type_name,
465                    "x-registry-ref": #type_name,
466                    "x-widget": "entity-selector"
467                })));
468             }
469        } else {
470            quote! { args_schema.push((#name.to_string(), ::process_mining::__private::serde_json::to_value(::process_mining::__private::schemars::schema_for!(#ty_without_ref)).unwrap())); }
471        }
472    });
473
474    // 3. Return Type Schema Logic
475    let raw_ret_type = match &input_fn.sig.output {
476        syn::ReturnType::Default => syn::parse_quote!(()), // Handle "void" -> unit type
477        syn::ReturnType::Type(_, ty) => *ty.clone(),
478    };
479
480    // Strip lifetimes from return type
481    let mut ret_type = strip_lifetimes(raw_ret_type);
482
483    if attrs.returns_handle && attrs.debug_output {
484        let err = syn::Error::new_spanned(
485            &input_fn.sig,
486            "#[register_binding(returns_handle)] cannot be combined with `debug_output`: \
487             one stores the value and returns its id, the other formats it away.",
488        )
489        .to_compile_error();
490        return TokenStream::from(quote! { #input_fn #err });
491    }
492
493    // If debug_output is set, the actual return type is String
494    if attrs.debug_output {
495        ret_type = syn::parse_quote!(String);
496    } else if attrs.stringify_error {
497        // If stringify_error is set, we expect a Result<T, E> (or io::Result<T>).
498        // We unwrap the Result and use only the Ok type for the schema,
499        // since errors are propagated via the handler's Result return.
500        if let Type::Path(tp) = &ret_type {
501            if let Some(segment) = tp.path.segments.last() {
502                // Heuristic: If it looks like a Result (std or io), grab the first generic arg (Ok type)
503                if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
504                    if let Some(ok_type) = args.args.first() {
505                        ret_type = syn::parse_quote!(#ok_type);
506                    }
507                }
508            }
509        }
510    }
511
512    // Handling the two ways a result ends up in the registry instead of being serialized
513    let returns_registry_handle = attrs.returns_handle || is_big_type(&ret_type).is_some();
514
515    let output_id_schema = if returns_registry_handle {
516        quote! {
517            args_schema.push((#OUTPUT_ID_ARG.to_string(), ::process_mining::__private::serde_json::json!({
518                "type": ["string", "null"],
519                "title": #OUTPUT_ID_ARG,
520                "description": "Store the result under this id instead of a generated one."
521            })));
522        }
523    } else {
524        quote! {}
525    };
526
527    let output_id_extraction = if returns_registry_handle {
528        quote! {
529            let __output_id = ::process_mining::bindings::extract_param_json::<Option<String>>(arg_map, #OUTPUT_ID_ARG, || Some(None))?;
530        }
531    } else {
532        quote! {}
533    };
534
535    let stored_id = quote! {
536        let id = __output_id
537            .unwrap_or_else(|| format!("res_{}", ::process_mining::__private::uuid::Uuid::new_v4()));
538    };
539
540    // A `#[bind(state)]`/`#[bind(state_mut)]` argument has no schema, so requiring it by name
541    // would tell a host to demand an argument it cannot describe and the caller cannot supply.
542    let required_arg_names = args_info
543        .iter()
544        .filter(|a| a.opts.default_value.is_none() && !a.opts.is_state && !a.opts.is_state_mut)
545        .map(|a| &a.name);
546
547    // 4. Generate the Execution Logic
548    let extractions: Vec<_> = extractions.collect();
549
550    // Apply error handling if requested, independent of whether the return type is a big type or not.
551    let error_handling = if attrs.stringify_error && !attrs.debug_output {
552        quote! { let result = result.map_err(|e| e.to_string())?; }
553    } else {
554        quote! {}
555    };
556
557    let serialization_logic = if attrs.debug_output {
558        quote! {
559            let final_result = format!("{:?}", result);
560            ::process_mining::__private::serde_json::to_vec(&final_result).map_err(|e| e.to_string())
561        }
562    } else {
563        quote! {
564            ::process_mining::__private::serde_json::to_vec(&result).map_err(|e| e.to_string())
565        }
566    };
567
568    let execution_block = if needs_write_lock {
569        // Mutable big type path: use write lock
570        // 1. Generate JSON extractions for non-mut-big-type params (no state needed)
571        let json_extractions: Vec<_> = args_info
572            .iter()
573            .filter(|a| a.mut_big_type_name.is_none() && !a.is_mut_handle() && !a.opts.is_state_mut)
574            .map(|a| {
575                let (name, is_ref, ty_without_ref, opts) = (&a.name, a.change_from_ref, &a.ty_without_ref, &a.opts);
576                let param_ident = format_ident!("__param_{}", name);
577                let maybe_ref = if is_ref {
578                    quote! { & }
579                } else {
580                    quote! {}
581                };
582                if let Some(default_expr) = &opts.default_value {
583                    quote! {
584                        let #param_ident = #maybe_ref ::process_mining::bindings::extract_param_json::<#ty_without_ref>(arg_map, #name, || Some(#default_expr))?;
585                    }
586                } else {
587                    quote! {
588                        let #param_ident = #maybe_ref ::process_mining::bindings::extract_param_json::<#ty_without_ref>(arg_map, #name, || None)?;
589                    }
590                }
591            })
592            .collect();
593
594        // 2. Generate mutable big type extractions from state
595        let mut_extractions: Vec<_> = args_info
596            .iter()
597            .filter_map(|a| {
598                let name = &a.name;
599                let param_ident = format_ident!("__param_{}", name);
600                if a.opts.is_state_mut {
601                    return Some(quote! {
602                        let #param_ident = ::process_mining::bindings::StateRefMut::new(&mut __state_guard);
603                    });
604                }
605                if let Some((elem, true)) = &a.handle_elem {
606                    return Some(quote! {
607                        let #param_ident = {
608                            let __id = arg_map.get(#name).and_then(|v| v.as_str())
609                                .ok_or_else(|| format!("Missing required argument {}", #name))?;
610                            __state_guard.get_mut(__id)
611                                .ok_or_else(|| format!("Item '{}' not found", __id))?
612                                .as_custom_mut::<#elem>()
613                                .ok_or_else(|| format!("ID '{}' is not a {}", __id,
614                                    <#elem as ::process_mining::bindings::CustomRegistryValue>::kind_name()))?
615                        };
616                    });
617                }
618                let type_name = a.mut_big_type_name.as_ref()?;
619                let variant_ident = format_ident!("{}", type_name);
620                Some(quote! {
621                    let #param_ident = {
622                        let __id = arg_map.get(#name).and_then(|v| v.as_str())
623                            .ok_or_else(|| format!("Missing required argument {}", #name))?;
624                        match __state_guard.get_mut(__id)
625                            .ok_or_else(|| format!("Item '{}' not found", __id))? {
626                            ::process_mining::bindings::RegistryItem::#variant_ident(inner) => inner,
627                            _ => return Err(format!("ID '{}' is not a {}", __id, #type_name)),
628                        }
629                    };
630                })
631            })
632            .collect();
633
634        // 3. Generate call arguments in original order
635        let call_args: Vec<_> = args_info
636            .iter()
637            .map(|a| {
638                let param_ident = format_ident!("__param_{}", a.name);
639                quote! { #param_ident }
640            })
641            .collect();
642
643        // The write guard is still live here, and `std::sync::RwLock` is not reentrant, so this
644        // has to insert through the guard rather than call `state_lock.add`, which would take the
645        // lock again and deadlock.
646        let mut_serialization = if attrs.returns_handle {
647            quote! {
648                #stored_id
649                __state_guard.insert(id.clone(), ::process_mining::bindings::RegistryItem::custom(result));
650                ::process_mining::__private::serde_json::to_vec(&id).map_err(|e| e.to_string())
651            }
652        } else if let Some(type_name) = is_big_type(&ret_type) {
653            let variant_ident = format_ident!("{}", type_name);
654            quote! {
655                #stored_id
656                __state_guard.insert(id.clone(), ::process_mining::bindings::RegistryItem::#variant_ident(result));
657                ::process_mining::__private::serde_json::to_vec(&id).map_err(|e| e.to_string())
658            }
659        } else {
660            serialization_logic.clone()
661        };
662
663        quote! {
664            #(#json_extractions)*
665            let mut __state_guard = state_lock.write();
666            #(#mut_extractions)*
667            let result = #fn_ident( #(#call_args),* );
668            #error_handling
669            #mut_serialization
670        }
671    } else if attrs.returns_handle {
672        quote! {
673            let result = {
674                let state_guard = state_lock.read();
675                let state = &*state_guard;
676                #fn_ident( #(#extractions),* )
677            };
678            #error_handling
679            #stored_id
680            state_lock.add(&id, ::process_mining::bindings::RegistryItem::custom(result));
681            ::process_mining::__private::serde_json::to_vec(&id).map_err(|e| e.to_string())
682        }
683    } else if let Some(type_name) = is_big_type(&ret_type) {
684        let variant_ident = format_ident!("{}", type_name);
685        quote! {
686            let result = {
687                let state_guard = state_lock.read();
688                let state = &*state_guard;
689                #fn_ident( #(#extractions),* )
690            };
691            #error_handling
692            #stored_id
693            state_lock.add(&id, ::process_mining::bindings::RegistryItem::#variant_ident(result));
694            ::process_mining::__private::serde_json::to_vec(&id).map_err(|e| e.to_string())
695        }
696    } else {
697        quote! {
698            let state_guard = state_lock.read();
699            let state = &*state_guard;
700            let result = #fn_ident( #(#extractions),* );
701            #error_handling
702            #serialization_logic
703        }
704    };
705
706    let ret_type_schema = if attrs.returns_handle {
707        quote! {
708            ::process_mining::__private::serde_json::json!({
709               "type": "string",
710               "title": <#ret_type as ::process_mining::bindings::CustomRegistryValue>::kind_name(),
711               "x-registry-ref": <#ret_type as ::process_mining::bindings::CustomRegistryValue>::kind_name(),
712               "x-widget": "entity-selector"
713           })
714        }
715    } else if let Some(type_name) = is_big_type(&ret_type) {
716        quote! {
717            ::process_mining::__private::serde_json::json!({
718               "type": "string",
719               "title": #type_name,
720               "x-registry-ref": #type_name,
721               "x-widget": "entity-selector"
722           })
723        }
724    } else {
725        quote! {
726            ::process_mining::__private::serde_json::to_value(::process_mining::__private::schemars::schema_for!(#ret_type)).unwrap()
727        }
728    };
729
730    // Shadowing the caller's own argument would silently change what the binding is passed, so
731    // refuse instead. The function itself is still emitted, to keep its call sites resolving.
732    if returns_registry_handle && args_info.iter().any(|a| a.name == OUTPUT_ID_ARG) {
733        let msg = format!(
734            "#[register_binding] on `{}`: its result is stored in the registry, so the binding \
735             already takes an `{}` argument. Rename the function's own argument.",
736            fn_ident, OUTPUT_ID_ARG
737        );
738        return TokenStream::from(quote! {
739            #input_fn
740            ::core::compile_error!(#msg);
741        });
742    }
743
744    let docs_fn_name = format_ident!("{}_docs", fn_ident);
745    let args_fn_name = format_ident!("{}_args", fn_ident);
746    let required_args_fn_name = format_ident!("{}_required_args", fn_ident);
747    let return_type_fn_name = format_ident!("{}_return_type", fn_ident);
748
749    // `cfg(feature = "bindings")` resolves in the crate being compiled, so a downstream crate with
750    // no feature by that name would silently register nothing.
751    let registration_cfg = if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("process_mining") {
752        quote! { #[cfg(feature = "bindings")] }
753    } else {
754        quote! {}
755    };
756
757    let expanded = quote! {
758        #input_fn
759
760        #registration_cfg
761        const _: () = {
762            use ::process_mining::bindings::{Binding, AppState};
763            use ::process_mining::__private::serde_json::Value;
764
765            fn #wrapper_name(args: &Value, state_lock: &AppState) -> Result<Vec<u8>, String> {
766                let arg_map = args.as_object().ok_or("Args must be JSON object")?;
767                #output_id_extraction
768                #execution_block
769            }
770
771            fn #docs_fn_name() -> Vec<String> {
772                vec![#(#docs.to_string(),)*]
773            }
774
775            fn #args_fn_name() -> Vec<(String, Value)> {
776                let mut args_schema = ::std::vec::Vec::new();
777                #(#schema_gens)*
778                #output_id_schema
779                args_schema
780            }
781
782            fn #required_args_fn_name() -> Vec<String> {
783                vec![#(#required_arg_names.to_string(),)*]
784            }
785
786            fn #return_type_fn_name() -> Value {
787                #ret_type_schema
788            }
789
790            ::process_mining::__private::inventory::submit! {
791                Binding {
792                    id: concat!(module_path!(), "::", stringify!(#fn_ident)),
793                    name: #binding_name_str,
794                    handler: #wrapper_name,
795                    docs: #docs_fn_name,
796                    module: module_path!(),
797                    source_path: file!(),
798                    source_line: line!(),
799                    args: #args_fn_name,
800                    required_args: #required_args_fn_name,
801                    return_type: #return_type_fn_name,
802                }
803            }
804        };
805    };
806    TokenStream::from(expanded)
807}
808
809/// Returns the list of "Big Types" known to the macro crate as a static string slice array.
810/// Used for consistency testing.
811#[proc_macro]
812pub fn big_types_list(_item: TokenStream) -> TokenStream {
813    let types = BIG_TYPES_NAMES;
814    let expanded = quote! {
815        &[#(#types),*]
816    };
817    TokenStream::from(expanded)
818}
819
820/// Let a `CustomRegistryValue` implementor be taken by `#[bind(handle)] &T`.
821///
822/// Generates the `FromContext` impl that turns the incoming id into a borrow of the stored value.
823///
824/// Unlike [`macro@RegistryEntity`], the impl is not behind `#[cfg(feature = "bindings")]`: that
825/// cfg is evaluated in the deriving crate's feature namespace, and a downstream crate need not
826/// have a feature of that name.
827#[proc_macro_derive(CustomRegistryEntity)]
828pub fn derive_custom_registry_entity(item: TokenStream) -> TokenStream {
829    let input = parse_macro_input!(item as syn::DeriveInput);
830    let name = &input.ident;
831
832    let expanded = quote! {
833        impl<'a> ::process_mining::bindings::FromContext<'a> for &'a #name {
834            fn from_context(value: &::process_mining::__private::serde_json::Value, state: &'a ::process_mining::bindings::InnerAppState) -> Result<Self, String> {
835                let id = value.as_str().ok_or("Expected String ID")?;
836                let item = state.get(id).ok_or_else(|| format!("Item '{}' not found", id))?;
837                item.as_custom::<#name>().ok_or_else(|| {
838                    format!(
839                        "ID '{}' is not a {}",
840                        id,
841                        <#name as ::process_mining::bindings::CustomRegistryValue>::kind_name()
842                    )
843                })
844            }
845        }
846    };
847    TokenStream::from(expanded)
848}
849
850#[proc_macro_derive(RegistryEntity)]
851pub fn derive_registry_entity(item: TokenStream) -> TokenStream {
852    let input = parse_macro_input!(item as syn::DeriveInput);
853    let name = &input.ident;
854    let name_str = name.to_string();
855
856    let expanded = quote! {
857        #[cfg(feature = "bindings")]
858        impl<'a> ::process_mining::bindings::FromContext<'a> for &'a #name {
859            fn from_context(value: &::process_mining::__private::serde_json::Value, state: &'a ::process_mining::bindings::InnerAppState) -> Result<Self, String> {
860                let id = value.as_str().ok_or("Expected String ID")?;
861                let item = state.get(id).ok_or_else(|| format!("Item '{}' not found", id))?;
862
863                if let ::process_mining::bindings::RegistryItem::#name(inner) = item {
864                    Ok(inner)
865                } else {
866                    Err(format!("ID '{}' is not a {}", id, #name_str))
867                }
868            }
869        }
870    };
871    TokenStream::from(expanded)
872}