use convert_case::{Case, Casing}; use quote::{format_ident, quote}; pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { let input: syn::DeriveInput = syn::parse_macro_input!(tokens); let parts = if let syn::Data::Struct(syn::DataStruct { struct_token: _, fields: syn::Fields::Named(fields), semi_token: _, }) = input.data { fields .named .into_iter() .map(|f| (f.ident.unwrap(), f.ty, f.attrs)) .collect::>() } else { panic!("Can only derive Entity on data structs with named fields!"); }; 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! { () }, 1 => { let ty = make_combined_name(&plist.first().as_ref().unwrap()); quote! { #ty } } _ => { let tys = plist.iter().map(|part| make_combined_name(&part)); quote! { ( #(#tys),* ) } } }; let vis = input.vis; let unique_ident = format_ident!("unique"); // collect list of unique parts let unique_parts = parts .iter() .filter(|part| { part.2.iter().any(|attr| { attr.parse_meta() .map(|a| a.path().is_ident(&unique_ident)) .unwrap_or(false) }) }) .cloned() .collect::>(); let part_defs = parts.iter().map(|part| { let part_combined_name = make_combined_name(&part); let part_base_name = &part.0.to_string(); let part_type = &part.1; let unique = unique_parts.iter().any(|p| p.0 == part.0); quote! { #vis struct #part_combined_name; impl ::microrm::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 } } } }); 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_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 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::>(); let parts_list = make_part_list(&parts); let uniques_list = make_part_list(&unique_parts); let entity_name = entity_ident.to_string().to_case(Case::Snake); let id_ident = format_ident!("{}ID", entity_ident); quote! { #(#part_defs)* impl #entity_ident { #(#part_names)* } #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Hash)] #vis struct #id_ident (i64); impl ::microrm::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::datum::Datum for #id_ident { fn sql_type() -> &'static str { ::sql_type() } fn bind_to<'a>(&self, stmt: &mut ::microrm::sqlite::Statement<'a>, index: usize) { ::bind_to(&self.0, stmt, index) } fn build_from<'a>( adata: ::microrm::schema::AssocData, stmt: &mut ::microrm::sqlite::Statement<'a>, index: usize, ) -> ::microrm::DBResult<(Self, usize)> where Self: Sized, { let raw = ::build_from(adata, stmt, index)?; Ok((Self(raw.0), raw.1)) } fn accept_discriminator(d: &mut impl ::microrm::schema::DatumDiscriminator) where Self: Sized { d.visit_entity_id::<#entity_ident>(); } } impl ::microrm::entity::Entity for #entity_ident { type Parts = #parts_list; type Uniques = #uniques_list; type ID = #id_ident; fn build(values: ::DatumList) -> Self { Self { #(#build_struct),* } } fn entity_name() -> &'static str { #entity_name } fn accept_part_visitor(v: &mut impl ::microrm::entity::EntityPartVisitor) { #( #part_visit );* } fn accept_part_visitor_ref(&self, v: &mut impl ::microrm::entity::EntityPartVisitor) { #( #part_ref_visit );* } } } .into() }