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}; use 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
104fn 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 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 = Some((f.ty.to_owned(), false));
126 }
127 }
128 _ => {}
129 };
130 }
131 ret
132}
133
134fn 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 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 recurse_get_scoped_resolvable_types(gat, accum);
180 },
181 _ => { }
185 };
186 })
187 },
188 PathArguments::Parenthesized(_) => {unimplemented!("Parenthesized path arguments (for example, Fn types) not yet supported inside Pax `Property<...>`")},
189 PathArguments::None => {
190 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 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 => { }
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
300fn 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 if is_main_component {
360 template_dependencies.push("BlankComponent".to_string());
361 }
362
363 let pax_dir: Option<PathBuf> = option_env!("PAX_DIR")
364 .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(¤t_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 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 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 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 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 }
658 }
659 }
660 } else {
661 quote! {}
662 };
663
664 let output = quote! {
665 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
678fn 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 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}