extern crate proc_macro;
extern crate proc_macro2;
mod launch;
mod root_crate;
mod source;
#[proc_macro]
pub fn init(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
launch::init(input, false)
}
#[proc_macro]
pub fn init_with(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
launch::init(input, true)
}
#[doc(hidden)]
#[proc_macro]
pub fn __pax_launch(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
launch::expand(input)
}
mod templating;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
use std::{env, path::PathBuf};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::punctuated::Punctuated;
use templating::{
EnumVariantDefinition, InternalDefinitions, StaticPropertyDefinition, TemplateArgsCartridge,
TemplateArgsDerivePax, TemplateBuildConfig,
};
use sailfish::TemplateOnce;
fn is_root_crate() -> bool {
if env_flag_explicitly_disabled("PAX_BUILD_CARTRIDGE") {
return false;
}
root_crate::is_application_crate(
env::var_os("PAX_CARTRIDGE_MANIFEST_DIR")
.as_deref()
.map(Path::new),
env::var_os("PAX_DIR").as_deref().map(Path::new),
&env::var_os("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_default(),
env::var_os("CARGO_PRIMARY_PACKAGE").is_some(),
)
}
#[proc_macro]
pub fn runnable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
source::runnable(input)
}
fn render_cartridge(root: &str, initializer: Option<String>, engine: &str) -> String {
render_generated_cartridge(
root,
initializer,
engine,
String::new(),
template_build_config(),
)
}
fn render_generated_cartridge(
root: &str,
initializer: Option<String>,
engine: &str,
generated: String,
build_config: TemplateBuildConfig,
) -> String {
let initializer = initializer.unwrap_or_else(|| format!("|_| Ok(<{root}>::default())"));
let mut output = format!(
r#"
#[allow(unused_imports)]
use {root} as __PaxSelectedRoot;
#[allow(dead_code)]
fn __pax_initialize(context: &{engine}::api::InitContext) -> {engine}::api::AppResult<__PaxSelectedRoot> {{
({initializer})(context)
}}
const _: Option<&str> = option_env!("PAX_DIR");
const _: Option<&str> = option_env!("PAX_CARTRIDGE_MANIFEST_DIR");
const _: Option<&str> = option_env!("PAX_BUILD_CARTRIDGE");
const _: Option<&str> = option_env!("PAX_BUILD_TARGET");
const _: Option<&str> = option_env!("PAX_BUILD_DESIGNTIME");
const _: Option<&str> = option_env!("CARGO_PRIMARY_PACKAGE");
"#
);
if generated.is_empty() && !is_root_crate() {
return output;
}
if !(build_config.web || build_config.macos || build_config.ios || build_config.ipados) {
return output;
}
let snippet = if cfg!(feature = "designtime")
&& env_flag_explicitly_disabled("PAX_BUILD_DESIGNTIME")
{
"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()
} else if !generated.is_empty() {
generated
} else if let Some(pax_dir) = env::var_os("PAX_DIR") {
let path = PathBuf::from(pax_dir).join("cartridge.partial.rs");
format!("include!({:?});", path.to_string_lossy())
} else {
"compile_error!(\"PAX_DIR was not set for the selected Pax cartridge. Build the application through pax-cli to generate its cartridge.\");".into()
};
let adapter = TemplateArgsCartridge {
catalog: false,
web_entrypoint: "init_web".into(),
root_path: "super::__PaxSelectedRoot".into(),
initializer: "super::__pax_initialize".into(),
cartridge_snippet: snippet,
engine_import_path: engine.into(),
build_config,
}
.render_once()
.unwrap();
let facade_import = if engine.replace(' ', "") == "pax_kit::pax_engine" {
"use pax_kit::*;"
} else {
""
};
output.push_str(&format!(
r#"
#[allow(unused_imports)]
mod __pax_cartridge {{
use super::*;
{facade_import}
use {engine}::*;
use {engine}::api::*;
{adapter}
}}
#[allow(unused_imports)]
pub use __pax_cartridge::*;
"#
));
output
}
fn env_flag(name: &str) -> bool {
env::var(name)
.map(|value| {
let normalized = value.to_ascii_lowercase();
matches!(normalized.as_str(), "1" | "true" | "yes" | "on")
})
.unwrap_or(false)
}
fn env_flag_explicitly_disabled(name: &str) -> bool {
env::var(name)
.map(|value| {
let normalized = value.to_ascii_lowercase();
matches!(normalized.as_str(), "0" | "false" | "no" | "off")
})
.unwrap_or(false)
}
fn cargo_feature_enabled(feature: &str) -> bool {
env::var_os(format!(
"CARGO_FEATURE_{}",
feature.replace('-', "_").to_ascii_uppercase()
))
.is_some()
}
pub(crate) fn designtime_enabled() -> bool {
match std::env::var("PAX_BUILD_DESIGNTIME") {
Ok(_) => env_flag("PAX_BUILD_DESIGNTIME"),
Err(_) => cfg!(feature = "designtime"),
}
}
fn template_build_config() -> TemplateBuildConfig {
let pax_build_target = env::var("PAX_BUILD_TARGET").unwrap_or_default();
let designtime = designtime_enabled();
TemplateBuildConfig {
web: pax_build_target == "web" || cargo_feature_enabled("web"),
macos: pax_build_target == "macos" || cargo_feature_enabled("macos"),
ios: pax_build_target == "ios" || cargo_feature_enabled("ios"),
ipados: pax_build_target == "ipados",
designtime,
}
}
use syn::{
parse_macro_input, Data, DeriveInput, Field, Fields, FnArg, GenericArgument, ImplItem,
ImplItemMethod, ItemImpl, Lit, Meta, PatType, PathArguments, Signature, Token, Type,
};
fn pax_primitive(
input_parsed: &DeriveInput,
_primitive_instance_import_path: String,
is_custom_interpolatable: bool,
is_custom_coercion_rules: bool,
can_derive_identity_roundtrip: bool,
engine_import_path: String,
) -> proc_macro2::TokenStream {
let _original_tokens = quote! { #input_parsed }.to_string();
let pascal_identifier = input_parsed.ident.to_string();
let is_enum = match &input_parsed.data {
Data::Enum(_) => true,
_ => false,
};
let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
let output = TemplateArgsDerivePax {
cartridge: String::new(),
internal_definitions,
pascal_identifier,
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
_is_enum: is_enum,
engine_import_path,
}
.render_once()
.unwrap()
.to_string();
TokenStream::from_str(&output).unwrap().into()
}
fn pax_struct_only_component(
input_parsed: &DeriveInput,
is_custom_interpolatable: bool,
is_custom_coercion_rules: bool,
can_derive_identity_roundtrip: bool,
engine_import_path: String,
) -> proc_macro2::TokenStream {
let pascal_identifier = input_parsed.ident.to_string();
let is_enum = match &input_parsed.data {
Data::Enum(_) => true,
_ => false,
};
let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
let output = TemplateArgsDerivePax {
cartridge: String::new(),
pascal_identifier: pascal_identifier.clone(),
internal_definitions,
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
_is_enum: is_enum,
engine_import_path,
}
.render_once()
.unwrap()
.to_string();
TokenStream::from_str(&output).unwrap().into()
}
fn is_rust_field(field: &Field) -> bool {
field.attrs.iter().any(|attr| {
attr.path.is_ident("pax")
&& attr
.parse_args::<syn::Ident>()
.map(|name| name == "rust")
.unwrap_or(false)
})
}
fn get_field_type(f: &Field) -> Option<(Type, bool)> {
if is_rust_field(f) {
return None;
}
let mut ret = None;
if let Type::Path(tp) = &f.ty {
match tp.qself {
None => {
tp.path.segments.iter().for_each(|ps| {
if ps.ident.to_string().ends_with("Property") {
if let PathArguments::AngleBracketed(abga) = &ps.arguments {
abga.args.iter().for_each(|abgaa| {
if let GenericArgument::Type(gat) = abgaa {
ret = Some((gat.to_owned(), true));
}
})
}
}
});
if ret.is_none() {
ret = Some((f.ty.to_owned(), false));
}
}
_ => {}
};
}
ret
}
fn get_scoped_resolvable_types(t: &Type) -> (Vec<String>, String) {
let mut accum: Vec<String> = vec![];
recurse_get_scoped_resolvable_types(t, &mut accum);
let root_scoped_resolvable_type = accum.get(accum.len() - 1).unwrap().clone();
(accum, root_scoped_resolvable_type)
}
fn recurse_get_scoped_resolvable_types(t: &Type, accum: &mut Vec<String>) {
match t {
Type::Path(tp) => {
match tp.qself {
None => {
let mut accumulated_scoped_resolvable_type = "".to_string();
tp.path.segments.iter().for_each(|ps| {
match &ps.arguments {
PathArguments::AngleBracketed(abga) => {
if accumulated_scoped_resolvable_type.ne("") {
accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
}
let ident = ps.ident.to_token_stream().to_string();
let turbofish_contents = ps.to_token_stream()
.to_string()
.replacen(&ident, "", 1)
.replace(" ", "");
accumulated_scoped_resolvable_type =
accumulated_scoped_resolvable_type.clone() +
&ident +
"::" +
&turbofish_contents;
abga.args.iter().for_each(|abgaa| {
match abgaa {
GenericArgument::Type(gat) => {
recurse_get_scoped_resolvable_types(gat, accum);
},
_ => { }
};
})
},
PathArguments::Parenthesized(_) => {unimplemented!("Parenthesized path arguments (for example, Fn types) not yet supported inside Pax `Property<...>`")},
PathArguments::None => {
if accumulated_scoped_resolvable_type.ne("") {
accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
}
accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + &ps.to_token_stream().to_string();
}
}
});
accum.push(accumulated_scoped_resolvable_type);
}
_ => {
unimplemented!("Self-types not yet supported with Pax `Property<...>`")
}
}
}
Type::Tuple(t) => {
t.elems.iter().for_each(|tuple_elem| {
recurse_get_scoped_resolvable_types(tuple_elem, accum);
});
}
_ => {
unimplemented!("Unsupported Type::Path {}", t.to_token_stream().to_string());
}
}
}
fn index_to_ascii_lowercase(index: usize) -> char {
(b'a' + (index as u8)) as char
}
fn get_internal_definitions_from_tokens(data: &Data) -> InternalDefinitions {
let ret = match data {
Data::Struct(ref data) => {
match data.fields {
Fields::Named(ref fields) => {
let mut spds = vec![];
fields.named.iter().for_each(|f| {
let field_name = f.ident.as_ref().unwrap();
let _field_type = match get_field_type(f) {
None => { }
Some(ty) => {
let type_name = quote!(#(ty.0)).to_string().replace(" ", "");
let (scoped_resolvable_types, root_scoped_resolvable_type) =
get_scoped_resolvable_types(&ty.0);
let pascal_identifier =
type_name.split("::").last().unwrap().to_string();
spds.push(StaticPropertyDefinition {
original_type: type_name,
field_name: quote!(#field_name).to_string(),
scoped_resolvable_types,
root_scoped_resolvable_type,
pascal_identifier,
is_property_wrapped: ty.1,
is_enum: false,
})
}
};
});
InternalDefinitions::Struct(spds)
}
_ => {
unimplemented!("Pax may only be attached to `struct`s with named fields");
}
}
}
Data::Enum(ref data) => {
let mut evds = vec![];
data.variants.iter().for_each(|variant| {
let variant_name = variant.ident.to_string();
let mut variant_fields = vec![];
for (i, f) in variant.fields.iter().enumerate() {
if let Some(ty) = get_field_type(f) {
let original_type = quote!(#(ty.0)).to_string().replace(" ", "");
let (scoped_resolvable_types, root_scoped_resolvable_type) =
get_scoped_resolvable_types(&ty.0);
let pascal_identifier =
original_type.split("::").last().unwrap().to_string();
variant_fields.push(StaticPropertyDefinition {
original_type,
field_name: index_to_ascii_lowercase(i).to_string(),
scoped_resolvable_types,
root_scoped_resolvable_type,
pascal_identifier,
is_property_wrapped: ty.1,
is_enum: true,
})
}
}
evds.push(EnumVariantDefinition {
variant_name,
variant_fields,
});
});
InternalDefinitions::Enum(evds)
}
_ => {
unreachable!("Pax may only be attached to `struct`s")
}
};
ret
}
fn pax_full_component(
_raw_pax: String,
input_parsed: &DeriveInput,
is_main_component: bool,
initializer: Option<String>,
include_fix: Option<TokenStream>,
is_custom_interpolatable: bool,
is_custom_coercion_rules: bool,
can_derive_identity_roundtrip: bool,
_associated_pax_file_path: Option<PathBuf>,
engine_import_path: String,
) -> proc_macro2::TokenStream {
let pascal_identifier = input_parsed.ident.to_string();
let is_enum = match &input_parsed.data {
Data::Enum(_) => true,
_ => false,
};
let internal_definitions = get_internal_definitions_from_tokens(&input_parsed.data);
let cartridge = if is_main_component {
render_cartridge(&pascal_identifier, initializer, &engine_import_path)
} else {
String::new()
};
let output = TemplateArgsDerivePax {
cartridge,
pascal_identifier,
internal_definitions,
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
_is_enum: is_enum,
engine_import_path,
}
.render_once()
.unwrap()
.to_string();
let ret: proc_macro2::TokenStream = TokenStream::from_str(&output).unwrap().into();
if !include_fix.is_none() {
quote! {
#include_fix
#ret
}
} else {
ret
}
.into()
}
struct Config {
is_main_component: bool,
initializer: Option<String>,
file_path: Option<String>,
svg_path: Option<String>,
inlined_contents: Option<String>,
custom_values: Option<Vec<String>>,
engine_import_path: Option<String>,
primitive_instance_import_path: Option<String>,
is_primitive: bool,
has_helpers: bool,
}
fn parse_config(attrs: &mut Vec<syn::Attribute>) -> Config {
let mut config = Config {
is_main_component: false,
initializer: None,
file_path: None,
svg_path: None,
inlined_contents: None,
custom_values: None,
primitive_instance_import_path: None,
engine_import_path: None,
is_primitive: false,
has_helpers: false,
};
attrs.retain(|attr| {
match attr.path.get_ident() {
Some(s) if s == "init" => {
let path: syn::Path = attr
.parse_args()
.expect("#[init(path)] requires one Rust function path");
config.initializer = Some(quote! { #path }.to_string());
return false;
}
Some(s) if s == "file" => {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if let Some(nested_meta) = meta_list.nested.first() {
if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
config.file_path = Some(file_str.value());
return false;
}
}
}
}
Some(s) if s == "svg" => {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if let Some(nested_meta) = meta_list.nested.first() {
if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
config.svg_path = Some(file_str.value());
return false;
}
}
}
}
Some(s) if s == "engine_import_path" => {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if let Some(nested_meta) = meta_list.nested.first() {
if let syn::NestedMeta::Lit(Lit::Str(engine_import_path)) = nested_meta {
config.engine_import_path = Some(engine_import_path.value());
return false;
}
}
}
}
Some(s) if s == "primitive" => {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if let Some(nested_meta) = meta_list.nested.first() {
if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
config.primitive_instance_import_path = Some(file_str.value());
config.is_primitive = true;
return false;
}
}
}
}
Some(s) if s == "route_branch" => {
return false;
}
Some(s) if s == "inlined" => {
let tokens = attr.tokens.clone();
let mut content = proc_macro2::TokenStream::new();
for token in tokens {
if let proc_macro2::TokenTree::Group(group) = token {
if group.delimiter() == proc_macro2::Delimiter::Parenthesis {
content.extend(group.stream());
}
}
}
if !content.is_empty() {
config.inlined_contents = Some(content.to_string());
return false;
}
}
Some(s) if s == "has_helpers" => {
config.has_helpers = true;
return false;
}
_ => {
if let Ok(Meta::Path(path)) = attr.parse_meta() {
if path.is_ident("main") {
config.is_main_component = true;
return false;
}
} else if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if meta_list.path.is_ident("custom") {
let values: Vec<String> = meta_list
.nested
.into_iter()
.filter_map(|nested_meta| {
if let syn::NestedMeta::Meta(Meta::Path(path)) = nested_meta {
path.get_ident().map(|ident| ident.to_string())
} else {
None
}
})
.collect();
config.custom_values = Some(values);
return false;
}
}
}
}
true
});
config
}
fn validate_config(
input: &syn::DeriveInput,
config: &Config,
) -> Result<(), proc_macro::TokenStream> {
let template_source_count = [
config.file_path.is_some(),
config.svg_path.is_some(),
config.inlined_contents.is_some(),
]
.into_iter()
.filter(|present| *present)
.count();
if template_source_count > 1 {
return Err(syn::Error::new_spanned(
input.ident.clone(),
"`#[file(...)]`, `#[svg(...)]`, and `#[inlined(...)]` attributes cannot be used together",
)
.to_compile_error()
.into());
}
if template_source_count == 0 && config.is_main_component {
return Err(syn::Error::new_spanned(
input.ident.clone(),
"Main (application-root) components must specify a Pax template source, e.g. #[file(\"some-file.pax\")], #[svg(\"some-file.svg\")], or #[inlined(<SomePax />)]",
)
.to_compile_error()
.into());
}
if config.is_primitive && template_source_count > 0 {
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.";
return Err(syn::Error::new_spanned(input.ident.clone(), ERR)
.to_compile_error()
.into());
}
Ok(())
}
#[proc_macro_attribute]
pub fn pax(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let _ = args;
source::pax(input)
}
#[doc(hidden)]
#[proc_macro]
pub fn __pax_resume(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
source::resume(input)
}
#[doc(hidden)]
#[proc_macro_attribute]
pub fn __pax_expand(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
let pascal_identifier = input.ident.to_string();
if input
.attrs
.iter()
.any(|attr| attr.path.is_ident("application"))
{
return quote! { compile_error!("#[application(Type)] has been removed; use #[init(path)] with an ordinary fn(&InitContext) -> AppResult<Root>"); }.into();
}
let config = parse_config(&mut input.attrs);
if config.initializer.is_some() && !config.is_main_component {
return quote! { compile_error!("Move #[init(path)] into runnable! { root: YourRoot, init: path }, or use it with the legacy #[main] shorthand"); }
.into();
}
validate_config(&input, &config).unwrap();
let mut trait_impls = vec!["Clone", "Default", "Serialize", "Deserialize", "Debug"];
let mut is_custom_interpolatable = false;
let mut is_custom_coercion_rules = false;
let engine_import_path = match config.engine_import_path {
Some(prefix) => prefix,
None => "pax_kit::pax_engine".to_string(),
};
if let Some(custom) = config.custom_values {
let custom_str: Vec<&str> = custom.iter().map(String::as_str).collect();
trait_impls.retain(|v| !custom_str.contains(v));
if custom.contains(&"Interpolatable".to_string()) {
is_custom_interpolatable = true;
}
if custom.contains(&"CoercionRules".to_string()) {
is_custom_coercion_rules = true;
}
}
let all_fields_represented = match &input.data {
Data::Struct(data) => data.fields.iter().all(|f| get_field_type(f).is_some()),
Data::Enum(data) => data
.variants
.iter()
.all(|v| v.fields.iter().all(|f| get_field_type(f).is_some())),
_ => false,
};
let can_derive_identity_roundtrip = all_fields_represented && trait_impls.contains(&"Default");
let is_pax_file = config.file_path.is_some();
let is_pax_svg = config.svg_path.is_some();
let is_pax_inlined = config.inlined_contents.is_some();
let appended_tokens = if is_pax_file {
let file_name = config.file_path.unwrap();
let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
let path = if Path::new(&root).join(&file_name).exists() {
Path::new(&root).join(&file_name)
} else {
Path::new(&root).join("src/").join(&file_name)
};
let name = Ident::new(&pascal_identifier, Span::call_site());
let include_fix = generate_include(&name, &path);
let associated_pax_file = Some(path.clone());
let file = File::open(path);
let mut content = String::new();
let _ = file.unwrap().read_to_string(&mut content);
pax_full_component(
content,
&input,
config.is_main_component,
config.initializer.clone(),
Some(include_fix),
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
associated_pax_file,
engine_import_path,
)
} else if is_pax_svg {
let file_name = config.svg_path.unwrap();
let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
let path = if Path::new(&root).join(&file_name).exists() {
Path::new(&root).join(&file_name)
} else {
Path::new(&root).join("src/").join(&file_name)
};
let name = Ident::new(&pascal_identifier, Span::call_site());
let include_fix = generate_include(&name, &path);
pax_full_component(
String::new(),
&input,
config.is_main_component,
config.initializer.clone(),
Some(include_fix),
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
None,
engine_import_path,
)
} else if is_pax_inlined {
let contents = config.inlined_contents.unwrap();
pax_full_component(
contents.to_owned(),
&input,
config.is_main_component,
config.initializer.clone(),
None,
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
None,
engine_import_path,
)
} else if config.is_primitive {
pax_primitive(
&input,
config.primitive_instance_import_path.unwrap(),
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
engine_import_path,
)
} else {
pax_struct_only_component(
&input,
is_custom_interpolatable,
is_custom_coercion_rules,
can_derive_identity_roundtrip,
engine_import_path,
)
};
if let Data::Enum(data) = &input.data {
if data
.variants
.iter()
.flat_map(|v| v.fields.iter())
.any(|f| f.attrs.iter().any(|a| a.path.is_ident("pax")))
{
return quote! { compile_error!("#[pax(rust)] is supported on struct fields only"); }
.into();
}
}
if let Data::Struct(data) = &mut input.data {
for field in &mut data.fields {
if is_rust_field(field) {
field.attrs.retain(|attr| !attr.path.is_ident("pax"));
field.attrs.push(syn::parse_quote!(#[serde(skip)]));
} else if field.attrs.iter().any(|attr| attr.path.is_ident("pax")) {
return quote! { compile_error!("the field attribute must be #[pax(rust)]"); }
.into();
}
}
}
let derives: proc_macro2::TokenStream = trait_impls
.into_iter()
.flat_map(|ident| {
let syn_ident = syn::Ident::new(ident, Span::call_site());
if ["Serialize", "Deserialize"].contains(&ident) {
quote! {pax_engine::serde::#syn_ident,}
} else {
quote! {#syn_ident,}
}
})
.collect();
let ident = &input.ident;
let helper_functions_impl = if !config.has_helpers {
quote! {
impl pax_engine::api::HelperFunctions for #ident {
fn register_all_functions() {
}
}
}
} else {
quote! {}
};
let output = quote! {
impl pax_engine::api::ImplToFromPaxAny for #ident {}
#[derive(#derives)]
#[serde(crate = "pax_engine::serde")]
#input
#appended_tokens
#helper_functions_impl
};
output.into()
}
fn generate_include(name: &Ident, path: &PathBuf) -> TokenStream {
let const_name = Ident::new(&format!("_PAX_FILE_{}", name), Span::call_site());
let path_str = path.to_str().expect("expected non-unicode path");
quote! {
#[allow(non_upper_case_globals)]
const #const_name: &'static str = include_str!(#path_str);
}
}
#[proc_macro_attribute]
pub fn helpers(
_attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as ItemImpl);
expand_helpers(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn public_associated_helpers(input: &ItemImpl) -> syn::Result<Vec<&ImplItemMethod>> {
input
.items
.iter()
.filter_map(|item| match item {
ImplItem::Method(method) if matches!(method.vis, syn::Visibility::Public(_)) => {
Some(method)
}
_ => None,
})
.map(|method| {
if method
.sig
.inputs
.iter()
.any(|arg| matches!(arg, FnArg::Receiver(_)))
{
Err(syn::Error::new_spanned(
method.sig.clone(),
"Helpers macro can only be used on associated functions (methods that don't take self)",
))
} else {
Ok(method)
}
})
.collect()
}
fn expand_helpers(input: ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
let struct_name = &input.self_ty;
let mut register_functions = vec![];
for method in public_associated_helpers(&input)? {
let func_name = &method.sig.ident;
let arg_count = method.sig.inputs.len();
let param_checks = generate_param_checks(&method.sig.inputs);
let func_call = generate_function_call(&method.sig, struct_name);
register_functions.push(quote! {
register_function(
stringify!(#struct_name).to_string(),
stringify!(#func_name).to_string(),
Arc::new(move |args: Vec<PaxValue>| -> Result<PaxValue, String> {
if args.len() != #arg_count {
return Err(format!("Expected {} arguments for function {}", #arg_count, stringify!(#func_name)));
}
#param_checks
#func_call
})
);
});
}
let expanded = quote! {
#input
impl pax_engine::api::HelperFunctions for #struct_name {
fn register_all_functions() {
use std::sync::Arc;
use pax_engine::api::{PaxValue, register_function};
#(#register_functions)*
}
}
};
Ok(expanded)
}
#[cfg(test)]
mod helper_visibility_tests {
use super::*;
#[test]
fn only_public_associated_functions_are_registered() {
let input: ItemImpl = syn::parse_quote! {
impl Example {
pub fn visible(value: f64) -> f64 { value }
fn private(value: f64) -> f64 { value }
pub(crate) fn crate_visible(value: f64) -> f64 { value }
}
};
let names = public_associated_helpers(&input)
.unwrap()
.into_iter()
.map(|method| method.sig.ident.to_string())
.collect::<Vec<_>>();
assert_eq!(names, vec!["visible"]);
}
#[test]
fn public_instance_methods_are_rejected() {
let input: ItemImpl = syn::parse_quote! {
impl Example {
pub fn instance(&self) {}
fn private_instance(&self) {}
}
};
assert!(public_associated_helpers(&input).is_err());
}
}
fn generate_param_checks(inputs: &Punctuated<FnArg, Token![,]>) -> proc_macro2::TokenStream {
let checks = inputs.iter().enumerate().filter_map(|(i, arg)| {
if let FnArg::Typed(PatType { ty, .. }) = arg {
let ty_string = quote!(#ty).to_string();
let arg_name = format_ident!("arg_{}", i);
Some(quote! {
let #arg_name = <#ty as pax_engine::api::CoercionRules>::try_coerce(args[#i].clone())
.map_err(|_| format!("Failed to coerce argument {} to {}", #i, #ty_string))?;
})
} else {
None
}
});
quote! { #(#checks)* }
}
fn generate_function_call(sig: &Signature, struct_name: &Box<Type>) -> proc_macro2::TokenStream {
let func_name = &sig.ident;
let args = sig.inputs.iter().enumerate().filter_map(|(i, arg)| {
if let FnArg::Typed(_) = arg {
let arg_name = format_ident!("arg_{}", i);
Some(quote! { #arg_name })
} else {
None
}
});
quote! {
let result = #struct_name::#func_name(#(#args),*);
Ok(<_ as pax_engine::api::ToPaxValue>::to_pax_value(result))
}
}