microrm-macros 0.2.2

Procedural macro implementations for the microrm crate.
Documentation
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};

use convert_case::{Case, Casing};

fn parse_fk(attrs: &[syn::Attribute]) -> bool {
    for attr in attrs {
        if attr.path.segments.len() == 1
            && attr.path.segments.last().unwrap().ident == "microrm_foreign"
        {
            return true;
        }
    }

    false
}

fn derive_columns<'a, I: Iterator<Item = &'a syn::Field>>(
    input: &DeriveInput,
    microrm_ref: &proc_macro2::TokenStream,
    fields: I,
) -> proc_macro2::TokenStream {
    let struct_name = &input.ident;
    let columns_name = format_ident!("_{}_columns", &input.ident.to_string().to_case(Case::Snake));
    let mut index = 0usize;

    let mut column_types = Vec::new();
    let mut column_impls = Vec::new();
    let mut column_consts = Vec::new();
    let mut column_array = Vec::new();

    for name in fields {
        let original_case = name.ident.as_ref().unwrap().clone();
        let snake_case = original_case.to_string();
        if snake_case != snake_case.to_case(Case::Snake) {
            return quote::quote_spanned!(original_case.span() => compile_error!("Names must be in snake_case"));
        }

        let converted_case =
            format_ident!("{}", original_case.to_string().to_case(Case::UpperCamel));

        let ty = &name.ty;

        let mut fk_table_name = quote! { None };
        let mut fk_column_name = quote! { None };

        if parse_fk(&name.attrs) {
            fk_table_name = quote! { Some(<<#ty as #microrm_ref::entity::EntityID>::Entity as #microrm_ref::entity::Entity>::table_name()) };
            fk_column_name = quote! { Some("id") };
        }

        index += 1;
        column_types.push(quote! { pub struct #converted_case (); });
        column_impls.push(quote! {
            impl #microrm_ref::entity::EntityColumn for #columns_name::#converted_case {
                type Entity = #struct_name;

                fn sql_type(&self) -> &'static str { <#ty as #microrm_ref::model::Modelable>::column_type() }
                fn index(&self) -> usize { #index }
                fn name(&self) -> &'static str { #snake_case }

                fn fk_table_name(&self) -> Option<&'static str> { #fk_table_name }
                fn fk_column_name(&self) -> Option<&'static str> { #fk_column_name }
            }
        });
        column_consts.push(quote! {
            #[allow(non_upper_case_globals)]
            pub const #converted_case : #columns_name::#converted_case = #columns_name::#converted_case();
        });

        column_array.push(quote! { & #columns_name::#converted_case() });
    }

    let columns_array_name = format_ident!(
        "{}_COLUMNS",
        struct_name.to_string().to_case(Case::ScreamingSnake)
    );

    quote! {
        pub mod #columns_name {
            pub struct ID ();
            #(#column_types)*
        }

        impl #microrm_ref::entity::EntityColumn for #columns_name::ID {
            type Entity = #struct_name;
            fn sql_type(&self) -> &'static str { "integer" }
            fn index(&self) -> usize { 0 }
            fn name(&self) -> &'static str { "id" }

            fn fk_table_name(&self) -> Option<&'static str> { None }
            fn fk_column_name(&self) -> Option<&'static str> { None }
        }
        #(#column_impls)*

        impl #struct_name {
            const ID : #columns_name::ID = #columns_name::ID();
            #(#column_consts)*
        }

        #microrm_ref::re_export::lazy_static::lazy_static!{
            static ref #columns_array_name: [&'static dyn #microrm_ref::entity::EntityColumn<Entity = #struct_name>;#index + 1] = {
                [ &#columns_name::ID(), #(#column_array),* ]
            };
        }
    }
}

fn derive_id(
    input: &DeriveInput,
    microrm_ref: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
    let struct_name = &input.ident;
    let id_name = format_ident!("{}ID", &input.ident);

    quote! {
        #[derive(Debug,PartialEq,Clone,Copy,#microrm_ref::re_export::serde::Serialize,#microrm_ref::re_export::serde::Deserialize)]
        #[allow(unused)]
        pub struct #id_name (i64);

        impl #microrm_ref::entity::EntityID for #id_name {
            type Entity = #struct_name;
            fn from_raw_id(raw: i64) -> Self { Self(raw) }
            fn raw_id(&self) -> i64 { self.0 }
        }

        impl #microrm_ref::model::Modelable for #id_name {
            fn bind_to(&self, stmt: &mut #microrm_ref::re_export::sqlite::Statement, col: usize) -> #microrm_ref::re_export::sqlite::Result<()> {
                use #microrm_ref::re_export::sqlite::Bindable;
                self.0.bind(stmt, col)
            }
            fn build_from(stmt: &#microrm_ref::re_export::sqlite::Statement, col_offset: usize) -> #microrm_ref::re_export::sqlite::Result<(Self, usize)> where Self: Sized {
                stmt.read::<i64>(col_offset).map(|x| (#id_name(x), 1))
            }
            fn column_type() -> &'static str where Self: Sized {
                "integer"
            }
        }
    }
}

pub(crate) fn derive(tokens: TokenStream) -> TokenStream {
    let input = parse_macro_input!(tokens as DeriveInput);

    let microrm_ref = crate::parse_microrm_ref(&input.attrs);

    let struct_name = &input.ident;
    let id_name = format_ident!("{}ID", &input.ident);

    let table_name = format!("{}", struct_name).to_case(Case::Snake);

    let st = match &input.data {
        syn::Data::Struct(st) => st,
        _ => panic!("Can only use derive(Entity) on structs!"),
    };
    let fields = match &st.fields {
        syn::Fields::Named(fields) => fields,
        _ => panic!("Can only use derive(Entity) on non-unit structs with named fields!"),
    };

    let mut variants = Vec::new();
    let mut value_references = Vec::new();
    let mut build_clauses = Vec::new();

    // let mut foreign_keys = Vec::new();
    // let mut foreign_key_impls = Vec::new();

    let mut index: usize = 0;

    let column_output = derive_columns(&input, &microrm_ref, fields.named.iter());
    let id_output = derive_id(&input, &microrm_ref);

    for name in fields.named.iter() {
        let original_case = name.ident.as_ref().unwrap().clone();
        let snake_case = original_case.to_string().to_case(Case::Snake);

        if original_case != snake_case {
            return quote::quote_spanned!(original_case.span() => compile_error!("Names must be in snake_case")).into();
        }

        let converted_case =
            format_ident!("{}", original_case.to_string().to_case(Case::UpperCamel));
        variants.push(converted_case.clone());

        let field_name = name.ident.as_ref().unwrap().clone();
        value_references.push(quote! { &self. #field_name });

        let ty = &name.ty;

        index += 1;
        build_clauses.push(quote! { #field_name: <#ty as #microrm_ref::model::Modelable>::build_from(stmt, #index)?.0 });

        /*
        if parse_fk(&name.attrs) {
            let fk_struct_name = format_ident!("{}{}ForeignKey", struct_name, converted_case);
            let ty = &name.ty;
            foreign_keys.push(quote! {
                &#fk_struct_name { col: #columns_name::#converted_case }
            });
            foreign_key_impls.push(quote!{
                struct #fk_struct_name {
                    col: #columns_name
                }
                impl #microrm_ref::entity::EntityForeignKey<#columns_name> for #fk_struct_name {
                    /*
                    fn local_column(&self) -> &#columns_name { &self.col }
                    fn foreign_table_name(&self) -> &'static str {
                        <<#ty as #microrm_ref::entity::EntityID>::Entity as #microrm_ref::entity::Entity>::table_name()
                    }
                    fn foreign_column_name(&self) -> &'static str {
                        "id"
                    }
                    */
                }
            });
        }
        */
    }

    let columns_array_name = format_ident!(
        "{}_COLUMNS",
        struct_name.to_string().to_case(Case::ScreamingSnake)
    );

    let field_count = fields.named.iter().count();

    quote!{
        // Related types for #struct_name

        #column_output
        #id_output

        // Implementations for #struct_name
        impl #microrm_ref::entity::Entity for #struct_name {
            type ID = #id_name;

            fn table_name() -> &'static str { #table_name }
            fn column_count() -> usize {
                // +1 for ID column
                #field_count + 1
            }
            fn values(&self) -> Vec<&dyn #microrm_ref::model::Modelable> {
                vec![ #(#value_references),* ]
            }

            fn build_from(stmt: &#microrm_ref::re_export::sqlite::Statement) -> #microrm_ref::re_export::sqlite::Result<Self> {
                Ok(Self {
                    #(#build_clauses),*
                })
            }

            fn columns() -> &'static [&'static dyn #microrm_ref::entity::EntityColumn<Entity = Self>] {
                #columns_array_name.as_ref()
            }

            /*fn foreign_keys() -> &'static [&'static dyn #microrm_ref::entity::EntityForeignKey<Self::Column>] {
                &[#(#foreign_keys),*]
            }*/
        }

        // Foreign key struct implementations
        // #(#foreign_key_impls)*
    }.into()
}