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 } 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::>(), _ => 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! { microrm::schema::entity::EmptyList }, 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; // collect list of unique parts let key_parts = parts .iter() .filter(|part| is_key(&part.2)) .cloned() .collect::>(); let part_defs = parts.iter().map(|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 placeholder = format!("${}_{}", entity_ident, part_base_name); let unique = is_unique(&part.2); let doc = extract_doc_comment(&part.2); quote! { #[derive(Clone, Copy, Default)] #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 placeholder() -> &'static str { #placeholder } fn unique() -> bool { #unique } fn desc() -> Option<&'static str> { #doc } fn get_datum(from: &Self::Entity) -> &Self::Datum { &from.#part_base_ident } } } }); 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 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 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::>(); 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); quote! { #(#part_defs)* impl #entity_ident { #(#part_names)* } #[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::entity::EntityPart for #id_ident { type Datum = Self; type Entity = #entity_ident; fn unique() -> bool { false } fn part_name() -> &'static str { "id" } fn placeholder() -> &'static str { "TODO" } fn desc() -> Option<&'static str> { None } fn get_datum(from: &Self::Entity) -> &Self::Datum { unreachable!() } } impl ::microrm::schema::datum::Datum for #id_ident { fn sql_type() -> &'static str { ::sql_type() } fn bind_to<'a>(&self, stmt: &mut ::microrm::db::StatementContext, index: i32) { ::bind_to(&self.0, stmt, index) } fn build_from<'a>( adata: ::microrm::schema::AssocData, stmt: &mut ::microrm::db::StatementRow, index: &mut i32, ) -> ::microrm::DBResult where Self: Sized, { Ok(Self(::build_from(adata, 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::ConcreteDatum for #id_ident {} 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; fn build(values: ::DatumList) -> Self { Self { #(#build_struct),* } } fn entity_name() -> &'static str { #entity_name } fn accept_part_visitor(v: &mut impl ::microrm::schema::entity::EntityPartVisitor) { #( #part_visit );* } fn accept_part_visitor_ref(&self, v: &mut impl ::microrm::schema::entity::EntityPartVisitor) { #( #part_ref_visit );* } fn accept_part_visitor_mut(&mut self, v: &mut impl ::microrm::schema::entity::EntityPartVisitor) { #( #part_mut_visit );* } } } .into() }