use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields, GenericArgument, PathArguments, Type, parse_macro_input};
mod declared_fields;
#[proc_macro_derive(DeclaredFields, attributes(declared))]
pub fn declared_fields(input: TokenStream) -> TokenStream {
declared_fields::expand(parse_macro_input!(input as DeriveInput))
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(WireBuilder, attributes(wire))]
pub fn wire_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new_spanned(
&input,
"WireBuilder requires a struct",
));
};
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new_spanned(
&input,
"WireBuilder requires named fields",
));
};
let name = &input.ident;
let builder = format_ident!("{}Builder", name);
let visibility = &input.vis;
let generics = &input.generics;
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
let mut arguments = Vec::new();
let mut initializers = Vec::new();
let mut setters = Vec::new();
for field in &fields.named {
let ident = field.ident.as_ref().expect("named field");
let ty = &field.ty;
let mut required = false;
for attr in field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("wire"))
{
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("required") {
required = true;
Ok(())
} else {
Err(meta.error("expected `required`"))
}
})?;
}
if required {
arguments.push(quote!(#ident: #ty));
initializers.push(quote!(#ident));
continue;
}
if let Some(inner) = option_inner(ty) {
initializers.push(quote!(#ident: ::core::option::Option::None));
setters.push(quote! {
#[must_use]
pub fn #ident(mut self, value: impl ::core::convert::Into<#inner>) -> Self {
self.0.#ident = ::core::option::Option::Some(value.into());
self
}
});
} else if ident == "rest" {
initializers.push(quote!(#ident: ::core::default::Default::default()));
setters.push(quote! {
#[must_use]
pub fn #ident(mut self, value: #ty) -> Self {
self.0.#ident = value;
self
}
});
} else {
arguments.push(quote!(#ident: #ty));
initializers.push(quote!(#ident));
}
}
Ok(quote! {
impl #impl_generics #name #type_generics #where_clause {
#[allow(clippy::too_many_arguments)]
pub fn builder(#(#arguments),*) -> #builder #type_generics {
#builder(Self { #(#initializers),* })
}
}
#[doc = concat!("Builder for [`", stringify!(#name), "`].")]
#[must_use]
#visibility struct #builder #generics (#name #type_generics) #where_clause;
impl #impl_generics #builder #type_generics #where_clause {
#(#setters)*
pub fn build(self) -> #name #type_generics {
self.0
}
}
})
}
fn option_inner(ty: &Type) -> Option<&Type> {
let Type::Path(path) = ty else { return None };
let segment = path.path.segments.last()?;
if segment.ident != "Option" {
return None;
}
let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return None;
};
match arguments.args.first()? {
GenericArgument::Type(inner) => Some(inner),
_ => None,
}
}