use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;
use crate::entity::parse::{EntityDef, SqlLevel};
pub fn generate(entity: &EntityDef) -> TokenStream {
if !entity.is_aggregate_root() || entity.sql == SqlLevel::None {
return TokenStream::new();
}
let new_struct = generate_new_struct(entity);
let from_impl = generate_from_impl(entity);
quote! {
#new_struct
#from_impl
}
}
fn generate_new_struct(entity: &EntityDef) -> TokenStream {
let vis = &entity.vis;
let new_name = entity.ident_with("New", "");
let fields = get_new_fields(entity);
let field_defs = fields.iter().map(|(name, ty)| {
quote! { pub #name: #ty }
});
let marker = crate::utils::marker::generated();
quote! {
#marker
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
#vis struct #new_name {
#(#field_defs),*
}
}
}
fn generate_from_impl(entity: &EntityDef) -> TokenStream {
let entity_name = entity.name();
let new_name = entity.ident_with("New", "");
let id_field = entity.id_field();
let id_name = id_field.name();
let new_fields = get_new_fields(entity);
let new_field_set: std::collections::HashSet<&Ident> =
new_fields.iter().map(|(name, _)| name).collect();
let mut assignments: Vec<TokenStream> = Vec::new();
assignments.push(quote! { #id_name: uuid::Uuid::new_v4() });
for (name, _) in &new_fields {
assignments.push(quote! { #name: new.#name });
}
for field in entity.all_fields() {
if !field.is_id() && !new_field_set.contains(field.name()) {
let name = field.name();
assignments.push(quote! { #name: Default::default() });
}
}
let marker = crate::utils::marker::generated();
quote! {
#marker
impl From<#new_name> for #entity_name {
fn from(new: #new_name) -> Self {
Self {
#(#assignments),*
}
}
}
}
}
fn get_new_fields(entity: &EntityDef) -> Vec<(Ident, syn::Type)> {
let mut fields = Vec::new();
for field in entity.create_fields() {
let name = field.name().clone();
let ty = field.ty().clone();
fields.push((name, ty));
}
fields
}
#[cfg(test)]
mod tests {
use syn::{DeriveInput, parse_quote};
use super::*;
use crate::entity::parse::EntityDef;
fn parse_entity(tokens: proc_macro2::TokenStream) -> EntityDef {
let input: DeriveInput = parse_quote!(#tokens);
EntityDef::from_derive_input(&input).unwrap()
}
#[test]
fn new_fields_excludes_id() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root)]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
}
});
let fields = get_new_fields(&entity);
let names: Vec<String> = fields.iter().map(|(n, _)| n.to_string()).collect();
assert!(!names.contains(&"id".to_string()));
assert!(names.contains(&"buyer_id".to_string()));
}
#[test]
fn new_fields_excludes_auto() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root)]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
#[field(response)]
#[auto]
pub created_at: chrono::DateTime<chrono::Utc>,
}
});
let fields = get_new_fields(&entity);
let names: Vec<String> = fields.iter().map(|(n, _)| n.to_string()).collect();
assert!(!names.contains(&"created_at".to_string()));
}
#[test]
fn new_struct_no_id_field() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root)]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
}
});
let ts = generate_new_struct(&entity);
let code = ts.to_string();
assert!(!code.contains("id:"));
assert!(code.contains("buyer_id"));
}
#[test]
fn from_impl_generates_uuid() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root)]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
}
});
let ts = generate_from_impl(&entity);
let code = ts.to_string();
assert!(code.contains("new_v4"));
}
#[test]
fn generate_returns_empty_when_not_aggregate_root() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders")]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
}
});
let ts = generate(&entity);
assert!(ts.is_empty());
}
#[test]
fn generate_returns_empty_when_sql_none() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root, sql = "none")]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
}
});
let ts = generate(&entity);
assert!(ts.is_empty());
}
#[test]
fn new_struct_includes_create_fields() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root)]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
#[field(create)]
pub seller_id: uuid::Uuid,
}
});
let ts = generate_new_struct(&entity);
let code = ts.to_string();
assert!(code.contains("buyer_id"));
assert!(code.contains("seller_id"));
}
#[test]
fn from_impl_copies_fields() {
let entity = parse_entity(quote::quote! {
#[entity(table = "orders", aggregate_root)]
pub struct Order {
#[id]
pub id: uuid::Uuid,
#[field(create)]
pub buyer_id: uuid::Uuid,
}
});
let ts = generate_from_impl(&entity);
let code = ts.to_string();
assert!(code.contains("buyer_id"));
assert!(code.contains("new"));
}
}