use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
Data, DeriveInput, Field, Fields, Ident, LitStr, Meta, Type, parse_macro_input,
punctuated::Punctuated, token::Comma,
};
#[proc_macro_derive(BasecoatProps, attributes(prop))]
pub fn derive_basecoat_props(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derive_impl(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
struct PropField {
ident: Ident,
ty: Type,
default_expr: Option<TokenStream2>, into: bool,
extend: bool,
}
impl PropField {
fn from_field(field: &Field) -> syn::Result<Self> {
let ident = field
.ident
.clone()
.ok_or_else(|| syn::Error::new_spanned(field, "BasecoatProps requires named fields"))?;
let ty = field.ty.clone();
let mut default_expr: Option<TokenStream2> = None;
let mut into = false;
let mut extend = false;
let mut has_default_marker = false;
for attr in &field.attrs {
if !attr.path().is_ident("prop") {
continue;
}
let nested = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
for meta in nested {
match &meta {
Meta::Path(p) if p.is_ident("default") => {
has_default_marker = true;
default_expr = Some(quote! { ::core::default::Default::default() });
}
Meta::NameValue(nv) if nv.path.is_ident("default") => {
has_default_marker = true;
let val = &nv.value;
default_expr = Some(quote! { #val });
}
Meta::Path(p) if p.is_ident("into") => {
into = true;
}
Meta::Path(p) if p.is_ident("optional") => {
if !has_default_marker {
default_expr = Some(quote! { ::core::option::Option::None });
}
}
Meta::Path(p) if p.is_ident("extend") => {
extend = true;
if !has_default_marker {
default_expr = Some(quote! { ::core::default::Default::default() });
}
}
other => {
return Err(syn::Error::new_spanned(
other,
"unknown prop attribute; expected default, into, optional, extend",
));
}
}
}
}
Ok(PropField {
ident,
ty,
default_expr,
into,
extend,
})
}
}
fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream2> {
let struct_ident = &input.ident;
let builder_ident = format_ident!("{}Builder", struct_ident);
let fields = match &input.data {
Data::Struct(ds) => match &ds.fields {
Fields::Named(f) => &f.named,
_ => {
return Err(syn::Error::new_spanned(
&input.ident,
"BasecoatProps only supports structs with named fields",
));
}
},
_ => {
return Err(syn::Error::new_spanned(
&input.ident,
"BasecoatProps can only be derived on structs",
));
}
};
let prop_fields: Vec<PropField> = fields
.iter()
.map(PropField::from_field)
.collect::<syn::Result<_>>()?;
let extend_fields: Vec<_> = prop_fields.iter().filter(|f| f.extend).collect();
if extend_fields.len() > 1 {
return Err(syn::Error::new_spanned(
struct_ident,
"at most one #[prop(extend)] field is allowed per prop struct",
));
}
let extend_field_name: Option<LitStr> = extend_fields
.first()
.map(|f| LitStr::new(&f.ident.to_string(), proc_macro2::Span::call_site()));
let builder_field_decls: Vec<TokenStream2> = prop_fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.ty;
quote! { #ident: ::core::option::Option<#ty> }
})
.collect();
let builder_none_inits: Vec<TokenStream2> = prop_fields
.iter()
.map(|f| {
let ident = &f.ident;
quote! { #ident: ::core::option::Option::None }
})
.collect();
let setters: Vec<TokenStream2> = prop_fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.ty;
if f.into {
quote! {
pub fn #ident(mut self, val: impl ::core::convert::Into<#ty>) -> Self {
self.#ident = ::core::option::Option::Some(val.into());
self
}
}
} else {
quote! {
pub fn #ident(mut self, val: #ty) -> Self {
self.#ident = ::core::option::Option::Some(val);
self
}
}
}
})
.collect();
let struct_name_str = struct_ident.to_string();
let build_fields: Vec<TokenStream2> = prop_fields
.iter()
.map(|f| {
let ident = &f.ident;
let field_name_str = ident.to_string();
if let Some(default) = &f.default_expr {
quote! {
#ident: self.#ident.unwrap_or_else(|| #default)
}
} else {
quote! {
#ident: self.#ident.unwrap_or_else(|| {
panic!(
"required field `{}` was not set on `{}::builder()`",
#field_name_str,
#struct_name_str,
)
})
}
}
})
.collect();
let extend_const = if let Some(name) = &extend_field_name {
quote! {
pub const __BASECOAT_EXTEND_FIELD: ::core::option::Option<&'static str> =
::core::option::Option::Some(#name);
}
} else {
quote! {
pub const __BASECOAT_EXTEND_FIELD: ::core::option::Option<&'static str> =
::core::option::Option::None;
}
};
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! {
impl #impl_generics #struct_ident #ty_generics #where_clause {
pub fn builder() -> #builder_ident #ty_generics {
#builder_ident {
#( #builder_none_inits, )*
}
}
#extend_const
}
pub struct #builder_ident #ty_generics #where_clause {
#( #builder_field_decls, )*
}
impl #impl_generics #builder_ident #ty_generics #where_clause {
#( #setters )*
pub fn build(self) -> #struct_ident #ty_generics {
#struct_ident {
#( #build_fields, )*
}
}
}
impl #impl_generics ::core::convert::From<#builder_ident #ty_generics>
for #struct_ident #ty_generics #where_clause
{
fn from(b: #builder_ident #ty_generics) -> Self {
b.build()
}
}
})
}