1extern crate proc_macro;
2extern crate proc_macro2;
3mod launch;
4mod root_crate;
5mod source;
6
7#[proc_macro]
15pub fn init(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
16 launch::init(input, false)
17}
18
19#[proc_macro]
24pub fn init_with(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
25 launch::init(input, true)
26}
27
28#[doc(hidden)]
29#[proc_macro]
30pub fn __pax_launch(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
31 launch::expand(input)
32}
33mod templating;
34use std::fs::File;
35use std::io::Read;
36use std::path::Path;
37use std::str::FromStr;
38use std::{env, path::PathBuf}; use proc_macro2::{Ident, Span, TokenStream};
41use quote::{format_ident, quote, ToTokens};
42
43use syn::punctuated::Punctuated;
44use templating::{
45 EnumVariantDefinition, InternalDefinitions, StaticPropertyDefinition, TemplateArgsCartridge,
46 TemplateArgsDerivePax, TemplateBuildConfig,
47};
48
49use sailfish::TemplateOnce;
50
51fn is_root_crate() -> bool {
52 if env_flag_explicitly_disabled("PAX_BUILD_CARTRIDGE") {
53 return false;
54 }
55 root_crate::is_application_crate(
56 env::var_os("PAX_CARTRIDGE_MANIFEST_DIR")
57 .as_deref()
58 .map(Path::new),
59 env::var_os("PAX_DIR").as_deref().map(Path::new),
60 &env::var_os("CARGO_MANIFEST_DIR")
61 .map(PathBuf::from)
62 .unwrap_or_default(),
63 env::var_os("CARGO_PRIMARY_PACKAGE").is_some(),
64 )
65}
66
67#[proc_macro]
89pub fn runnable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
90 source::runnable(input)
91}
92
93fn render_cartridge(root: &str, initializer: Option<String>, engine: &str) -> String {
94 render_generated_cartridge(
95 root,
96 initializer,
97 engine,
98 String::new(),
99 template_build_config(),
100 )
101}
102
103fn render_generated_cartridge(
104 root: &str,
105 initializer: Option<String>,
106 engine: &str,
107 generated: String,
108 build_config: TemplateBuildConfig,
109) -> String {
110 let initializer = initializer.unwrap_or_else(|| format!("|_| Ok(<{root}>::default())"));
111 let mut output = format!(
114 r#"
115 #[allow(unused_imports)]
116 use {root} as __PaxSelectedRoot;
117 #[allow(dead_code)]
118 fn __pax_initialize(context: &{engine}::api::InitContext) -> {engine}::api::AppResult<__PaxSelectedRoot> {{
119 ({initializer})(context)
120 }}
121 const _: Option<&str> = option_env!("PAX_DIR");
122 const _: Option<&str> = option_env!("PAX_CARTRIDGE_MANIFEST_DIR");
123 const _: Option<&str> = option_env!("PAX_BUILD_CARTRIDGE");
124 const _: Option<&str> = option_env!("PAX_BUILD_TARGET");
125 const _: Option<&str> = option_env!("PAX_BUILD_DESIGNTIME");
126 const _: Option<&str> = option_env!("CARGO_PRIMARY_PACKAGE");
127 "#
128 );
129 if generated.is_empty() && !is_root_crate() {
130 return output;
131 }
132 if !(build_config.web || build_config.macos || build_config.ios || build_config.ipados) {
133 return output;
134 }
135 let snippet = if cfg!(feature = "designtime")
136 && env_flag_explicitly_disabled("PAX_BUILD_DESIGNTIME")
137 {
138 "compile_error!(\"Pax release builds cannot include the `pax-engine/designtime` feature. Remove the transitive designtime feature activation or build this app in debug mode.\");".into()
139 } else if !generated.is_empty() {
140 generated
141 } else if let Some(pax_dir) = env::var_os("PAX_DIR") {
142 let path = PathBuf::from(pax_dir).join("cartridge.partial.rs");
143 format!("include!({:?});", path.to_string_lossy())
144 } else {
145 "compile_error!(\"PAX_DIR was not set for the selected Pax cartridge. Build the application through pax-cli to generate its cartridge.\");".into()
146 };
147 let adapter = TemplateArgsCartridge {
148 catalog: false,
149 web_entrypoint: "init_web".into(),
150 root_path: "super::__PaxSelectedRoot".into(),
151 initializer: "super::__pax_initialize".into(),
152 cartridge_snippet: snippet,
153 engine_import_path: engine.into(),
154 build_config,
155 }
156 .render_once()
157 .unwrap();
158 let facade_import = if engine.replace(' ', "") == "pax_kit::pax_engine" {
159 "use pax_kit::*;"
160 } else {
161 ""
162 };
163 output.push_str(&format!(
164 r#"
165 #[allow(unused_imports)]
166 mod __pax_cartridge {{
167 use super::*;
168 {facade_import}
169 use {engine}::*;
170 use {engine}::api::*;
171 {adapter}
172 }}
173 #[allow(unused_imports)]
174 pub use __pax_cartridge::*;
175 "#
176 ));
177 output
178}
179
180fn env_flag(name: &str) -> bool {
181 env::var(name)
182 .map(|value| {
183 let normalized = value.to_ascii_lowercase();
184 matches!(normalized.as_str(), "1" | "true" | "yes" | "on")
185 })
186 .unwrap_or(false)
187}
188
189fn env_flag_explicitly_disabled(name: &str) -> bool {
190 env::var(name)
191 .map(|value| {
192 let normalized = value.to_ascii_lowercase();
193 matches!(normalized.as_str(), "0" | "false" | "no" | "off")
194 })
195 .unwrap_or(false)
196}
197
198fn cargo_feature_enabled(feature: &str) -> bool {
199 env::var_os(format!(
200 "CARGO_FEATURE_{}",
201 feature.replace('-', "_").to_ascii_uppercase()
202 ))
203 .is_some()
204}
205
206pub(crate) fn designtime_enabled() -> bool {
207 match std::env::var("PAX_BUILD_DESIGNTIME") {
208 Ok(_) => env_flag("PAX_BUILD_DESIGNTIME"),
209 Err(_) => cfg!(feature = "designtime"),
210 }
211}
212
213fn template_build_config() -> TemplateBuildConfig {
214 let pax_build_target = env::var("PAX_BUILD_TARGET").unwrap_or_default();
215 let designtime = designtime_enabled();
216
217 TemplateBuildConfig {
218 web: pax_build_target == "web" || cargo_feature_enabled("web"),
219 macos: pax_build_target == "macos" || cargo_feature_enabled("macos"),
220 ios: pax_build_target == "ios" || cargo_feature_enabled("ios"),
221 ipados: pax_build_target == "ipados",
222 designtime,
223 }
224}
225
226use syn::{
227 parse_macro_input, Data, DeriveInput, Field, Fields, FnArg, GenericArgument, ImplItem,
228 ImplItemMethod, ItemImpl, Lit, Meta, PatType, PathArguments, Signature, Token, Type,
229};
230
231fn pax_primitive(
232 input_parsed: &DeriveInput,
233 _primitive_instance_import_path: String,
234 is_custom_interpolatable: bool,
235 is_custom_coercion_rules: bool,
236 can_derive_identity_roundtrip: bool,
237 engine_import_path: String,
238) -> proc_macro2::TokenStream {
239 let _original_tokens = quote! { #input_parsed }.to_string();
240 let pascal_identifier = input_parsed.ident.to_string();
241 let is_enum = match &input_parsed.data {
242 Data::Enum(_) => true,
243 _ => false,
244 };
245
246 let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
247
248 let output = TemplateArgsDerivePax {
249 cartridge: String::new(),
250 internal_definitions,
251 pascal_identifier,
252 is_custom_interpolatable,
253 is_custom_coercion_rules,
254 can_derive_identity_roundtrip,
255 _is_enum: is_enum,
256 engine_import_path,
257 }
258 .render_once()
259 .unwrap()
260 .to_string();
261
262 TokenStream::from_str(&output).unwrap().into()
263}
264
265fn pax_struct_only_component(
266 input_parsed: &DeriveInput,
267 is_custom_interpolatable: bool,
268 is_custom_coercion_rules: bool,
269 can_derive_identity_roundtrip: bool,
270 engine_import_path: String,
271) -> proc_macro2::TokenStream {
272 let pascal_identifier = input_parsed.ident.to_string();
273 let is_enum = match &input_parsed.data {
274 Data::Enum(_) => true,
275 _ => false,
276 };
277
278 let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
279
280 let output = TemplateArgsDerivePax {
281 cartridge: String::new(),
282
283 pascal_identifier: pascal_identifier.clone(),
284 internal_definitions,
285 is_custom_interpolatable,
286 is_custom_coercion_rules,
287 can_derive_identity_roundtrip,
288 _is_enum: is_enum,
289 engine_import_path,
290 }
291 .render_once()
292 .unwrap()
293 .to_string();
294
295 TokenStream::from_str(&output).unwrap().into()
296}
297
298fn is_rust_field(field: &Field) -> bool {
301 field.attrs.iter().any(|attr| {
302 attr.path.is_ident("pax")
303 && attr
304 .parse_args::<syn::Ident>()
305 .map(|name| name == "rust")
306 .unwrap_or(false)
307 })
308}
309
310fn get_field_type(f: &Field) -> Option<(Type, bool)> {
311 if is_rust_field(f) {
312 return None;
313 }
314 let mut ret = None;
315 if let Type::Path(tp) = &f.ty {
316 match tp.qself {
317 None => {
318 tp.path.segments.iter().for_each(|ps| {
319 if ps.ident.to_string().ends_with("Property") {
321 if let PathArguments::AngleBracketed(abga) = &ps.arguments {
322 abga.args.iter().for_each(|abgaa| {
323 if let GenericArgument::Type(gat) = abgaa {
324 ret = Some((gat.to_owned(), true));
325 }
326 })
327 }
328 }
329 });
330 if ret.is_none() {
331 ret = Some((f.ty.to_owned(), false));
333 }
334 }
335 _ => {}
336 };
337 }
338 ret
339}
340
341fn get_scoped_resolvable_types(t: &Type) -> (Vec<String>, String) {
345 let mut accum: Vec<String> = vec![];
346 recurse_get_scoped_resolvable_types(t, &mut accum);
347
348 let root_scoped_resolvable_type = accum.get(accum.len() - 1).unwrap().clone();
351
352 (accum, root_scoped_resolvable_type)
353}
354
355fn recurse_get_scoped_resolvable_types(t: &Type, accum: &mut Vec<String>) {
356 match t {
357 Type::Path(tp) => {
358 match tp.qself {
359 None => {
360 let mut accumulated_scoped_resolvable_type = "".to_string();
361 tp.path.segments.iter().for_each(|ps| {
362 match &ps.arguments {
363 PathArguments::AngleBracketed(abga) => {
364 if accumulated_scoped_resolvable_type.ne("") {
365 accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
366 }
367 let ident = ps.ident.to_token_stream().to_string();
368 let turbofish_contents = ps.to_token_stream()
369 .to_string()
370 .replacen(&ident, "", 1)
371 .replace(" ", "");
372
373 accumulated_scoped_resolvable_type =
374 accumulated_scoped_resolvable_type.clone() +
375 &ident +
376 "::" +
377 &turbofish_contents;
378
379 abga.args.iter().for_each(|abgaa| {
380 match abgaa {
381 GenericArgument::Type(gat) => {
382 recurse_get_scoped_resolvable_types(gat, accum);
384 },
385 _ => { }
389 };
390 })
391 },
392 PathArguments::Parenthesized(_) => {unimplemented!("Parenthesized path arguments (for example, Fn types) not yet supported inside Pax `Property<...>`")},
393 PathArguments::None => {
394 if accumulated_scoped_resolvable_type.ne("") {
398 accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
399 }
400 accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + &ps.to_token_stream().to_string();
401 }
402 }
403 });
404
405 accum.push(accumulated_scoped_resolvable_type);
406 }
407 _ => {
408 unimplemented!("Self-types not yet supported with Pax `Property<...>`")
409 }
410 }
411 }
412 Type::Tuple(t) => {
414 t.elems.iter().for_each(|tuple_elem| {
415 recurse_get_scoped_resolvable_types(tuple_elem, accum);
416 });
417 }
418 _ => {
419 unimplemented!("Unsupported Type::Path {}", t.to_token_stream().to_string());
420 }
421 }
422}
423
424fn index_to_ascii_lowercase(index: usize) -> char {
425 (b'a' + (index as u8)) as char
426}
427
428fn get_internal_definitions_from_tokens(data: &Data) -> InternalDefinitions {
429 let ret = match data {
430 Data::Struct(ref data) => {
431 match data.fields {
432 Fields::Named(ref fields) => {
433 let mut spds = vec![];
434 fields.named.iter().for_each(|f| {
435 let field_name = f.ident.as_ref().unwrap();
436 let _field_type = match get_field_type(f) {
437 None => { }
438 Some(ty) => {
439 let type_name = quote!(#(ty.0)).to_string().replace(" ", "");
440
441 let (scoped_resolvable_types, root_scoped_resolvable_type) =
442 get_scoped_resolvable_types(&ty.0);
443 let pascal_identifier =
444 type_name.split("::").last().unwrap().to_string();
445 spds.push(StaticPropertyDefinition {
446 original_type: type_name,
447 field_name: quote!(#field_name).to_string(),
448 scoped_resolvable_types,
449 root_scoped_resolvable_type,
450 pascal_identifier,
451 is_property_wrapped: ty.1,
452 is_enum: false,
453 })
454 }
455 };
456 });
457 InternalDefinitions::Struct(spds)
458 }
459 _ => {
460 unimplemented!("Pax may only be attached to `struct`s with named fields");
461 }
462 }
463 }
464 Data::Enum(ref data) => {
465 let mut evds = vec![];
466 data.variants.iter().for_each(|variant| {
467 let variant_name = variant.ident.to_string();
468 let mut variant_fields = vec![];
469 for (i, f) in variant.fields.iter().enumerate() {
470 if let Some(ty) = get_field_type(f) {
471 let original_type = quote!(#(ty.0)).to_string().replace(" ", "");
472 let (scoped_resolvable_types, root_scoped_resolvable_type) =
473 get_scoped_resolvable_types(&ty.0);
474 let pascal_identifier =
475 original_type.split("::").last().unwrap().to_string();
476 variant_fields.push(StaticPropertyDefinition {
477 original_type,
478 field_name: index_to_ascii_lowercase(i).to_string(),
479 scoped_resolvable_types,
480 root_scoped_resolvable_type,
481 pascal_identifier,
482 is_property_wrapped: ty.1,
483 is_enum: true,
484 })
485 }
486 }
487 evds.push(EnumVariantDefinition {
488 variant_name,
489 variant_fields,
490 });
491 });
492
493 InternalDefinitions::Enum(evds)
494 }
495
496 _ => {
497 unreachable!("Pax may only be attached to `struct`s")
498 }
499 };
500
501 ret
502}
503
504fn pax_full_component(
505 _raw_pax: String,
506 input_parsed: &DeriveInput,
507 is_main_component: bool,
508 initializer: Option<String>,
509 include_fix: Option<TokenStream>,
510 is_custom_interpolatable: bool,
511 is_custom_coercion_rules: bool,
512 can_derive_identity_roundtrip: bool,
513 _associated_pax_file_path: Option<PathBuf>,
514 engine_import_path: String,
515) -> proc_macro2::TokenStream {
516 let pascal_identifier = input_parsed.ident.to_string();
517 let is_enum = match &input_parsed.data {
518 Data::Enum(_) => true,
519 _ => false,
520 };
521
522 let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
523
524 let cartridge = if is_main_component {
525 render_cartridge(&pascal_identifier, initializer, &engine_import_path)
526 } else {
527 String::new()
528 };
529 let output = TemplateArgsDerivePax {
530 cartridge,
531 pascal_identifier,
532 internal_definitions,
533 is_custom_interpolatable,
534 is_custom_coercion_rules,
535 can_derive_identity_roundtrip,
536 _is_enum: is_enum,
537 engine_import_path,
538 }
539 .render_once()
540 .unwrap()
541 .to_string();
542
543 let ret: proc_macro2::TokenStream = TokenStream::from_str(&output).unwrap().into();
544 if !include_fix.is_none() {
545 quote! {
546 #include_fix
547 #ret
548 }
549 } else {
550 ret
551 }
552 .into()
553}
554
555struct Config {
556 is_main_component: bool,
557 initializer: Option<String>,
558 file_path: Option<String>,
559 svg_path: Option<String>,
560 inlined_contents: Option<String>,
561 custom_values: Option<Vec<String>>,
562 engine_import_path: Option<String>,
563 primitive_instance_import_path: Option<String>,
564 is_primitive: bool,
565 has_helpers: bool,
566}
567
568fn parse_config(attrs: &mut Vec<syn::Attribute>) -> Config {
569 let mut config = Config {
570 is_main_component: false,
571 initializer: None,
572 file_path: None,
573 svg_path: None,
574 inlined_contents: None,
575 custom_values: None,
576 primitive_instance_import_path: None,
577 engine_import_path: None,
578 is_primitive: false,
579 has_helpers: false,
580 };
581
582 attrs.retain(|attr| {
585 match attr.path.get_ident() {
586 Some(s) if s == "init" => {
587 let path: syn::Path = attr
588 .parse_args()
589 .expect("#[init(path)] requires one Rust function path");
590 config.initializer = Some(quote! { #path }.to_string());
591 return false;
592 }
593 Some(s) if s == "file" => {
594 if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
595 if let Some(nested_meta) = meta_list.nested.first() {
596 if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
597 config.file_path = Some(file_str.value());
598 return false;
599 }
600 }
601 }
602 }
603 Some(s) if s == "svg" => {
604 if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
605 if let Some(nested_meta) = meta_list.nested.first() {
606 if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
607 config.svg_path = Some(file_str.value());
608 return false;
609 }
610 }
611 }
612 }
613 Some(s) if s == "engine_import_path" => {
614 if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
615 if let Some(nested_meta) = meta_list.nested.first() {
616 if let syn::NestedMeta::Lit(Lit::Str(engine_import_path)) = nested_meta {
617 config.engine_import_path = Some(engine_import_path.value());
618 return false;
619 }
620 }
621 }
622 }
623 Some(s) if s == "primitive" => {
624 if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
625 if let Some(nested_meta) = meta_list.nested.first() {
626 if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
627 config.primitive_instance_import_path = Some(file_str.value());
628 config.is_primitive = true;
629 return false;
630 }
631 }
632 }
633 }
634 Some(s) if s == "route_branch" => {
635 return false;
636 }
637 Some(s) if s == "inlined" => {
638 let tokens = attr.tokens.clone();
639 let mut content = proc_macro2::TokenStream::new();
640
641 for token in tokens {
642 if let proc_macro2::TokenTree::Group(group) = token {
643 if group.delimiter() == proc_macro2::Delimiter::Parenthesis {
644 content.extend(group.stream());
645 }
646 }
647 }
648
649 if !content.is_empty() {
650 config.inlined_contents = Some(content.to_string());
651 return false;
652 }
653 }
654 Some(s) if s == "has_helpers" => {
655 config.has_helpers = true;
656 return false;
657 }
658 _ => {
659 if let Ok(Meta::Path(path)) = attr.parse_meta() {
660 if path.is_ident("main") {
661 config.is_main_component = true;
662 return false;
663 }
664 } else if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
665 if meta_list.path.is_ident("custom") {
666 let values: Vec<String> = meta_list
667 .nested
668 .into_iter()
669 .filter_map(|nested_meta| {
670 if let syn::NestedMeta::Meta(Meta::Path(path)) = nested_meta {
671 path.get_ident().map(|ident| ident.to_string())
672 } else {
673 None
674 }
675 })
676 .collect();
677 config.custom_values = Some(values);
678 return false;
679 }
680 }
681 }
682 }
683 true
684 });
685
686 config
687}
688
689fn validate_config(
690 input: &syn::DeriveInput,
691 config: &Config,
692) -> Result<(), proc_macro::TokenStream> {
693 let template_source_count = [
694 config.file_path.is_some(),
695 config.svg_path.is_some(),
696 config.inlined_contents.is_some(),
697 ]
698 .into_iter()
699 .filter(|present| *present)
700 .count();
701 if template_source_count > 1 {
702 return Err(syn::Error::new_spanned(
703 input.ident.clone(),
704 "`#[file(...)]`, `#[svg(...)]`, and `#[inlined(...)]` attributes cannot be used together",
705 )
706 .to_compile_error()
707 .into());
708 }
709 if template_source_count == 0 && config.is_main_component {
710 return Err(syn::Error::new_spanned(
711 input.ident.clone(),
712 "Main (application-root) components must specify a Pax template source, e.g. #[file(\"some-file.pax\")], #[svg(\"some-file.svg\")], or #[inlined(<SomePax />)]",
713 )
714 .to_compile_error()
715 .into());
716 }
717 if config.is_primitive && template_source_count > 0 {
718 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.";
719 return Err(syn::Error::new_spanned(input.ident.clone(), ERR)
720 .to_compile_error()
721 .into());
722 }
723 Ok(())
724}
725
726#[proc_macro_attribute]
727pub fn pax(
728 args: proc_macro::TokenStream,
729 input: proc_macro::TokenStream,
730) -> proc_macro::TokenStream {
731 let _ = args;
732 source::pax(input)
733}
734
735#[doc(hidden)]
736#[proc_macro]
737pub fn __pax_resume(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
738 source::resume(input)
739}
740
741#[doc(hidden)]
742#[proc_macro_attribute]
743pub fn __pax_expand(
744 _args: proc_macro::TokenStream,
745 input: proc_macro::TokenStream,
746) -> proc_macro::TokenStream {
747 let mut input = parse_macro_input!(input as DeriveInput);
748
749 let pascal_identifier = input.ident.to_string();
750 if input
751 .attrs
752 .iter()
753 .any(|attr| attr.path.is_ident("application"))
754 {
755 return quote! { compile_error!("#[application(Type)] has been removed; use #[init(path)] with an ordinary fn(&InitContext) -> AppResult<Root>"); }.into();
756 }
757 let config = parse_config(&mut input.attrs);
758 if config.initializer.is_some() && !config.is_main_component {
759 return quote! { compile_error!("Move #[init(path)] into runnable! { root: YourRoot, init: path }, or use it with the legacy #[main] shorthand"); }
760 .into();
761 }
762 validate_config(&input, &config).unwrap();
763
764 let mut trait_impls = vec!["Clone", "Default", "Serialize", "Deserialize", "Debug"];
765
766 let mut is_custom_interpolatable = false;
767 let mut is_custom_coercion_rules = false;
768
769 let engine_import_path = match config.engine_import_path {
770 Some(prefix) => prefix,
771 None => "pax_kit::pax_engine".to_string(),
772 };
773
774 if let Some(custom) = config.custom_values {
776 let custom_str: Vec<&str> = custom.iter().map(String::as_str).collect();
777 trait_impls.retain(|v| !custom_str.contains(v));
778
779 if custom.contains(&"Interpolatable".to_string()) {
780 is_custom_interpolatable = true;
781 }
782 if custom.contains(&"CoercionRules".to_string()) {
783 is_custom_coercion_rules = true;
784 }
785 }
786
787 let all_fields_represented = match &input.data {
791 Data::Struct(data) => data.fields.iter().all(|f| get_field_type(f).is_some()),
792 Data::Enum(data) => data
793 .variants
794 .iter()
795 .all(|v| v.fields.iter().all(|f| get_field_type(f).is_some())),
796 _ => false,
797 };
798 let can_derive_identity_roundtrip = all_fields_represented && trait_impls.contains(&"Default");
799
800 let is_pax_file = config.file_path.is_some();
801 let is_pax_svg = config.svg_path.is_some();
802 let is_pax_inlined = config.inlined_contents.is_some();
803
804 let appended_tokens = if is_pax_file {
805 let file_name = config.file_path.unwrap();
806
807 let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
808
809 let path = if Path::new(&root).join(&file_name).exists() {
810 Path::new(&root).join(&file_name)
811 } else {
812 Path::new(&root).join("src/").join(&file_name)
813 };
814
815 let name = Ident::new(&pascal_identifier, Span::call_site());
817 let include_fix = generate_include(&name, &path);
818 let associated_pax_file = Some(path.clone());
819 let file = File::open(path);
820 let mut content = String::new();
821 let _ = file.unwrap().read_to_string(&mut content);
822 pax_full_component(
823 content,
824 &input,
825 config.is_main_component,
826 config.initializer.clone(),
827 Some(include_fix),
828 is_custom_interpolatable,
829 is_custom_coercion_rules,
830 can_derive_identity_roundtrip,
831 associated_pax_file,
832 engine_import_path,
833 )
834 } else if is_pax_svg {
835 let file_name = config.svg_path.unwrap();
836
837 let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
838
839 let path = if Path::new(&root).join(&file_name).exists() {
840 Path::new(&root).join(&file_name)
841 } else {
842 Path::new(&root).join("src/").join(&file_name)
843 };
844
845 let name = Ident::new(&pascal_identifier, Span::call_site());
847 let include_fix = generate_include(&name, &path);
848 pax_full_component(
849 String::new(),
850 &input,
851 config.is_main_component,
852 config.initializer.clone(),
853 Some(include_fix),
854 is_custom_interpolatable,
855 is_custom_coercion_rules,
856 can_derive_identity_roundtrip,
857 None,
858 engine_import_path,
859 )
860 } else if is_pax_inlined {
861 let contents = config.inlined_contents.unwrap();
862
863 pax_full_component(
864 contents.to_owned(),
865 &input,
866 config.is_main_component,
867 config.initializer.clone(),
868 None,
869 is_custom_interpolatable,
870 is_custom_coercion_rules,
871 can_derive_identity_roundtrip,
872 None,
873 engine_import_path,
874 )
875 } else if config.is_primitive {
876 pax_primitive(
877 &input,
878 config.primitive_instance_import_path.unwrap(),
879 is_custom_interpolatable,
880 is_custom_coercion_rules,
881 can_derive_identity_roundtrip,
882 engine_import_path,
883 )
884 } else {
885 pax_struct_only_component(
886 &input,
887 is_custom_interpolatable,
888 is_custom_coercion_rules,
889 can_derive_identity_roundtrip,
890 engine_import_path,
891 )
892 };
893
894 if let Data::Enum(data) = &input.data {
895 if data
896 .variants
897 .iter()
898 .flat_map(|v| v.fields.iter())
899 .any(|f| f.attrs.iter().any(|a| a.path.is_ident("pax")))
900 {
901 return quote! { compile_error!("#[pax(rust)] is supported on struct fields only"); }
902 .into();
903 }
904 }
905 if let Data::Struct(data) = &mut input.data {
907 for field in &mut data.fields {
908 if is_rust_field(field) {
909 field.attrs.retain(|attr| !attr.path.is_ident("pax"));
910 field.attrs.push(syn::parse_quote!(#[serde(skip)]));
911 } else if field.attrs.iter().any(|attr| attr.path.is_ident("pax")) {
912 return quote! { compile_error!("the field attribute must be #[pax(rust)]"); }
913 .into();
914 }
915 }
916 }
917 let derives: proc_macro2::TokenStream = trait_impls
918 .into_iter()
919 .flat_map(|ident| {
920 let syn_ident = syn::Ident::new(ident, Span::call_site());
921 if ["Serialize", "Deserialize"].contains(&ident) {
922 quote! {pax_engine::serde::#syn_ident,}
924 } else {
925 quote! {#syn_ident,}
926 }
927 })
928 .collect();
929
930 let ident = &input.ident;
931 let helper_functions_impl = if !config.has_helpers {
932 quote! {
933 impl pax_engine::api::HelperFunctions for #ident {
934 fn register_all_functions() {
935 }
937 }
938 }
939 } else {
940 quote! {}
941 };
942
943 let output = quote! {
944 impl pax_engine::api::ImplToFromPaxAny for #ident {}
946
947 #[derive(#derives)]
948 #[serde(crate = "pax_engine::serde")]
949 #input
950 #appended_tokens
951
952 #helper_functions_impl
953 };
954 output.into()
955}
956
957fn generate_include(name: &Ident, path: &PathBuf) -> TokenStream {
961 let const_name = Ident::new(&format!("_PAX_FILE_{}", name), Span::call_site());
962 let path_str = path.to_str().expect("expected non-unicode path");
963 quote! {
964 #[allow(non_upper_case_globals)]
965 const #const_name: &'static str = include_str!(#path_str);
966 }
967}
968#[proc_macro_attribute]
973pub fn helpers(
974 _attr: proc_macro::TokenStream,
975 item: proc_macro::TokenStream,
976) -> proc_macro::TokenStream {
977 let input = parse_macro_input!(item as ItemImpl);
978 expand_helpers(input)
979 .unwrap_or_else(syn::Error::into_compile_error)
980 .into()
981}
982
983fn public_associated_helpers(input: &ItemImpl) -> syn::Result<Vec<&ImplItemMethod>> {
984 input
985 .items
986 .iter()
987 .filter_map(|item| match item {
988 ImplItem::Method(method) if matches!(method.vis, syn::Visibility::Public(_)) => {
989 Some(method)
990 }
991 _ => None,
992 })
993 .map(|method| {
994 if method
995 .sig
996 .inputs
997 .iter()
998 .any(|arg| matches!(arg, FnArg::Receiver(_)))
999 {
1000 Err(syn::Error::new_spanned(
1001 method.sig.clone(),
1002 "Helpers macro can only be used on associated functions (methods that don't take self)",
1003 ))
1004 } else {
1005 Ok(method)
1006 }
1007 })
1008 .collect()
1009}
1010
1011fn expand_helpers(input: ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
1012 let struct_name = &input.self_ty;
1013
1014 let mut register_functions = vec![];
1015
1016 for method in public_associated_helpers(&input)? {
1017 let func_name = &method.sig.ident;
1018 let arg_count = method.sig.inputs.len();
1019 let param_checks = generate_param_checks(&method.sig.inputs);
1020 let func_call = generate_function_call(&method.sig, struct_name);
1021
1022 register_functions.push(quote! {
1023 register_function(
1024 stringify!(#struct_name).to_string(),
1025 stringify!(#func_name).to_string(),
1026 Arc::new(move |args: Vec<PaxValue>| -> Result<PaxValue, String> {
1027 if args.len() != #arg_count {
1028 return Err(format!("Expected {} arguments for function {}", #arg_count, stringify!(#func_name)));
1029 }
1030 #param_checks
1031 #func_call
1032 })
1033 );
1034 });
1035 }
1036
1037 let expanded = quote! {
1038 #input
1039
1040 impl pax_engine::api::HelperFunctions for #struct_name {
1041 fn register_all_functions() {
1042 use std::sync::Arc;
1043 use pax_engine::api::{PaxValue, register_function};
1044 #(#register_functions)*
1045 }
1046 }
1047 };
1048
1049 Ok(expanded)
1050}
1051
1052#[cfg(test)]
1053mod helper_visibility_tests {
1054 use super::*;
1055
1056 #[test]
1057 fn only_public_associated_functions_are_registered() {
1058 let input: ItemImpl = syn::parse_quote! {
1059 impl Example {
1060 pub fn visible(value: f64) -> f64 { value }
1061 fn private(value: f64) -> f64 { value }
1062 pub(crate) fn crate_visible(value: f64) -> f64 { value }
1063 }
1064 };
1065
1066 let names = public_associated_helpers(&input)
1067 .unwrap()
1068 .into_iter()
1069 .map(|method| method.sig.ident.to_string())
1070 .collect::<Vec<_>>();
1071 assert_eq!(names, vec!["visible"]);
1072 }
1073
1074 #[test]
1075 fn public_instance_methods_are_rejected() {
1076 let input: ItemImpl = syn::parse_quote! {
1077 impl Example {
1078 pub fn instance(&self) {}
1079 fn private_instance(&self) {}
1080 }
1081 };
1082
1083 assert!(public_associated_helpers(&input).is_err());
1084 }
1085}
1086
1087fn generate_param_checks(inputs: &Punctuated<FnArg, Token![,]>) -> proc_macro2::TokenStream {
1088 let checks = inputs.iter().enumerate().filter_map(|(i, arg)| {
1089 if let FnArg::Typed(PatType { ty, .. }) = arg {
1090 let ty_string = quote!(#ty).to_string();
1091 let arg_name = format_ident!("arg_{}", i);
1092 Some(quote! {
1093 let #arg_name = <#ty as pax_engine::api::CoercionRules>::try_coerce(args[#i].clone())
1094 .map_err(|_| format!("Failed to coerce argument {} to {}", #i, #ty_string))?;
1095 })
1096 } else {
1097 None
1098 }
1099 });
1100
1101 quote! { #(#checks)* }
1102}
1103
1104fn generate_function_call(sig: &Signature, struct_name: &Box<Type>) -> proc_macro2::TokenStream {
1105 let func_name = &sig.ident;
1106 let args = sig.inputs.iter().enumerate().filter_map(|(i, arg)| {
1107 if let FnArg::Typed(_) = arg {
1108 let arg_name = format_ident!("arg_{}", i);
1109 Some(quote! { #arg_name })
1110 } else {
1111 None
1112 }
1113 });
1114
1115 quote! {
1116 let result = #struct_name::#func_name(#(#args),*);
1117 Ok(<_ as pax_engine::api::ToPaxValue>::to_pax_value(result))
1118 }
1119}