use convert_case::{Case, Casing};
use darling::{ast, FromDeriveInput, FromField};
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{DeriveInput, GenericArgument, Generics, TypeParamBound};
const UNSUPPORTED_ERROR: &str = r#"FieldSetter can only be derived for structs with named fields"#;
pub(crate) fn field_setter_impl(input: DeriveInput) -> proc_macro::TokenStream {
let struct_receiver = match StructReceiver::from_derive_input(&input) {
Ok(r) => r,
Err(e) => {
return proc_macro::TokenStream::from(
darling::Error::custom(format!("{}. {}", UNSUPPORTED_ERROR, e)).write_errors(),
)
}
};
quote! {
#struct_receiver
}
.into()
}
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(field_setter), supports(struct_named))]
struct StructReceiver {
ident: syn::Ident,
data: ast::Data<(), FieldReceiver>,
generics: Generics,
}
impl ToTokens for StructReceiver {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ident = &self.ident;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let mut setter_functions = quote![];
let mut default_vals = quote![];
match self.data {
ast::Data::Struct(ref f) => {
for field in f.fields.iter() {
let setter = field.setter();
setter_functions.append_all(setter);
default_vals.append_all(field.default_value());
}
}
_ => unreachable!(),
}
tokens.append_all(quote! {
impl #impl_generics Default for #ident #ty_generics #where_clause {
fn default() -> Self {
Self {
#default_vals
}
}
}
impl #impl_generics #ident #ty_generics #where_clause {
#setter_functions
}
});
}
}
#[allow(clippy::large_enum_variant, clippy::enum_variant_names)]
#[derive(Clone)]
enum FieldType {
OptionString,
OptionOneOrManyString,
OptionClosure,
OptionOther(syn::Type),
}
fn _type_str_parts(field_type: &syn::Type) -> (Vec<String>, Vec<syn::Type>) {
let mut type_ = field_type;
let mut parts = Vec::new();
let mut types = vec![type_.clone()];
loop {
match type_ {
syn::Type::Path(ref type_path) if type_path.qself == None => {
if let Some(segment) = type_path.path.segments.last() {
parts.push(segment.ident.to_string());
match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => match args.args.first() {
Some(first) => {
if let GenericArgument::Type(inner_ty) = first {
type_ = inner_ty;
types.push(type_.clone());
} else {
break;
}
}
None => break,
},
_ => break,
}
} else {
break;
}
}
syn::Type::TraitObject(ref obj) => {
if obj.dyn_token.is_some() {
if let Some(TypeParamBound::Trait(t)) = obj.bounds.first() {
if let Some(segment) = t.path.segments.last() {
parts.push(segment.ident.to_string());
}
}
}
break;
}
_ => break,
}
}
(parts, types)
}
impl FieldType {
fn infer(field_type: &syn::Type) -> Self {
let (type_str_parts, types) = _type_str_parts(field_type);
let remaining: Vec<_> = type_str_parts.iter().skip(1).map(|x| x.as_str()).collect();
match remaining.as_slice() {
["String"] => FieldType::OptionString,
["OneOrMany", "String"] => FieldType::OptionOneOrManyString,
["Closure", _] => FieldType::OptionClosure,
_ => FieldType::OptionOther(types.get(1).cloned().unwrap()),
}
}
}
#[derive(Debug, FromField)]
#[darling(attributes(field_setter), forward_attrs(doc))]
struct FieldReceiver {
ident: Option<syn::Ident>,
ty: syn::Type,
attrs: Vec<syn::Attribute>,
#[darling(default)]
skip: bool,
}
impl FieldReceiver {
fn default_value(&self) -> TokenStream {
let field_ident = self.ident.as_ref().unwrap();
quote![
#field_ident: None,
]
}
fn setter(&self) -> TokenStream {
if self.skip {
return quote![];
}
let field_ident = self.ident.as_ref().unwrap();
let field_type = &self.ty;
let field_docs = self.docs();
let field_type = FieldType::infer(field_type);
let (mutability, value_type, value_convert, array_value_convert) = match &field_type {
FieldType::OptionClosure => {
let pascal_cased = field_ident.to_string().to_case(Case::Pascal);
let js_params = format!("I{pascal_cased}Params");
let js_params = Ident::new(&js_params, proc_macro2::Span::call_site());
let params = format!("{pascal_cased}Params");
let params = Ident::new(¶ms, proc_macro2::Span::call_site());
(
quote![mut],
quote![impl FnMut(crate::callbacks::#params) -> String + 'static],
quote![wasm_bindgen::closure::Closure::<
dyn FnMut(crate::callbacks::#js_params) -> String,
>::new(
move |js_params: crate::callbacks::#js_params| value(
(&js_params).into()
)
)],
quote![],
)
}
FieldType::OptionString => (
quote![],
quote![impl AsRef<str>],
quote![value.as_ref().to_owned()],
quote![],
),
FieldType::OptionOneOrManyString => (
quote![],
quote![impl AsRef<str>],
quote![value.as_ref().to_string().into()],
quote![value
.iter()
.map(|v| v.as_ref().to_string())
.collect::<Vec<_>>()
.into()],
),
FieldType::OptionOther(inner_ty) => {
(quote![], quote![#inner_ty], quote![value], quote![])
}
};
let setter = quote! {
#field_docs
pub fn #field_ident(mut self, #mutability value: #value_type) -> Self {
self.#field_ident = Some(#value_convert);
self
}
};
let array_setter = match field_type {
FieldType::OptionOneOrManyString => {
let array_ident = Ident::new(
&format!("{}_array", field_ident.to_string().trim_end_matches('_')),
proc_macro2::Span::call_site(),
);
quote! {
#field_docs
pub fn #array_ident(mut self, value: Vec<#value_type>) -> Self {
self.#field_ident = Some(#array_value_convert);
self
}
}
}
_ => quote![],
};
quote![
#setter
#array_setter
]
}
fn docs(&self) -> TokenStream {
self.search_attrs("doc")
}
fn search_attrs(&self, name: &str) -> TokenStream {
self.attrs
.iter()
.filter(|attr| {
attr.path
.segments
.first()
.map_or(false, |p| p.ident == name)
})
.map(|attr| {
quote![
#attr
]
})
.collect()
}
}