microrm-macros 0.5.0

Procedural macro implementations for the microrm crate.
Documentation
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!")
        },
    };

    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;

    // collect list of unique parts
    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)]
            #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_impl = match is_migratable(&input.attrs) {
        Some(syn::Meta::List(mlist)) => {
            assert_eq!(mlist.nested.len(), 1);
            let syn::NestedMeta::Meta(syn::Meta::Path(mpath)) = &mlist.nested[0] else {
                panic!()
            };
            let field_names = parts.iter().map(|v| &v.0);
            let field_types = parts.iter().map(|v| &v.1);
            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 )?
                                ),*
                            }
                        ))
                    }
                }
            }
        },
        Some(m) => panic!("migrate attr must be given a path: {m:?}"),
        None => quote! {},
    };

    quote! {
        #(#part_defs)*

        impl #entity_ident {
            #(#part_names)*
            #(#part_indices)*
        }

        #[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) {
                <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::ConcreteDatum for #id_ident {}

        #[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;

            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>) {
                #(
                    #part_ref_visit
                );*
            }

            fn accept_part_visitor_mut(&mut self, v: &mut impl ::microrm::schema::entity::EntityPartVisitor<Entity = Self>) {
                #(
                    #part_mut_visit
                );*
            }
        }

        #migrate_impl
    }
    .into()
}