use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields};
pub fn entity(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut name_field: Option<syn::Ident> = None;
let mut name_method: Option<syn::Ident> = None;
let mut is_singleton = false;
let attr_str = attr.to_string();
if attr_str.contains("singleton") {
is_singleton = true;
}
if let Some(start) = attr_str.find("name_field")
&& let Some(eq_pos) = attr_str[start..].find('=')
{
let after_eq = &attr_str[start + eq_pos + 1..];
if let Some(value) = extract_string_literal(after_eq) {
name_field = Some(syn::Ident::new(&value, proc_macro2::Span::call_site()));
}
}
if let Some(start) = attr_str.find("name_method")
&& let Some(eq_pos) = attr_str[start..].find('=')
{
let after_eq = &attr_str[start + eq_pos + 1..];
if let Some(value) = extract_string_literal(after_eq) {
name_method = Some(syn::Ident::new(&value, proc_macro2::Span::call_site()));
}
}
if !is_singleton && name_field.is_some() && name_method.is_some() {
return syn::Error::new_spanned(&input, "Cannot specify both name_field and name_method")
.to_compile_error()
.into();
}
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
&input,
"stately::entity can only be used on structs with named fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input, "stately::entity can only be used on structs")
.to_compile_error()
.into();
}
};
let name_impl = if is_singleton {
quote! {
fn name(&self) -> &str {
"default"
}
}
} else if let Some(method) = name_method {
quote! {
fn name(&self) -> &str {
self.#method()
}
}
} else {
let field =
name_field.unwrap_or_else(|| syn::Ident::new("name", proc_macro2::Span::call_site()));
let has_field = fields.iter().any(|f| f.ident.as_ref() == Some(&field));
if !has_field {
return syn::Error::new_spanned(
&input,
format!(
"Struct must have a '{}' field, or use #[stately::entity(name_field = \
\"field_name\")] or #[stately::entity(name_method = \"method_name\")]",
field
),
)
.to_compile_error()
.into();
}
quote! {
fn name(&self) -> &str {
&self.#field
}
}
};
let expanded = quote! {
#input
impl #impl_generics ::stately::HasName for #name #ty_generics #where_clause {
#name_impl
}
};
TokenStream::from(expanded)
}
fn extract_string_literal(s: &str) -> Option<String> {
let end = s.trim().strip_prefix('"')?;
Some(end.to_string())
}