use convert_case::{Case, Casing};
use quote::{format_ident, quote};
fn extract_doc_comment(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
attrs
.iter()
.flat_map(|a| match a.parse_meta() {
Ok(syn::Meta::NameValue(mnv)) => {
if mnv.path.is_ident("doc") {
if let syn::Lit::Str(ls) = mnv.lit {
let lsv = ls.value();
return Some(quote! { Some(#lsv) });
}
}
None
},
_ => None,
})
.next()
.unwrap_or(quote! { None })
}
fn is_elided(attrs: &[syn::Attribute]) -> bool {
attrs.iter().filter(|a| a.path.is_ident("elide")).count() > 0
}
fn is_unique(attrs: &[syn::Attribute]) -> bool {
attrs.iter().filter(|a| a.path.is_ident("unique")).count() > 0
}
fn is_key(attrs: &[syn::Attribute]) -> bool {
attrs.iter().filter(|a| a.path.is_ident("key")).count() > 0
}
fn is_migratable(attrs: &[syn::Attribute]) -> Option<syn::Meta> {
attrs
.iter()
.filter(|a| a.path.is_ident("migratable"))
.map(|a| a.parse_meta())
.next()
.into_iter()
.flatten()
.next()
}
pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: syn::DeriveInput = syn::parse_macro_input!(tokens);
let parts = match input.data {
syn::Data::Struct(syn::DataStruct {
struct_token: _,
fields: syn::Fields::Named(fields),
semi_token: _,
}) => fields
.named
.into_iter()
.map(|f| (f.ident.unwrap(), f.ty, f.attrs))
.collect::<Vec<_>>(),
_ => {
panic!("Can only derive Entity on data structs with named fields!")
},
};
if parts.is_empty() {
panic!("entities must contain at least one field!");
}
let entity_ident = input.ident;
let make_combined_name = |part: &(syn::Ident, syn::Type, _)| {
format_ident!(
"{}{}PartType",
entity_ident,
part.0.to_string().to_case(Case::UpperCamel)
)
};
let make_part_list = |plist: &Vec<_>| match plist.len() {
0 => quote! { microrm::schema::entity::EmptyList<Self> },
1 => {
let ty = make_combined_name(plist.first().as_ref().unwrap());
quote! { #ty }
},
_ => {
let tys = plist.iter().map(make_combined_name);
quote! { ( #(#tys),* ) }
},
};
let vis = input.vis;
let key_parts = parts
.iter()
.filter(|part| is_key(&part.2))
.cloned()
.collect::<Vec<_>>();
let part_defs = parts.iter().enumerate().map(|(index, part)| {
let part_combined_name = make_combined_name(part);
let part_base_ident = &part.0;
let part_base_name = &part.0.to_string();
let part_type = &part.1;
let unique = is_unique(&part.2);
let doc = extract_doc_comment(&part.2);
quote! {
#[derive(Clone, Copy, Default)]
#[doc(hidden)]
#vis struct #part_combined_name;
impl ::microrm::schema::entity::EntityPart for #part_combined_name {
type Datum = #part_type;
type Entity = #entity_ident;
fn part_name() -> &'static str {
#part_base_name
}
fn unique() -> bool {
#unique
}
fn desc() -> Option<&'static str> {
#doc
}
fn get_datum(from: &Self::Entity) -> &Self::Datum {
&from.#part_base_ident
}
}
impl ::microrm::schema::index::IndexedEntityPart<#index> for #entity_ident {
type Entity = #entity_ident;
type Part = #part_combined_name;
}
}
});
let part_visit = parts.iter().map(|part| {
let part_combined_name = make_combined_name(part);
quote! {
v.visit::<#part_combined_name>();
}
});
let part_ref_visit = parts.iter().map(|part| {
let part_combined_name = make_combined_name(part);
let field = &part.0;
quote! {
v.visit_datum::<#part_combined_name>(&self.#field)?;
}
});
let part_mut_visit = parts.iter().map(|part| {
let part_combined_name = make_combined_name(part);
let field = &part.0;
quote! {
v.visit_datum_mut::<#part_combined_name>(&mut self.#field);
}
});
let part_names = parts.iter().map(|part| {
let part_combined_name = make_combined_name(part);
let part_camel_name = format_ident!("{}", part.0.to_string().to_case(Case::UpperCamel));
quote! {
pub const #part_camel_name : #part_combined_name = #part_combined_name;
}
});
let part_indices = parts.iter().enumerate().map(|(i, part)| {
let part_index_name =
format_ident!("_{}_INDEX", part.0.to_string().to_case(Case::UpperSnake));
quote! {
#[doc(hidden)]
pub const #part_index_name : usize = #i;
}
});
let build_struct = parts
.iter()
.enumerate()
.map(|(i, part)| {
let ident = &part.0;
match parts.len() {
1 => {
quote! {
#ident: values
}
},
_ => {
let idx = syn::Index::from(i);
quote! {
#ident: values. #idx
}
},
}
})
.collect::<Vec<_>>();
let debug_fields = parts
.iter()
.filter(|part| !is_elided(&part.2))
.map(|part| {
let ident = &part.0;
let field = ident.to_string();
quote! {
self . #ident . debug_field(#field, &mut ds);
}
})
.collect::<Vec<_>>();
let parts_list = make_part_list(&parts);
let key_list = make_part_list(&key_parts);
let entity_ident_str = entity_ident.to_string();
let entity_name = entity_ident.to_string().to_case(Case::Snake);
let id_ident = format_ident!("{}ID", entity_ident);
let id_part_ident = format_ident!("{}IDPart", entity_ident);
let migrate_impls = match is_migratable(&input.attrs) {
Some(syn::Meta::List(mlist)) => {
let mut impls = vec![];
for nested in mlist.nested.iter() {
let syn::NestedMeta::Meta(syn::Meta::Path(mpath)) = nested else {
panic!()
};
let field_names = parts.iter().map(|v| &v.0);
let field_types = parts.iter().map(|v| &v.1);
impls.push(quote! {
impl ::microrm::schema::migration::MigratableEntity< #mpath > for #entity_ident {
fn migrate(from: & #mpath) -> ::microrm::DBResult<Option<Self>> {
Ok(Some(
Self {
#(
#field_names : <#field_types as ::microrm::schema::migration::MigratableDatum<_>>::migrate_datum (&from . #field_names )?
),*
}
))
}
}
})
}
impls
},
Some(m) => panic!("migrate attr must be given a path: {m:?}"),
None => vec![],
};
let ref_ident = format_ident!("{}Ref", entity_ident);
let ref_fields = parts.iter().map(|p| {
let fname = &p.0;
let ftype = &p.1;
quote! {
#vis #fname : < #ftype as ::microrm::schema::datum::OwnedDatum >:: RefData<'l>
}
});
let as_entity_fields = parts.iter().map(|p| {
let fname = &p.0;
let ftype = &p.1;
quote! {
#fname: <
< #ftype as ::microrm::schema::datum::OwnedDatum >::RefData<'_>
as ::microrm::schema::datum::BorrowedDatum<'_, #ftype > >
:: as_owned( &self . #fname )
}
});
let as_ref_fields = parts.iter().map(|p| {
let fname = &p.0;
let ftype = &p.1;
quote! { #fname: < #ftype as ::microrm::schema::datum::OwnedDatum >::as_ref( &self . #fname ) }
});
let ref_as_borrowed_list = parts.iter().map(|p| {
let fname = &p.0;
quote! { self.#fname }
});
let ref_from_borrowed_list = parts.iter().enumerate().map(|(idx, p)| {
let fname = &p.0;
let idx = syn::Index::from(idx);
if parts.len() > 1 {
quote! { #fname: l. #idx }
} else {
quote! { #fname: l }
}
});
quote! {
#(#part_defs)*
impl #entity_ident {
#(#part_names)*
#(#part_indices)*
}
#[derive(Clone, Copy)]
#vis struct #ref_ident<'l> {
#(#ref_fields),*
}
impl<'l> ::microrm::schema::entity::EntityRef<'l> for #ref_ident<'l> {
type Entity = #entity_ident;
fn as_entity(&self) -> Self::Entity {
Self::Entity {
#(#as_entity_fields),*
}
}
fn as_borrowed_list<'r>(&self) -> ::microrm::schema::entity::EntityRefDatumList<'r, #entity_ident> where 'l: 'r {
( #(#ref_as_borrowed_list),* )
}
fn from_borrowed_list<'r>(l: ::microrm::schema::entity::EntityRefDatumList<'r, #entity_ident>) -> Self where 'r: 'l {
Self {
#(#ref_from_borrowed_list),*
}
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
#vis struct #id_ident (i64);
impl ::microrm::schema::entity::EntityID for #id_ident {
type Entity = #entity_ident;
fn from_raw(raw: i64) -> Self { Self(raw) }
fn into_raw(self) -> i64 { self.0 }
}
impl ::microrm::schema::datum::Datum for #id_ident {
fn sql_type() -> &'static str {
<i64 as ::microrm::schema::datum::Datum>::sql_type()
}
fn bind_to<'a>(&self, stmt: &mut ::microrm::db::StatementContext, index: i32) -> ::microrm::DBResult<()> {
<i64 as ::microrm::schema::datum::Datum>::bind_to(&self.0, stmt, index)
}
fn build_from<'a>(
rdata: ::microrm::schema::relation::RelationData,
stmt: &mut ::microrm::db::StatementRow,
index: &mut i32,
) -> ::microrm::DBResult<Self>
where
Self: Sized,
{
Ok(Self(<i64 as ::microrm::schema::datum::Datum>::build_from(rdata, stmt, index)?))
}
fn accept_discriminator(d: &mut impl ::microrm::schema::datum::DatumDiscriminator) where Self: Sized {
d.visit_entity_id::<#entity_ident>();
}
fn accept_discriminator_ref(&self, d: &mut impl ::microrm::schema::datum::DatumDiscriminatorRef) where Self: Sized {
d.visit_entity_id::<#entity_ident>(self);
}
}
impl ::microrm::schema::datum::OwnedDatum for #id_ident {
type RefData<'a> = Self;
fn as_ref(&self) -> Self::RefData<'_> { *self }
}
impl ::microrm::schema::datum::BorrowedDatum<'_, #id_ident> for #id_ident {
fn as_owned(&self) -> Self { *self }
}
#[doc(hidden)]
#[derive(Clone, Copy, Default)]
#vis struct #id_part_ident;
impl ::microrm::schema::entity::EntityPart for #id_part_ident {
type Datum = #id_ident;
type Entity = #entity_ident;
fn unique() -> bool { false }
fn part_name() -> &'static str { "id" }
fn desc() -> Option<&'static str> { None }
fn get_datum(_from: &Self::Entity) -> &Self::Datum {
unreachable!()
}
}
impl ::std::fmt::Debug for #entity_ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
use ::microrm::schema::datum::Datum;
let mut ds = f.debug_struct(#entity_ident_str);
#(#debug_fields)*
ds.finish()
}
}
impl ::microrm::schema::entity::Entity for #entity_ident {
type Parts = #parts_list;
type Keys = #key_list;
type ID = #id_ident;
type IDPart = #id_part_ident;
type ERef<'a> = #ref_ident <'a>;
fn as_ref(&self) -> Self::ERef<'_> {
#ref_ident {
#( #as_ref_fields ),*
}
}
fn build(values: <Self::Parts as ::microrm::schema::entity::EntityPartList>::DatumList) -> Self {
Self {
#(#build_struct),*
}
}
fn entity_name() -> &'static str { #entity_name }
fn accept_part_visitor(v: &mut impl ::microrm::schema::entity::EntityPartVisitor<Entity = Self>) {
#(
#part_visit
);*
}
fn accept_part_visitor_ref(&self, v: &mut impl ::microrm::schema::entity::EntityPartVisitor<Entity = Self>) -> ::microrm::DBResult<()> {
#(
#part_ref_visit
);*
Ok(())
}
fn accept_part_visitor_mut(&mut self, v: &mut impl ::microrm::schema::entity::EntityPartVisitor<Entity = Self>) {
#(
#part_mut_visit
);*
}
}
#(#migrate_impls)*
}
.into()
}