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 {
pub 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);
let id_name_as_str = id_name.to_string();
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 std::fmt::Display for #id_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}({})", #id_name_as_str, 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 index: usize = 0;
let column_output = derive_columns(&input, µrm_ref, fields.named.iter());
let id_output = derive_id(&input, µrm_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 });
}
let columns_array_name = format_ident!(
"{}_COLUMNS",
struct_name.to_string().to_case(Case::ScreamingSnake)
);
let field_count = fields.named.iter().count();
let columns_name = format_ident!("_{}_columns", &input.ident.to_string().to_case(Case::Snake));
quote!{
#column_output
#id_output
impl #microrm_ref::entity::Entity for #struct_name {
type ID = #id_name;
type IDColumn = #columns_name::ID;
fn table_name() -> &'static str { #table_name }
fn column_count() -> usize {
#field_count + 1
}
fn visit_values<E, F: FnMut(&dyn #microrm_ref::model::Modelable) -> (Result<(), E>)>(&self, visit: &mut F) -> Result<(),E> {
#(visit(#value_references)?);*;
Ok(())
}
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 id_column() -> Self::IDColumn {
Self::ID
}
}
}.into()
}