use crate::util::validate_sql_identifier;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Fields, LitStr, Type};
struct RelationField {
ident: syn::Ident,
many: bool,
child_ty: Type,
parent_key: syn::Ident,
entity_key: syn::Ident,
entity_column: String,
junction: Option<JunctionMeta>,
}
struct JunctionMeta {
table: String,
parent_key: String,
entity_key: String,
}
fn key_ident(s: &str, what: &str, attr: &syn::Attribute) -> syn::Result<syn::Ident> {
syn::parse_str::<syn::Ident>(s).map_err(|_| {
syn::Error::new(
attr.span(),
format!(
"{what} \"{s}\" 는 유효한 러스트 식별자여야 합니다 — SQL 컬럼명이 필드명과 다르면 entity_column 을 사용하세요"
),
)
})
}
fn unwrap_container(ty: &Type) -> Option<(bool, Type)> {
if let Type::Path(tp) = ty {
if let Some(seg) = tp.path.segments.last() {
if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
if let Some(syn::GenericArgument::Type(inner)) = ab.args.first() {
if seg.ident == "Vec" {
return Some((true, inner.clone()));
}
if seg.ident == "Option" {
return Some((false, inner.clone()));
}
}
}
}
}
None
}
pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
let input: DeriveInput = syn::parse2(input)?;
let ident = input.ident.clone();
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new(
input.span(),
"#[derive(Relation)]은 구조체 전용입니다",
));
};
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new(
input.span(),
"named 필드 구조체만 지원합니다",
));
};
let mut embedded: Option<(syn::Ident, Type)> = None;
let mut relations: Vec<RelationField> = Vec::new();
for field in &fields.named {
let fident = field.ident.clone().expect("named 필드 보장");
let mut is_embedded = false;
let mut rel: Option<RelationField> = None;
for attr in &field.attrs {
if attr.path().is_ident("embedded") {
is_embedded = true;
} else if attr.path().is_ident("relation") {
let Some((many, child_ty)) = unwrap_container(&field.ty) else {
return Err(syn::Error::new(
field.ty.span(),
"관계 필드는 Vec<Child>(1:N/N:M) 또는 Option<Child>(1:1)여야 합니다",
));
};
let mut parent_key = None;
let mut entity_key = None;
let mut entity_column = None;
let mut junction_table = None;
let mut j_parent = None;
let mut j_entity = None;
attr.parse_nested_meta(|meta| {
let get = |m: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
let lit = m.value()?.parse::<LitStr>()?;
validate_sql_identifier(&lit.value(), lit.span())?;
Ok(lit.value())
};
if meta.path.is_ident("entity") {
let _: syn::Path = meta.value()?.parse()?;
Ok(())
} else if meta.path.is_ident("parent_key") {
parent_key = Some(get(&meta)?);
Ok(())
} else if meta.path.is_ident("entity_key") {
entity_key = Some(get(&meta)?);
Ok(())
} else if meta.path.is_ident("entity_column") {
entity_column = Some(get(&meta)?);
Ok(())
} else if meta.path.is_ident("junction") {
junction_table = Some(get(&meta)?);
Ok(())
} else if meta.path.is_ident("junction_parent_key") {
j_parent = Some(get(&meta)?);
Ok(())
} else if meta.path.is_ident("junction_entity_key") {
j_entity = Some(get(&meta)?);
Ok(())
} else {
Err(meta.error(
"알 수 없는 relation 인자 — entity/parent_key/entity_key/entity_column/junction/junction_parent_key/junction_entity_key",
))
}
})?;
let parent_key = parent_key.ok_or_else(|| {
syn::Error::new(attr.span(), "parent_key = \"…\" 가 필요합니다")
})?;
let entity_key = entity_key.ok_or_else(|| {
syn::Error::new(attr.span(), "entity_key = \"…\" 가 필요합니다")
})?;
let entity_column = entity_column.unwrap_or_else(|| entity_key.clone());
let parent_key = key_ident(&parent_key, "parent_key", attr)?;
let entity_key = key_ident(&entity_key, "entity_key", attr)?;
let junction = match (junction_table, j_parent, j_entity) {
(None, None, None) => None,
(Some(t), Some(p), Some(e)) => Some(JunctionMeta {
table: t,
parent_key: p,
entity_key: e,
}),
_ => {
return Err(syn::Error::new(
attr.span(),
"N:M은 junction/junction_parent_key/junction_entity_key 세 개가 모두 필요합니다",
));
}
};
rel = Some(RelationField {
ident: fident.clone(),
many,
child_ty,
parent_key,
entity_key,
entity_column,
junction,
});
}
}
if is_embedded && rel.is_some() {
return Err(syn::Error::new(
fident.span(),
"한 필드에 #[embedded]와 #[relation]을 함께 사용할 수 없습니다",
));
}
if is_embedded {
if embedded.is_some() {
return Err(syn::Error::new(
fident.span(),
"#[embedded] 필드는 1개만 허용됩니다",
));
}
embedded = Some((fident, field.ty.clone()));
} else if let Some(r) = rel {
relations.push(r);
} else {
return Err(syn::Error::new(
fident.span(),
"관계 뷰의 모든 필드는 #[embedded] 또는 #[relation(...)]이어야 합니다",
));
}
}
let Some((parent_ident, parent_ty)) = embedded else {
return Err(syn::Error::new(
input.span(),
"#[embedded] 부모 필드가 필요합니다",
));
};
let mut loaders: Vec<TokenStream> = Vec::new();
let mut assemblers: Vec<TokenStream> = Vec::new();
for (i, r) in relations.iter().enumerate() {
let map_ident = format_ident!("__rel_map_{i}");
let fident = &r.ident;
let child_ty = &r.child_ty;
let pk = &r.parent_key;
let ek = &r.entity_key;
let ek_col = &r.entity_column;
let loader = match &r.junction {
None => quote! {
#[allow(unused_mut)]
let mut #map_ident = ::roomrs::load_children(
cx,
&__keys,
<#child_ty as ::roomrs::Entity>::TABLE,
#ek_col,
|__c: &#child_ty| __c.#ek.clone(),
)?;
},
Some(j) => {
let jt = &j.table;
let jpk = &j.parent_key;
let jek = &j.entity_key;
quote! {
#[allow(unused_mut)]
let mut #map_ident = ::roomrs::load_junction(
cx,
&__keys,
#jt,
#jpk,
#jek,
<#child_ty as ::roomrs::Entity>::TABLE,
#ek_col,
|__c: &#child_ty| __c.#ek.clone(),
)?;
}
}
};
loaders.push(loader);
let assemble = if r.many {
quote! { #fident: #map_ident.remove(&__p.#pk).unwrap_or_default() }
} else {
quote! {
#fident: #map_ident
.remove(&__p.#pk)
.and_then(|mut v| if v.is_empty() { None } else { Some(v.remove(0)) })
}
};
assemblers.push(assemble);
}
if let Some(first) = relations.first() {
if relations.iter().any(|r| r.parent_key != first.parent_key) {
return Err(syn::Error::new(
ident.span(),
"v1 제약: 모든 #[relation]의 parent_key는 동일해야 합니다",
));
}
}
let parent_key_ident = relations
.first()
.map(|r| r.parent_key.clone())
.unwrap_or_else(|| format_ident!("id"));
Ok(quote! {
impl ::roomrs::RelationView for #ident {
type Parent = #parent_ty;
fn load<C: ::roomrs::SqlContext>(
cx: &C,
parents: Vec<Self::Parent>,
) -> ::roomrs::Result<Vec<Self>> {
let __keys: Vec<_> = parents.iter().map(|p| p.#parent_key_ident.clone()).collect();
#(#loaders)*
Ok(parents
.into_iter()
.map(|__p| Self {
#(#assemblers,)*
#parent_ident: __p,
})
.collect())
}
}
})
}