pax_macro/
lib.rs

1extern crate proc_macro;
2extern crate proc_macro2;
3mod parsing;
4mod templating;
5use std::fs::File;
6use std::io::Read;
7use std::path::Path;
8use std::str::FromStr;
9use std::{env, fs, path::PathBuf}; // Necessary for `writeln!` macro to work
10
11use proc_macro2::{Ident, Span, TokenStream};
12use quote::{format_ident, quote, ToTokens};
13
14use syn::punctuated::Punctuated;
15use templating::{
16    ArgsFullComponent, ArgsPrimitive, ArgsStructOnlyComponent, EnumVariantDefinition,
17    InternalDefinitions, StaticPropertyDefinition, TemplateArgsDerivePax,
18};
19
20use sailfish::TemplateOnce;
21
22const CRATES_WHERE_WE_DONT_PARSE_DESIGNER: &[&str] = &["pax-designer", "pax-std", "pax-runtime"];
23
24fn is_root_crate() -> bool {
25    let is_not_blacklisted = !CRATES_WHERE_WE_DONT_PARSE_DESIGNER
26        .contains(&std::env::var("CARGO_PKG_NAME").unwrap_or_default().as_str());
27    is_not_blacklisted
28}
29
30use syn::{
31    parse_macro_input, Data, DeriveInput, Field, Fields, FnArg, GenericArgument, ImplItem,
32    ItemImpl, Lit, Meta, PatType, PathArguments, Signature, Token, Type,
33};
34
35fn pax_primitive(
36    input_parsed: &DeriveInput,
37    primitive_instance_import_path: String,
38    is_custom_interpolatable: bool,
39    engine_import_path: String,
40) -> proc_macro2::TokenStream {
41    let _original_tokens = quote! { #input_parsed }.to_string();
42    let pascal_identifier = input_parsed.ident.to_string();
43    let is_enum = match &input_parsed.data {
44        Data::Enum(_) => true,
45        _ => false,
46    };
47
48    let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
49
50    let output = TemplateArgsDerivePax {
51        args_primitive: Some(ArgsPrimitive {
52            primitive_instance_import_path,
53        }),
54        args_struct_only_component: None,
55        args_full_component: None,
56        internal_definitions,
57        pascal_identifier,
58        cargo_dir: std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "".into()),
59        is_custom_interpolatable,
60        is_root_crate: is_root_crate(),
61        _is_enum: is_enum,
62        engine_import_path,
63    }
64    .render_once()
65    .unwrap()
66    .to_string();
67
68    TokenStream::from_str(&output).unwrap().into()
69}
70
71fn pax_struct_only_component(
72    input_parsed: &DeriveInput,
73    is_custom_interpolatable: bool,
74    engine_import_path: String,
75) -> proc_macro2::TokenStream {
76    let pascal_identifier = input_parsed.ident.to_string();
77    let is_enum = match &input_parsed.data {
78        Data::Enum(_) => true,
79        _ => false,
80    };
81
82    let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
83
84    let output = TemplateArgsDerivePax {
85        args_full_component: None,
86        args_primitive: None,
87        args_struct_only_component: Some(ArgsStructOnlyComponent {}),
88
89        pascal_identifier: pascal_identifier.clone(),
90        internal_definitions,
91        cargo_dir: std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "".into()),
92        is_root_crate: is_root_crate(),
93        is_custom_interpolatable,
94        _is_enum: is_enum,
95        engine_import_path,
96    }
97    .render_once()
98    .unwrap()
99    .to_string();
100
101    TokenStream::from_str(&output).unwrap().into()
102}
103
104/// Returns the type associated with a field, as well as a flag describing whether the property
105/// type is wrapped in Property<T>
106fn get_field_type(f: &Field) -> Option<(Type, bool)> {
107    let mut ret = None;
108    if let Type::Path(tp) = &f.ty {
109        match tp.qself {
110            None => {
111                tp.path.segments.iter().for_each(|ps| {
112                    //Only generate parsing logic for types wrapped in `Property<>`
113                    if ps.ident.to_string().ends_with("Property") {
114                        if let PathArguments::AngleBracketed(abga) = &ps.arguments {
115                            abga.args.iter().for_each(|abgaa| {
116                                if let GenericArgument::Type(gat) = abgaa {
117                                    ret = Some((gat.to_owned(), true));
118                                }
119                            })
120                        }
121                    }
122                });
123                if ret.is_none() {
124                    //ret is still None, so we will assume this is a simple type and pass it forward
125                    ret = Some((f.ty.to_owned(), false));
126                }
127            }
128            _ => {}
129        };
130    }
131    ret
132}
133
134/// Break apart a raw Property inner type (`T<K>` for `Property<T<K>>`):
135/// into a list of `rustc` resolvable identifiers, possible namespace-nested,
136/// which may be appended with `::get_type_id(...)` for dynamic analysis.
137/// For example: `K` and `T::<K>`, which become `K::get_type_id(...)` and `T::<K>::get_type_id(...)`.
138/// This is used to bridge from static to dynamic analysis, parse-time "reflection,"
139/// so that the Pax compiler can resolve fully qualified paths.
140fn get_scoped_resolvable_types(t: &Type) -> (Vec<String>, String) {
141    let mut accum: Vec<String> = vec![];
142    recurse_get_scoped_resolvable_types(t, &mut accum);
143
144    //the recursion above was post-order, so we will assume
145    //the final element is root
146    let root_scoped_resolvable_type = accum.get(accum.len() - 1).unwrap().clone();
147
148    (accum, root_scoped_resolvable_type)
149}
150
151fn recurse_get_scoped_resolvable_types(t: &Type, accum: &mut Vec<String>) {
152    match t {
153        Type::Path(tp) => {
154            match tp.qself {
155                None => {
156                    let mut accumulated_scoped_resolvable_type = "".to_string();
157                    tp.path.segments.iter().for_each(|ps| {
158                        match &ps.arguments {
159                            PathArguments::AngleBracketed(abga) => {
160                                if accumulated_scoped_resolvable_type.ne("") {
161                                    accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
162                                }
163                                let ident = ps.ident.to_token_stream().to_string();
164                                let turbofish_contents = ps.to_token_stream()
165                                    .to_string()
166                                    .replacen(&ident, "", 1)
167                                    .replace(" ", "");
168
169                                accumulated_scoped_resolvable_type =
170                                    accumulated_scoped_resolvable_type.clone() +
171                                        &ident +
172                                        "::" +
173                                        &turbofish_contents;
174
175                                abga.args.iter().for_each(|abgaa| {
176                                    match abgaa {
177                                        GenericArgument::Type(gat) => {
178                                            //break apart, for example, `Vec` from `Vec<(usize, Size)` >
179                                            recurse_get_scoped_resolvable_types(gat, accum);
180                                        },
181                                        //FUTURE: _might_ need to extract and deal with lifetimes, most notably where the "full string type" is used.
182                                        //      May be a non-issue, but this is where that data would need to be extracted.
183                                        //      Finally: might want to choose whether to require that any lifetimes used in Pax `Property<...>` are compatible with `'static`
184                                        _ => { }
185                                    };
186                                })
187                            },
188                            PathArguments::Parenthesized(_) => {unimplemented!("Parenthesized path arguments (for example, Fn types) not yet supported inside Pax `Property<...>`")},
189                            PathArguments::None => {
190                                //PathSegments without Args are vanilla segments, like
191                                //`std` or `collections`.  While visiting path segments, assemble our
192                                //accumulated_scoped_resolvable_type
193                                if accumulated_scoped_resolvable_type.ne("") {
194                                    accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
195                                }
196                                accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + &ps.to_token_stream().to_string();
197                            }
198                        }
199                    });
200
201                    accum.push(accumulated_scoped_resolvable_type);
202                }
203                _ => {
204                    unimplemented!("Self-types not yet supported with Pax `Property<...>`")
205                }
206            }
207        }
208        //For example, the contained tuple: `Property<(usize, Vec<String>)>`
209        Type::Tuple(t) => {
210            t.elems.iter().for_each(|tuple_elem| {
211                recurse_get_scoped_resolvable_types(tuple_elem, accum);
212            });
213        }
214        _ => {
215            unimplemented!("Unsupported Type::Path {}", t.to_token_stream().to_string());
216        }
217    }
218}
219
220fn index_to_ascii_lowercase(index: usize) -> char {
221    (b'a' + (index as u8)) as char
222}
223
224fn get_internal_definitions_from_tokens(data: &Data) -> InternalDefinitions {
225    let ret = match data {
226        Data::Struct(ref data) => {
227            match data.fields {
228                Fields::Named(ref fields) => {
229                    let mut spds = vec![];
230                    fields.named.iter().for_each(|f| {
231                        let field_name = f.ident.as_ref().unwrap();
232                        let _field_type = match get_field_type(f) {
233                            None => { /* noop */ }
234                            Some(ty) => {
235                                let type_name = quote!(#(ty.0)).to_string().replace(" ", "");
236
237                                let (scoped_resolvable_types, root_scoped_resolvable_type) =
238                                    get_scoped_resolvable_types(&ty.0);
239                                let pascal_identifier =
240                                    type_name.split("::").last().unwrap().to_string();
241                                spds.push(StaticPropertyDefinition {
242                                    original_type: type_name,
243                                    field_name: quote!(#field_name).to_string(),
244                                    scoped_resolvable_types,
245                                    root_scoped_resolvable_type,
246                                    pascal_identifier,
247                                    is_property_wrapped: ty.1,
248                                    is_enum: false,
249                                })
250                            }
251                        };
252                    });
253                    InternalDefinitions::Struct(spds)
254                }
255                _ => {
256                    unimplemented!("Pax may only be attached to `struct`s with named fields");
257                }
258            }
259        }
260        Data::Enum(ref data) => {
261            let mut evds = vec![];
262            data.variants.iter().for_each(|variant| {
263                let variant_name = variant.ident.to_string();
264                let mut variant_fields = vec![];
265                for (i, f) in variant.fields.iter().enumerate() {
266                    if let Some(ty) = get_field_type(f) {
267                        let original_type = quote!(#(ty.0)).to_string().replace(" ", "");
268                        let (scoped_resolvable_types, root_scoped_resolvable_type) =
269                            get_scoped_resolvable_types(&ty.0);
270                        let pascal_identifier =
271                            original_type.split("::").last().unwrap().to_string();
272                        variant_fields.push(StaticPropertyDefinition {
273                            original_type,
274                            field_name: index_to_ascii_lowercase(i).to_string(),
275                            scoped_resolvable_types,
276                            root_scoped_resolvable_type,
277                            pascal_identifier,
278                            is_property_wrapped: ty.1,
279                            is_enum: true,
280                        })
281                    }
282                }
283                evds.push(EnumVariantDefinition {
284                    variant_name,
285                    variant_fields,
286                });
287            });
288
289            InternalDefinitions::Enum(evds)
290        }
291
292        _ => {
293            unreachable!("Pax may only be attached to `struct`s")
294        }
295    };
296
297    ret
298}
299
300/* Context:
301[ ] Issue: we are including cartridge.partial.rs across every #[main], which e.g. causes build of pax-designer to fail
302        when running Fireworks.
303
304        Drafted solution:
305            [ ] detect whether we are in the root crate of this build.
306                [ ] might be able to store a static mutable Option<root_crate_pkg_name>, a write-once-read-many (WORM) signal to the rest of the build.
307            [ ] in the stpl template, check this signal and only include the partial if we are in the root crate.
308                [-] This might be fragile if somehow different versions of pax-macro are included in a build (is that possible or does cargo prevent it?) Answer: cargo prevents it.
309 */
310
311// Task at hand: [ ] detect whether we are in the root crate of this build.
312//                 [ ] might be able to store a static mutable Option<root_crate_pkg_name>, a write-once-read-many (WORM) signal to the rest of the build.
313
314//I should set this in the pax-macro crate, and then check it in the stpl template.
315//How can I access that env value, correctly reflecting the package being built (instead of pax-macro, this package) ?
316// [ ] I could set it in the build script, but that would require the user to add a build script to their project.
317// [ ] I could set it in the pax-macro crate, but that would require the user to include pax-macro in their project.
318//     This is okay -- pax-macro is available in the workspace, so it's not a big deal.
319// To verify: what snippet of code will read the env value and set the static mutable variable?
320// ```
321// let root_crate_pkg_name = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
322// if let None = unsafe { ROOT_CRATE_PKG_NAME } {
323//      unsafe { ROOT_CRATE_PKG_NAME = Some(root_crate_pkg_name); }
324// }
325// ```
326// And to doubly verify: this first time this is run, CARGO_PKG_NAME should be the root crate being built?
327// [ ] I should add a println! to the build script to verify this.
328
329fn pax_full_component(
330    raw_pax: String,
331    input_parsed: &DeriveInput,
332    is_main_component: bool,
333    include_fix: Option<TokenStream>,
334    is_custom_interpolatable: bool,
335    associated_pax_file_path: Option<PathBuf>,
336    engine_import_path: String,
337) -> proc_macro2::TokenStream {
338    let pascal_identifier = input_parsed.ident.to_string();
339    let is_enum = match &input_parsed.data {
340        Data::Enum(_) => true,
341        _ => false,
342    };
343
344    let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
345
346    let mut template_dependencies = vec![];
347    let mut error_message: Option<String> = None;
348
349    match parsing::parse_pascal_identifiers_from_component_definition_string(&raw_pax) {
350        Ok(deps) => {
351            template_dependencies = deps;
352        }
353        Err(err) => {
354            error_message = Some(err);
355        }
356    }
357
358    // Add BlankComponent to template_dependencies so it's guaranteed to be included in the PaxManifest
359    if is_main_component {
360        template_dependencies.push("BlankComponent".to_string());
361    }
362
363    let pax_dir: Option<PathBuf> = option_env!("PAX_DIR")
364        // The \\?\ prefix in Windows paths is the Win32 file namespace prefix.
365        // Needs to be removed to properly check if start matches below.
366        .map(|v| v.trim_start_matches("\\\\?\\"))
367        .map(|e| PathBuf::from(e));
368    let cartridge_snippet = if let Some(pax_dir) = pax_dir {
369        let current_manifest_dir = env::var("CARGO_MANIFEST_DIR")
370            .map(PathBuf::from)
371            .unwrap_or_else(|_| ".".into());
372        if pax_dir.starts_with(&current_manifest_dir) {
373            let cartridge_path = pax_dir.join("cartridge.partial.rs");
374            fs::read_to_string(&cartridge_path).unwrap()
375        } else {
376            "".to_string()
377        }
378    } else {
379        "".to_string()
380    };
381    let output = TemplateArgsDerivePax {
382        args_primitive: None,
383        args_struct_only_component: None,
384        args_full_component: Some(ArgsFullComponent {
385            is_main_component,
386            raw_pax,
387            template_dependencies,
388            cartridge_snippet,
389            associated_pax_file_path,
390            error_message,
391        }),
392        pascal_identifier,
393        internal_definitions,
394        cargo_dir: std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "".into()),
395        is_root_crate: is_root_crate(),
396        is_custom_interpolatable,
397        _is_enum: is_enum,
398        engine_import_path,
399    }
400    .render_once()
401    .unwrap()
402    .to_string();
403
404    let ret = TokenStream::from_str(&output).unwrap().into();
405    if !include_fix.is_none() {
406        quote! {
407            #include_fix
408            #ret
409        }
410    } else {
411        ret
412    }
413    .into()
414}
415
416struct Config {
417    is_main_component: bool,
418    file_path: Option<String>,
419    inlined_contents: Option<String>,
420    custom_values: Option<Vec<String>>,
421    engine_import_path: Option<String>,
422    primitive_instance_import_path: Option<String>,
423    is_primitive: bool,
424    has_helpers: bool,
425}
426
427fn parse_config(attrs: &mut Vec<syn::Attribute>) -> Config {
428    let mut config = Config {
429        is_main_component: false,
430        file_path: None,
431        inlined_contents: None,
432        custom_values: None,
433        primitive_instance_import_path: None,
434        engine_import_path: None,
435        is_primitive: false,
436        has_helpers: false,
437    };
438
439    // iterate through `derive macro helper attributes` to gather config & args
440    // remove the ones we use, don't remove the ones we don't
441    attrs.retain(|attr| {
442        match attr.path.get_ident() {
443            Some(s) if s == "file" => {
444                if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
445                    if let Some(nested_meta) = meta_list.nested.first() {
446                        if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
447                            config.file_path = Some(file_str.value());
448                            return false;
449                        }
450                    }
451                }
452            }
453            Some(s) if s == "engine_import_path" => {
454                if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
455                    if let Some(nested_meta) = meta_list.nested.first() {
456                        if let syn::NestedMeta::Lit(Lit::Str(engine_import_path)) = nested_meta {
457                            config.engine_import_path = Some(engine_import_path.value());
458                            return false;
459                        }
460                    }
461                }
462            }
463            Some(s) if s == "primitive" => {
464                if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
465                    if let Some(nested_meta) = meta_list.nested.first() {
466                        if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
467                            config.primitive_instance_import_path = Some(file_str.value());
468                            config.is_primitive = true;
469                            return false;
470                        }
471                    }
472                }
473            }
474            Some(s) if s == "inlined" => {
475                let tokens = attr.tokens.clone();
476                let mut content = proc_macro2::TokenStream::new();
477
478                for token in tokens {
479                    if let proc_macro2::TokenTree::Group(group) = token {
480                        if group.delimiter() == proc_macro2::Delimiter::Parenthesis {
481                            content.extend(group.stream());
482                        }
483                    }
484                }
485
486                if !content.is_empty() {
487                    config.inlined_contents = Some(content.to_string());
488                    return false;
489                }
490            }
491            Some(s) if s == "has_helpers" => {
492                config.has_helpers = true;
493                return false;
494            }
495            _ => {
496                if let Ok(Meta::Path(path)) = attr.parse_meta() {
497                    if path.is_ident("main") {
498                        config.is_main_component = true;
499                        return false;
500                    }
501                } else if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
502                    if meta_list.path.is_ident("custom") {
503                        let values: Vec<String> = meta_list
504                            .nested
505                            .into_iter()
506                            .filter_map(|nested_meta| {
507                                if let syn::NestedMeta::Meta(Meta::Path(path)) = nested_meta {
508                                    path.get_ident().map(|ident| ident.to_string())
509                                } else {
510                                    None
511                                }
512                            })
513                            .collect();
514                        config.custom_values = Some(values);
515                        return false;
516                    }
517                }
518            }
519        }
520        true
521    });
522
523    config
524}
525
526fn validate_config(
527    input: &syn::DeriveInput,
528    config: &Config,
529) -> Result<(), proc_macro::TokenStream> {
530    if config.file_path.is_some() && config.inlined_contents.is_some() {
531        return Err(syn::Error::new_spanned(
532            input.ident.clone(),
533            "`#[file(...)]` and `#[inlined(...)]` attributes cannot be used together",
534        )
535        .to_compile_error()
536        .into());
537    }
538    if config.file_path.is_none() && config.inlined_contents.is_none() && config.is_main_component {
539        return Err(syn::Error::new_spanned(
540            input.ident.clone(),
541            "Main (application-root) components must specify either a Pax file or inlined Pax content, e.g. #[file(\"some-file.pax\")] or #[inlined(<SomePax />)]",
542        )
543        .to_compile_error()
544        .into());
545    }
546    if config.is_primitive && (config.file_path.is_some() || config.inlined_contents.is_some()) {
547        const ERR: &str = "Primitives cannot have attached templates. Instead, specify a fully qualified Rust import path pointing to the `impl RenderNode` struct for this primitive.";
548        return Err(syn::Error::new_spanned(input.ident.clone(), ERR)
549            .to_compile_error()
550            .into());
551    }
552    Ok(())
553}
554
555#[proc_macro_attribute]
556pub fn pax(
557    _args: proc_macro::TokenStream,
558    input: proc_macro::TokenStream,
559) -> proc_macro::TokenStream {
560    let mut input = parse_macro_input!(input as DeriveInput);
561
562    let pascal_identifier = input.ident.to_string();
563    let config = parse_config(&mut input.attrs);
564    validate_config(&input, &config).unwrap();
565
566    let mut trait_impls = vec!["Clone", "Default", "Serialize", "Deserialize", "Debug"];
567
568    let mut is_custom_interpolatable = false;
569
570    let engine_import_path = match config.engine_import_path {
571        Some(prefix) => prefix,
572        None => "pax_kit::pax_engine".to_string(),
573    };
574
575    //wipe out the above derives if `#[custom(...)]` attrs are set
576    if let Some(custom) = config.custom_values {
577        let custom_str: Vec<&str> = custom.iter().map(String::as_str).collect();
578        trait_impls.retain(|v| !custom_str.contains(v));
579
580        if custom.contains(&"Interpolatable".to_string()) {
581            is_custom_interpolatable = true;
582        }
583    }
584
585    let is_pax_file = config.file_path.is_some();
586    let is_pax_inlined = config.inlined_contents.is_some();
587
588    let appended_tokens = if is_pax_file {
589        let file_name = config.file_path.unwrap();
590
591        let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
592
593        let path = if Path::new(&root).join(&file_name).exists() {
594            Path::new(&root).join(&file_name)
595        } else {
596            Path::new(&root).join("src/").join(&file_name)
597        };
598
599        // generate_include to watch for changes in specified file, ensuring macro is re-evaluated when file changes
600        let name = Ident::new(&pascal_identifier, Span::call_site());
601        let include_fix = generate_include(&name, &path);
602        let associated_pax_file = Some(path.clone());
603        let file = File::open(path);
604        let mut content = String::new();
605        let _ = file.unwrap().read_to_string(&mut content);
606        pax_full_component(
607            content,
608            &input,
609            config.is_main_component,
610            Some(include_fix),
611            is_custom_interpolatable,
612            associated_pax_file,
613            engine_import_path,
614        )
615    } else if is_pax_inlined {
616        let contents = config.inlined_contents.unwrap();
617
618        pax_full_component(
619            contents.to_owned(),
620            &input,
621            config.is_main_component,
622            None,
623            is_custom_interpolatable,
624            None,
625            engine_import_path,
626        )
627    } else if config.is_primitive {
628        pax_primitive(
629            &input,
630            config.primitive_instance_import_path.unwrap(),
631            is_custom_interpolatable,
632            engine_import_path,
633        )
634    } else {
635        pax_struct_only_component(&input, is_custom_interpolatable, engine_import_path)
636    };
637
638    let derives: proc_macro2::TokenStream = trait_impls
639        .into_iter()
640        .flat_map(|ident| {
641            let syn_ident = syn::Ident::new(ident, Span::call_site());
642            if ["Serialize", "Deserialize"].contains(&ident) {
643                // fully qualify serde dependencies
644                quote! {pax_engine::serde::#syn_ident,}
645            } else {
646                quote! {#syn_ident,}
647            }
648        })
649        .collect();
650
651    let ident = &input.ident;
652    let helper_functions_impl = if !config.has_helpers {
653        quote! {
654            impl pax_engine::api::HelperFunctions for #ident {
655                fn register_all_functions() {
656                    // Do nothing
657                }
658            }
659        }
660    } else {
661        quote! {}
662    };
663
664    let output = quote! {
665        // TODO make this value represented in PaxValue instead (map of properties), and impl to/from that value
666        impl pax_engine::api::ImplToFromPaxAny for #ident {}
667
668        #[derive(#derives)]
669        #[serde(crate = "pax_engine::serde")]
670        #input
671        #appended_tokens
672
673        #helper_functions_impl
674    };
675    output.into()
676}
677
678// Needed because Cargo wouldn't otherwise watch for changes in pax files.
679// By include_str!ing the file contents,
680// (Trick borrowed from Pest: github.com/pest-parser/pest)
681fn generate_include(name: &Ident, path: &PathBuf) -> TokenStream {
682    let const_name = Ident::new(&format!("_PAX_FILE_{}", name), Span::call_site());
683    let path_str = path.to_str().expect("expected non-unicode path");
684    quote! {
685        #[allow(non_upper_case_globals)]
686        const #const_name: &'static str = include_str!(#path_str);
687    }
688}
689#[proc_macro_attribute]
690pub fn helpers(
691    _attr: proc_macro::TokenStream,
692    item: proc_macro::TokenStream,
693) -> proc_macro::TokenStream {
694    let input = parse_macro_input!(item as ItemImpl);
695    let struct_name = &input.self_ty;
696
697    let mut register_functions = vec![];
698
699    for item in input.items.iter() {
700        if let ImplItem::Method(method) = item {
701            let func_name = &method.sig.ident;
702
703            // Make sure it's associated function (doesn't use `self`)
704            if method
705                .sig
706                .inputs
707                .iter()
708                .any(|arg| matches!(arg, FnArg::Receiver(_)))
709            {
710                return syn::Error::new_spanned(
711                    method.sig.clone(),
712                    "Helpers macro can only be used on associated functions (methods that don't take self)",
713                )
714                .to_compile_error()
715                .into();
716            }
717
718            let arg_count = method.sig.inputs.len();
719            let param_checks = generate_param_checks(&method.sig.inputs);
720            let func_call = generate_function_call(&method.sig, struct_name);
721
722            register_functions.push(quote! {
723                register_function(
724                    stringify!(#struct_name).to_string(),
725                    stringify!(#func_name).to_string(),
726                    Arc::new(move |args: Vec<PaxValue>| -> Result<PaxValue, String> {
727                        if args.len() != #arg_count {
728                            return Err(format!("Expected {} arguments for function {}", #arg_count, stringify!(#func_name)));
729                        }
730                        #param_checks
731                        #func_call
732                    })
733                );
734            });
735        }
736    }
737
738    let expanded = quote! {
739        #input
740
741        impl pax_engine::api::HelperFunctions for #struct_name {
742            fn register_all_functions() {
743                use std::sync::Arc;
744                use pax_engine::api::{PaxValue, register_function};
745                #(#register_functions)*
746            }
747        }
748    };
749
750    expanded.into()
751}
752
753fn generate_param_checks(inputs: &Punctuated<FnArg, Token![,]>) -> proc_macro2::TokenStream {
754    let checks = inputs.iter().enumerate().filter_map(|(i, arg)| {
755        if let FnArg::Typed(PatType { ty, .. }) = arg {
756            let ty_string = quote!(#ty).to_string();
757            let arg_name = format_ident!("arg_{}", i);
758            Some(quote! {
759                let #arg_name = <#ty as pax_engine::api::CoercionRules>::try_coerce(args[#i].clone())
760                    .map_err(|_| format!("Failed to coerce argument {} to {}", #i, #ty_string))?;
761            })
762        } else {
763            None
764        }
765    });
766
767    quote! { #(#checks)* }
768}
769
770fn generate_function_call(sig: &Signature, struct_name: &Box<Type>) -> proc_macro2::TokenStream {
771    let func_name = &sig.ident;
772    let args = sig.inputs.iter().enumerate().filter_map(|(i, arg)| {
773        if let FnArg::Typed(_) = arg {
774            let arg_name = format_ident!("arg_{}", i);
775            Some(quote! { #arg_name })
776        } else {
777            None
778        }
779    });
780
781    quote! {
782        let result = #struct_name::#func_name(#(#args),*);
783        Ok(<_ as pax_engine::api::ToPaxValue>::to_pax_value(result))
784    }
785}