#![allow(non_snake_case)]
extern crate ctor;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Item, ItemFn, LitStr, Meta, parse_macro_input, punctuated::Punctuated, token::Comma};
#[proc_macro_attribute]
pub fn persist(attr: TokenStream, item: TokenStream) -> TokenStream {
let metas = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
let is_comp = metas
.iter()
.any(|m| matches!(m, Meta::Path(p) if p.is_ident("component")));
let is_res = metas
.iter()
.any(|m| matches!(m, Meta::Path(p) if p.is_ident("resource")));
let is_rel = metas
.iter()
.any(|m| matches!(m, Meta::Path(p) if p.is_ident("relationship")));
if !is_comp && !is_res && !is_rel {
return item;
}
let mut ast = parse_macro_input!(item as Item);
if let Item::Struct(s) = &mut ast {
if is_comp {
s.attrs.push(syn::parse_quote!(
#[derive(::bevy::prelude::Component, ::serde::Serialize, ::serde::Deserialize)]
));
if let syn::Fields::Unnamed(fields) = &s.fields {
if fields.unnamed.len() == 1 {
s.attrs.push(syn::parse_quote!(#[serde(transparent)]));
}
}
} else if is_res {
s.attrs.push(syn::parse_quote!(
#[derive(::bevy::prelude::Resource, ::serde::Serialize, ::serde::Deserialize)]
));
if let syn::Fields::Unnamed(fields) = &s.fields {
if fields.unnamed.len() == 1 {
s.attrs.push(syn::parse_quote!(#[serde(transparent)]));
}
}
} else {
s.attrs.push(syn::parse_quote!(
#[cfg_attr(feature = "bevy_many_relationship_edges", derive(::serde::Serialize, ::serde::Deserialize))]
));
if let syn::Fields::Unnamed(fields) = &s.fields {
if fields.unnamed.len() == 1 {
s.attrs.push(syn::parse_quote!(
#[cfg_attr(feature = "bevy_many_relationship_edges", serde(transparent))]
));
}
}
}
} else if let Item::Enum(e) = &mut ast {
let derive_list = if is_comp {
quote! { ::bevy::prelude::Component, ::serde::Serialize, ::serde::Deserialize }
} else if is_res {
quote! { ::bevy::prelude::Resource, ::serde::Serialize, ::serde::Deserialize }
} else {
quote! { ::serde::Serialize, ::serde::Deserialize }
};
e.attrs.push(syn::parse_quote!(#[derive(#derive_list)]));
}
let name = match &ast {
Item::Struct(s) => &s.ident,
Item::Enum(e) => &e.ident,
_ => unreachable!(),
};
let register_fn = format_ident!("__persist_register_{}", name);
let ctor_fn = format_ident!("__persist_ctor_{}", name);
let crate_path = get_crate_path();
let registration = if is_comp {
quote! {
#[allow(non_snake_case)]
fn #register_fn(app: &mut bevy::app::App) {
use bevy::prelude::IntoScheduleConfigs;
let type_id = std::any::TypeId::of::<#name>();
let mut registered = app
.world_mut()
.resource_mut::<#crate_path::bevy::plugins::persistence_plugin::RegisteredPersistTypes>();
if registered.types.insert(type_id) {
app.world_mut()
.resource_mut::<#crate_path::core::session::PersistenceSession>()
.register_component::<#name>();
app.add_systems(
bevy::app::PostUpdate,
#crate_path::bevy::plugins::persistence_plugin::auto_dirty_tracking_entity_system::<#name>
.in_set(#crate_path::bevy::plugins::persistence_plugin::PersistenceSystemSet::ChangeDetection),
);
}
}
}
} else if is_res {
quote! {
#[allow(non_snake_case)]
fn #register_fn(app: &mut bevy::app::App) {
use bevy::prelude::IntoScheduleConfigs;
let type_id = std::any::TypeId::of::<#name>();
let mut registered = app
.world_mut()
.resource_mut::<#crate_path::bevy::plugins::persistence_plugin::RegisteredPersistTypes>();
if registered.types.insert(type_id) {
app.world_mut()
.resource_mut::<#crate_path::core::session::PersistenceSession>()
.register_resource::<#name>();
app.add_systems(
bevy::app::PostUpdate,
#crate_path::bevy::plugins::persistence_plugin::auto_dirty_tracking_resource_system::<#name>
.in_set(#crate_path::bevy::plugins::persistence_plugin::PersistenceSystemSet::ChangeDetection),
);
}
}
}
} else {
quote! {
#[allow(non_snake_case)]
fn #register_fn(app: &mut bevy::app::App) {
#[cfg(feature = "bevy_many_relationship_edges")]
{
use bevy::prelude::IntoScheduleConfigs;
let type_id = std::any::TypeId::of::<#name>();
let mut registered = app
.world_mut()
.resource_mut::<#crate_path::bevy::plugins::persistence_plugin::RegisteredPersistTypes>();
if registered.types.insert(type_id) {
app.world_mut()
.resource_mut::<#crate_path::core::session::PersistenceSession>()
.register_many_relationship::<#name>(stringify!(#name));
app.add_systems(
bevy::app::PostUpdate,
#crate_path::bevy::plugins::persistence_plugin::auto_dirty_tracking_relationship_system::<#name>
.in_set(#crate_path::bevy::plugins::persistence_plugin::PersistenceSystemSet::ChangeDetection),
);
}
}
#[cfg(not(feature = "bevy_many_relationship_edges"))]
{
use bevy::prelude::IntoScheduleConfigs;
let type_id = std::any::TypeId::of::<#name>();
let mut registered = app
.world_mut()
.resource_mut::<#crate_path::bevy::plugins::persistence_plugin::RegisteredPersistTypes>();
if registered.types.insert(type_id) {
app.world_mut()
.resource_mut::<#crate_path::core::session::PersistenceSession>()
.register_bevy_relationship::<#name>(stringify!(#name));
app.add_systems(
bevy::app::PostUpdate,
#crate_path::bevy::plugins::persistence_plugin::auto_dirty_tracking_bevy_relationship_system::<#name>
.in_set(#crate_path::bevy::plugins::persistence_plugin::PersistenceSystemSet::ChangeDetection),
);
}
}
}
}
};
let ctor_registration = quote! {
#[ctor::ctor]
#[allow(non_snake_case)]
fn #ctor_fn() {
#crate_path::bevy::registration::COMPONENT_REGISTRY
.lock()
.unwrap()
.push(#register_fn);
}
};
let impl_persist = if is_rel {
quote! {
#[cfg(feature = "bevy_many_relationship_edges")]
impl #crate_path::core::persist::Persist for #name {
fn name() -> &'static str {
stringify!(#name)
}
}
}
} else {
quote! {
impl #crate_path::core::persist::Persist for #name {
fn name() -> &'static str {
stringify!(#name)
}
}
}
};
let mut field_methods = quote! {};
if let Item::Struct(ref s) = ast {
let methods = s.fields.iter().filter_map(|f| f.ident.as_ref()).map(|ident| {
let field_str = LitStr::new(&ident.to_string(), proc_macro2::Span::call_site());
quote! {
pub fn #ident() -> #crate_path::core::query::filter_expression::FilterExpression {
#crate_path::core::query::filter_expression::FilterExpression::field(<Self as #crate_path::core::persist::Persist>::name(), #field_str)
}
}
});
let methods_body = quote! {
impl #name {
#(#methods)*
}
};
field_methods = if is_rel {
quote! {
#[cfg(feature = "bevy_many_relationship_edges")]
#methods_body
}
} else {
methods_body
};
}
TokenStream::from(quote! {
#ast
#registration
#ctor_registration
#impl_persist
#field_methods
})
}
#[proc_macro_attribute]
pub fn db_matrix_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let body = &input.block;
let orig_name = &sig.ident;
let passthrough_attrs: Vec<syn::Attribute> = input
.attrs
.into_iter()
.filter(|a| !a.path().is_ident("test"))
.collect();
let gen_backend_test = |suffix: &str,
backend_expr: proc_macro2::TokenStream,
cfg: proc_macro2::TokenStream| {
let fn_name = format_ident!("{}_{}", orig_name, suffix);
let attrs = &passthrough_attrs;
let skip_check = quote! {
let wants = std::env::var("bevy_persistence_database_TEST_BACKENDS").unwrap_or_default();
if !wants.is_empty() {
let mut enabled = false;
for token in wants.split(',').map(|s| s.trim().to_ascii_lowercase()).filter(|s| !s.is_empty()) {
if token == #suffix {
enabled = true;
break;
}
}
if !enabled {
eprintln!("skipping {} due to bevy_persistence_database_TEST_BACKENDS={}", stringify!(#fn_name), wants);
return;
}
}
};
quote! {
#cfg
#(#attrs)*
#[test]
#vis fn #fn_name() {
#skip_check
let backend = #backend_expr;
let setup = || crate::common::setup_backend(backend);
#body
}
}
};
#[cfg(feature = "ra-fallback")]
{
let attrs = &passthrough_attrs;
let expanded = quote! {
#(#attrs)*
#[test]
#vis fn #orig_name() {
#[allow(unused_mut)]
let mut backend_opt = None;
#[cfg(feature = "postgres")]
{ backend_opt = Some(crate::common::TestBackend::Postgres); }
#[cfg(all(not(feature = "postgres"), feature = "arango"))]
{ backend_opt = Some(crate::common::TestBackend::Arango); }
let backend = backend_opt.expect("No backend feature enabled");
let setup = || crate::common::setup_backend(backend);
#body
}
};
return TokenStream::from(expanded);
}
#[cfg(not(feature = "ra-fallback"))]
{
let arango_test = gen_backend_test(
"db_arango",
quote!(crate::common::TestBackend::Arango),
quote!(#[cfg(feature = "arango")]),
);
let postgres_test = gen_backend_test(
"db_postgres",
quote!(crate::common::TestBackend::Postgres),
quote!(#[cfg(feature = "postgres")]),
);
let expanded = quote! {
#arango_test
#postgres_test
};
return TokenStream::from(expanded);
}
}
fn get_crate_path() -> proc_macro2::TokenStream {
use proc_macro_crate::{FoundCrate, crate_name};
if let Ok(FoundCrate::Itself) = crate_name("bevy_persistence_database") {
return quote!(crate);
}
if let Ok(FoundCrate::Name(name)) = crate_name("bevy_persistence_database") {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
return quote!(::#ident);
}
quote!(::bevy_persistence_database)
}