use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Result, parse_quote};
use crate::path::core_path;
pub fn expand(input: DeriveInput) -> Result<TokenStream> {
for attr in &input.attrs {
if attr.path().is_ident("entity") {
return Err(syn::Error::new_spanned(
attr,
"entity(id) must be placed on a named field",
));
}
}
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return Err(syn::Error::new_spanned(
&input.ident,
"Entity requires a named-field struct",
));
}
},
_ => {
return Err(syn::Error::new_spanned(
&input.ident,
"Entity can only be derived for a named-field struct",
));
}
};
let mut id_field = None;
for field in fields {
if has_id_attribute(field)? {
if id_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"Entity requires exactly one #[entity(id)] field",
));
}
id_field = Some(field);
}
}
let id_field = id_field.ok_or_else(|| {
syn::Error::new_spanned(
&input.ident,
"Entity requires exactly one #[entity(id)] field",
)
})?;
let id_name = id_field.ident.as_ref().expect("named fields have names");
let id_ty = &id_field.ty;
let core = core_path()?;
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut partial_eq_generics = input.generics.clone();
partial_eq_generics
.make_where_clause()
.predicates
.push(parse_quote!(#id_ty: ::core::cmp::PartialEq));
let (partial_eq_impl, partial_eq_ty, partial_eq_where) = partial_eq_generics.split_for_impl();
let mut eq_generics = input.generics.clone();
eq_generics
.make_where_clause()
.predicates
.push(parse_quote!(#id_ty: ::core::cmp::Eq));
let (eq_impl, eq_ty, eq_where) = eq_generics.split_for_impl();
let mut hash_generics = input.generics.clone();
hash_generics
.make_where_clause()
.predicates
.push(parse_quote!(#id_ty: ::core::hash::Hash));
let (hash_impl, hash_ty, hash_where) = hash_generics.split_for_impl();
Ok(quote! {
impl #impl_generics #core::Entity for #name #ty_generics #where_clause {
type Id = #id_ty;
fn id(&self) -> &Self::Id {
&self.#id_name
}
}
impl #impl_generics #name #ty_generics #where_clause {
pub fn id(&self) -> &#id_ty {
&self.#id_name
}
}
impl #partial_eq_impl ::core::cmp::PartialEq for #name #partial_eq_ty #partial_eq_where {
fn eq(&self, other: &Self) -> bool {
self.#id_name == other.#id_name
}
}
impl #eq_impl ::core::cmp::Eq for #name #eq_ty #eq_where {}
impl #hash_impl ::core::hash::Hash for #name #hash_ty #hash_where {
fn hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
::core::hash::Hash::hash(&self.#id_name, state);
}
}
})
}
fn has_id_attribute(field: &syn::Field) -> Result<bool> {
let mut found = false;
for attr in &field.attrs {
if !attr.path().is_ident("entity") {
continue;
}
let mut option_found = false;
attr.parse_nested_meta(|meta| {
option_found = true;
if !meta.path.is_ident("id") {
return Err(meta.error("unknown entity attribute"));
}
if !meta.input.is_empty() {
return Err(meta.error("id is a flag and takes no value"));
}
if found {
return Err(meta.error("duplicate id attribute"));
}
found = true;
Ok(())
})?;
if !option_found {
return Err(syn::Error::new_spanned(attr, "entity requires the id flag"));
}
}
Ok(found)
}