use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use syn::DeriveInput;
use syn::Fields;
use syn::Result;
use syn::spanned::Spanned;
use crate::attr;
struct Column {
ident: syn::Ident,
ty: syn::Type,
name: String,
nullable: bool,
pk: bool,
auto_increment: bool,
}
fn is_option(ty: &syn::Type) -> bool {
if let syn::Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "Option"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& args.args.len() == 1
&& let Some(syn::GenericArgument::Type(_)) = args.args.first()
{
return true;
}
false
}
struct RelationOut {
variant: syn::Ident,
ident: syn::Ident,
name: String,
kind: attr::RelationKind,
target: syn::Path,
foreign_key: String,
local_key: TokenStream,
default: TokenStream,
}
fn relation_default(ty: &syn::Type, span: proc_macro2::Span) -> Result<TokenStream> {
if let syn::Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
{
if seg.ident == "Vec" {
return Ok(quote!(::std::vec::Vec::new()));
}
if seg.ident == "Option" {
return Ok(quote!(::core::option::Option::None));
}
}
Err(syn::Error::new(
span,
"a relation field must be `Vec<Target>` (has_many) or `Option<Target>` (has_one/belongs_to)",
))
}
pub fn expand(input: DeriveInput) -> Result<TokenStream> {
let container = attr::parse_container(&input.attrs)?;
let struct_ident = input.ident.clone();
let table = container
.table
.unwrap_or_else(|| attr::to_snake_case(&struct_ident.to_string()));
let named = match &input.data {
syn::Data::Struct(s) => match &s.fields {
Fields::Named(n) => n,
_ => {
return Err(syn::Error::new(
input.span(),
"Entity requires a struct with named fields",
));
}
},
_ => {
return Err(syn::Error::new(
input.span(),
"Entity can only be derived for structs",
));
}
};
let mut columns: Vec<Column> = Vec::new();
let mut relations: Vec<RelationOut> = Vec::new();
let mut flags = FlagsAcc::default();
let mut pk_ident: Option<syn::Ident> = None;
let mut pk_name: Option<String> = None;
for field in &named.named {
let ident = field.ident.clone().expect("named field");
let fa = attr::parse_field(&field.attrs)?;
if let Some(rel) = &fa.relation {
let target = rel.target.clone();
let local_key = match (&rel.local_key, rel.kind) {
(Some(k), _) => quote!(#k),
(None, attr::RelationKind::BelongsTo) => {
quote!(<#target as ::ecat_orm::Entity>::PK)
}
(None, _) => quote!(<#struct_ident as ::ecat_orm::Entity>::PK),
};
let name = ident.to_string();
relations.push(RelationOut {
variant: format_ident!("{}", attr::to_pascal_case(&name)),
ident,
name,
kind: rel.kind,
target,
foreign_key: rel.foreign_key.clone(),
local_key,
default: relation_default(&field.ty, field.span())?,
});
continue; }
let name = fa.column.clone().unwrap_or_else(|| ident.to_string());
let nullable = is_option(&field.ty);
if fa.created_at {
flags.created_at = Some(name.clone());
}
if fa.updated_at {
flags.updated_at = Some(name.clone());
}
if fa.soft_delete {
flags.soft_delete = Some(name.clone());
}
if fa.version {
flags.version = Some(name.clone());
}
if fa.pk {
if pk_ident.is_some() {
return Err(syn::Error::new(
field.span(),
"multiple `pk` fields; ecat-orm does not support composite primary keys",
));
}
pk_ident = Some(ident.clone());
pk_name = Some(name.clone());
}
columns.push(Column {
ident,
ty: field.ty.clone(),
name,
nullable,
pk: fa.pk,
auto_increment: fa.auto_increment,
});
}
let pk_ident = pk_ident.ok_or_else(|| {
syn::Error::new(
struct_ident.span(),
"no `#[entity(pk)]` field; every entity needs a single-column primary key",
)
})?;
let pk_name = pk_name.expect("set alongside pk_ident");
let col_names: Vec<_> = columns.iter().map(|c| c.name.as_str()).collect();
let col_tys: Vec<_> = columns.iter().map(|c| &c.ty).collect();
let col_nullable: Vec<_> = columns.iter().map(|c| c.nullable).collect();
let col_pk: Vec<_> = columns.iter().map(|c| c.pk).collect();
let col_ai: Vec<_> = columns.iter().map(|c| c.auto_increment).collect();
let col_idents: Vec<_> = columns.iter().map(|c| &c.ident).collect();
let rel_variants: Vec<_> = relations.iter().map(|r| &r.variant).collect();
let rel_idents: Vec<_> = relations.iter().map(|r| &r.ident).collect();
let rel_names: Vec<_> = relations.iter().map(|r| r.name.as_str()).collect();
let rel_kinds: Vec<_> = relations
.iter()
.map(|r| match r.kind {
attr::RelationKind::HasMany => quote!(::ecat_orm::RelationKind::HasMany),
attr::RelationKind::HasOne => quote!(::ecat_orm::RelationKind::HasOne),
attr::RelationKind::BelongsTo => quote!(::ecat_orm::RelationKind::BelongsTo),
})
.collect();
let rel_targets: Vec<_> = relations.iter().map(|r| &r.target).collect();
let rel_fks: Vec<_> = relations.iter().map(|r| r.foreign_key.as_str()).collect();
let rel_lks: Vec<_> = relations.iter().map(|r| &r.local_key).collect();
let rel_defaults: Vec<_> = relations.iter().map(|r| &r.default).collect();
let rel_setters: Vec<_> = relations
.iter()
.map(|r| {
let field = &r.ident;
let target = &r.target;
if r.kind == attr::RelationKind::HasMany {
quote! {
let mut out = ::std::vec::Vec::with_capacity(rows.len());
for r in &rows {
out.push(<#target as ::ecat_orm::Entity>::from_row(r)?);
}
self.#field = out;
}
} else {
quote! {
self.#field = match rows.first() {
::core::option::Option::Some(r) => ::core::option::Option::Some(
<#target as ::ecat_orm::Entity>::from_row(r)?,
),
::core::option::Option::None => ::core::option::Option::None,
};
}
}
})
.collect();
let unknown_relation = quote! {
::ecat_orm::OrmError::UnknownColumn(
::std::format!("unknown relation `{name}`"),
)
};
let set_relation = if relations.is_empty() {
quote! {
fn set_relation(
&mut self,
name: &str,
_rows: ::std::vec::Vec<::ecat_orm::Row>,
) -> ::core::result::Result<(), ::ecat_orm::OrmError> {
::core::result::Result::Err(#unknown_relation)
}
}
} else {
quote! {
fn set_relation(
&mut self,
name: &str,
rows: ::std::vec::Vec<::ecat_orm::Row>,
) -> ::core::result::Result<(), ::ecat_orm::OrmError> {
match name {
#(
#rel_names => {
#rel_setters
}
)*
_ => {
return ::core::result::Result::Err(#unknown_relation);
}
}
::core::result::Result::Ok(())
}
}
};
let rel_enum = (!relations.is_empty()).then(|| {
let enum_ident = format_ident!("{}Relation", struct_ident);
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum #enum_ident {
#( #rel_variants, )*
}
impl #enum_ident {
pub fn name(self) -> &'static str {
match self {
#( Self::#rel_variants => #rel_names, )*
}
}
}
impl ::ecat_orm::relation::RelationSelector for #enum_ident {
fn name(self) -> &'static str {
self.name()
}
}
}
});
let opt_str = |v: &Option<String>| match v {
Some(s) => quote!(::core::option::Option::Some(#s)),
None => quote!(::core::option::Option::None),
};
let created_at = opt_str(&flags.created_at);
let updated_at = opt_str(&flags.updated_at);
let soft_delete = opt_str(&flags.soft_delete);
let version = opt_str(&flags.version);
Ok(quote! {
const _: () = {
impl ::ecat_orm::Entity for #struct_ident {
const TABLE: &'static str = #table;
const PK: &'static str = #pk_name;
const META: &'static ::ecat_orm::EntityMeta = &META;
fn from_row(
row: &::ecat_orm::Row,
) -> ::core::result::Result<Self, ::ecat_orm::OrmError> {
::core::result::Result::Ok(Self {
#(
#col_idents: ::ecat_orm::value::from_row_col::<#col_tys>(row, #col_names)?,
)*
#( #rel_idents: #rel_defaults, )*
})
}
fn to_values(
&self,
) -> ::std::vec::Vec<(&'static str, ::ecat_orm::serde_json::Value)> {
::std::vec![
#(
(
#col_names,
::ecat_orm::value::ColumnValue::to_json(&self.#col_idents),
),
)*
]
}
fn pk_value(&self) -> ::ecat_orm::serde_json::Value {
::ecat_orm::value::ColumnValue::to_json(&self.#pk_ident)
}
#set_relation
}
#[doc(hidden)]
#[allow(non_upper_case_globals)]
static META: ::ecat_orm::EntityMeta = ::ecat_orm::EntityMeta {
table: #table,
pk: #pk_name,
columns: &[
#(
::ecat_orm::ColumnMeta {
name: #col_names,
ty: <#col_tys as ::ecat_orm::value::ColumnValue>::COL_TYPE,
nullable: #col_nullable,
pk: #col_pk,
auto_increment: #col_ai,
},
)*
],
relations: &[
#(
::ecat_orm::RelationMeta {
name: #rel_names,
kind: #rel_kinds,
target_table: <#rel_targets as ::ecat_orm::Entity>::TABLE,
foreign_key: #rel_fks,
local_key: #rel_lks,
},
)*
],
flags: ::ecat_orm::EntityFlags {
created_at: #created_at,
updated_at: #updated_at,
soft_delete: #soft_delete,
version: #version,
},
};
};
#rel_enum
})
}
#[derive(Default)]
struct FlagsAcc {
created_at: Option<String>,
updated_at: Option<String>,
soft_delete: Option<String>,
version: Option<String>,
}