use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, LitStr};
#[proc_macro_derive(Entity, attributes(orm))]
pub fn derive_entity(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident.clone();
struct IndexDef {
columns: Vec<String>,
unique: bool,
}
let mut table = to_snake_case(&ident.to_string());
let mut composite: Vec<IndexDef> = Vec::new();
for attr in &input.attrs {
if attr.path().is_ident("orm") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("table") {
table = meta.value()?.parse::<LitStr>()?.value();
} else if meta.path.is_ident("index") {
composite.push(IndexDef {
columns: split_columns(&meta.value()?.parse::<LitStr>()?.value()),
unique: false,
});
} else if meta.path.is_ident("unique") {
composite.push(IndexDef {
columns: split_columns(&meta.value()?.parse::<LitStr>()?.value()),
unique: true,
});
}
Ok(())
});
}
}
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(n) => n.named.clone(),
_ => return compile_err("Entity requires named struct fields"),
},
_ => return compile_err("Entity can only be derived for a struct"),
};
struct FieldMeta {
ident: syn::Ident,
ty: syn::Type,
column: String,
pk: bool,
auto_increment: bool,
unique: bool,
index: bool,
shard: bool,
references: Option<(String, String)>, on_delete: Option<String>,
on_update: Option<String>,
}
let mut metas: Vec<FieldMeta> = Vec::new();
let mut pk: Option<(syn::Ident, String)> = None;
for f in &fields {
let fident = f.ident.clone().unwrap();
let mut column = fident.to_string();
let mut is_pk = false;
let mut auto_increment = false;
let mut unique = false;
let mut index = false;
let mut shard = false;
let mut references = None;
let mut on_delete = None;
let mut on_update = None;
for attr in &f.attrs {
if attr.path().is_ident("orm") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("pk") {
is_pk = true;
} else if meta.path.is_ident("auto_increment") {
auto_increment = true;
} else if meta.path.is_ident("unique") {
unique = true;
} else if meta.path.is_ident("index") {
index = true;
} else if meta.path.is_ident("shard_key") {
shard = true;
} else if meta.path.is_ident("column") {
column = meta.value()?.parse::<LitStr>()?.value();
} else if meta.path.is_ident("references") {
let v = meta.value()?.parse::<LitStr>()?.value();
references = Some(parse_references(&v));
} else if meta.path.is_ident("on_delete") {
on_delete = Some(meta.value()?.parse::<LitStr>()?.value());
} else if meta.path.is_ident("on_update") {
on_update = Some(meta.value()?.parse::<LitStr>()?.value());
}
Ok(())
});
}
}
if is_pk {
pk = Some((fident.clone(), column.clone()));
}
metas.push(FieldMeta {
ident: fident,
ty: f.ty.clone(),
column,
pk: is_pk,
auto_increment,
unique,
index,
shard,
references,
on_delete,
on_update,
});
}
let Some((pk_ident, pk_col)) = pk else {
return compile_err("Entity needs exactly one field marked `#[orm(pk)]`");
};
let col_specs = metas.iter().map(|m| {
let name = &m.column;
let ty = &m.ty;
let is_pk = m.pk;
let ai = m.auto_increment;
let uniq = m.unique;
quote! {
::rainier_orm::ColumnSpec {
name: #name,
ty: <#ty as ::rainier_orm::SqlType>::COLUMN_TYPE,
nullable: <#ty as ::rainier_orm::SqlType>::NULLABLE,
pk: #is_pk,
auto_increment: #ai,
unique: #uniq,
}
}
});
let mut index_specs: Vec<TokenStream2> = Vec::new();
for m in &metas {
if m.index {
let name = format!("idx_{table}_{}", m.column);
let col = &m.column;
index_specs.push(quote! {
::rainier_orm::IndexSpec { name: #name, columns: &[#col], unique: false }
});
}
}
for ix in &composite {
let joined = ix.columns.join("_");
let prefix = if ix.unique { "uq" } else { "idx" };
let name = format!("{prefix}_{table}_{joined}");
let cols = &ix.columns;
let uniq = ix.unique;
index_specs.push(quote! {
::rainier_orm::IndexSpec { name: #name, columns: &[#(#cols),*], unique: #uniq }
});
}
let fk_specs = metas.iter().filter_map(|m| {
let (ftable, fcol) = m.references.as_ref()?;
let name = format!("fk_{table}_{}", m.column);
let col = &m.column;
let on_delete = ref_action(m.on_delete.as_deref());
let on_update = ref_action(m.on_update.as_deref());
Some(quote! {
::rainier_orm::ForeignKeySpec {
name: #name,
columns: &[#col],
foreign_table: #ftable,
foreign_columns: &[#fcol],
on_delete: #on_delete,
on_update: #on_update,
}
})
});
let shard_cols = metas.iter().filter(|m| m.shard).map(|m| {
let c = &m.column;
quote! { #c }
});
let from_row_fields = metas.iter().map(|m| {
let fident = &m.ident;
let ty = &m.ty;
let col = &m.column;
quote! { #fident: <#ty as ::rainier_orm::FromColumn>::from_column(row, #col)? }
});
let insert_vals = metas.iter().filter(|m| !m.auto_increment).map(|m| {
let fident = &m.ident;
let col = &m.column;
quote! { (#col, ::rainier_orm::ToColumn::to_value(&self.#fident)) }
});
let update_vals = metas.iter().filter(|m| !m.pk).map(|m| {
let fident = &m.ident;
let col = &m.column;
quote! { (#col, ::rainier_orm::ToColumn::to_value(&self.#fident)) }
});
quote! {
impl ::rainier_orm::Entity for #ident {
fn table() -> &'static str { #table }
fn columns() -> &'static [::rainier_orm::ColumnSpec] {
&[ #(#col_specs),* ]
}
fn primary_key() -> &'static str { #pk_col }
fn indexes() -> &'static [::rainier_orm::IndexSpec] {
&[ #(#index_specs),* ]
}
fn foreign_keys() -> &'static [::rainier_orm::ForeignKeySpec] {
&[ #(#fk_specs),* ]
}
fn shard_columns() -> &'static [&'static str] {
&[ #(#shard_cols),* ]
}
fn from_row(row: &dyn ::rainier_orm::Row)
-> ::core::result::Result<Self, ::rainier_orm::Error>
{
::core::result::Result::Ok(Self { #(#from_row_fields),* })
}
fn insert_values(&self)
-> ::std::vec::Vec<(&'static str, ::rainier_orm::sea_query::Value)>
{
::std::vec![ #(#insert_vals),* ]
}
fn update_values(&self)
-> ::std::vec::Vec<(&'static str, ::rainier_orm::sea_query::Value)>
{
::std::vec![ #(#update_vals),* ]
}
fn pk_value(&self) -> ::rainier_orm::sea_query::Value {
::rainier_orm::ToColumn::to_value(&self.#pk_ident)
}
}
}
.into()
}
fn split_columns(s: &str) -> Vec<String> {
s.split(',').map(|c| c.trim().to_string()).filter(|c| !c.is_empty()).collect()
}
fn parse_references(s: &str) -> (String, String) {
let s = s.trim();
if let Some(open) = s.find('(') {
let table = s[..open].trim().to_string();
let col = s[open + 1..].trim_end_matches([')', ' ']).trim().to_string();
(table, col)
} else if let Some(dot) = s.find('.') {
(s[..dot].trim().to_string(), s[dot + 1..].trim().to_string())
} else {
(s.to_string(), "id".to_string())
}
}
fn ref_action(s: Option<&str>) -> TokenStream2 {
let Some(s) = s else {
return quote! { ::core::option::Option::None };
};
let variant = match s.trim().to_ascii_lowercase().replace([' ', '-'], "_").as_str() {
"cascade" => quote! { Cascade },
"restrict" => quote! { Restrict },
"set_null" | "setnull" => quote! { SetNull },
"set_default" | "setdefault" => quote! { SetDefault },
"no_action" | "noaction" => quote! { NoAction },
_ => return quote! { ::core::option::Option::None },
};
quote! { ::core::option::Option::Some(::rainier_orm::RefAction::#variant) }
}
fn to_snake_case(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() {
if i != 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
fn compile_err(msg: &str) -> TokenStream {
quote! { compile_error!(#msg); }.into()
}
#[proc_macro_derive(Factory)]
pub fn derive_factory(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident;
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
quote! {
impl #impl_generics ::rainier_database::factory::HasFactory for #ident #type_generics
#where_clause
{
fn factory() -> ::rainier_database::factory::Factory<Self> {
::rainier_database::factory::Factory::new(|_index| <Self as Default>::default())
}
}
}
.into()
}