|
use proc_macro2::{Ident, Literal, TokenStream as TokenStream2}; |
|
use quote::ToTokens; |
|
use syn::spanned::Spanned; |
|
use syn::{Attribute, Data, DeriveInput, Field, PathArguments, Type}; |
|
|
|
|
|
fn has_attribute(attrs: &[Attribute], target: &str) -> bool { |
|
attrs |
|
.iter() |
|
.filter(|attr| attr.path().to_token_stream().to_string() == "widget_builder") |
|
.any(|attr| attr.meta.require_list().is_ok_and(|list| list.tokens.to_string() == target)) |
|
} |
|
|
|
|
|
|
|
|
|
fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) { |
|
if let Type::Path(type_path) = field_ty { |
|
if let Some(last_segment) = type_path.path.segments.last() { |
|
|
|
|
|
if last_segment.ident == Ident::new("String", last_segment.ident.span()) { |
|
return ( |
|
quote::quote_spanned!(type_path.span()=> impl Into<String>), |
|
quote::quote_spanned!(field_ident.span()=> #field_ident.into()), |
|
); |
|
} |
|
} |
|
} |
|
(quote::quote_spanned!(field_ty.span()=> #field_ty), quote::quote_spanned!(field_ident.span()=> #field_ident)) |
|
} |
|
|
|
|
|
fn extract_ident(field: &Field) -> syn::Result<&Ident> { |
|
field |
|
.ident |
|
.as_ref() |
|
.ok_or_else(|| syn::Error::new_spanned(field, "Constructing a builder not supported for unnamed fields")) |
|
} |
|
|
|
|
|
|
|
|
|
fn find_type_and_assignment(field: &Field) -> syn::Result<(TokenStream2, TokenStream2)> { |
|
let field_ty = &field.ty; |
|
let field_ident = extract_ident(field)?; |
|
|
|
let (mut function_input_ty, mut assignment) = easier_string_assignment(field_ty, field_ident); |
|
|
|
|
|
if let Type::Path(type_path) = field_ty { |
|
if let Some(last_segment) = type_path.path.segments.last() { |
|
if let PathArguments::AngleBracketed(generic_args) = &last_segment.arguments { |
|
if let Some(first_generic) = generic_args.args.first() { |
|
if last_segment.ident == Ident::new("WidgetCallback", last_segment.ident.span()) { |
|
|
|
function_input_ty = quote::quote_spanned!(field_ty.span()=> impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync); |
|
assignment = quote::quote_spanned!(field_ident.span()=> crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident)); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
Ok((function_input_ty, assignment)) |
|
} |
|
|
|
|
|
fn construct_builder(field: &Field) -> syn::Result<TokenStream2> { |
|
|
|
if has_attribute(&field.attrs, "skip") { |
|
return Ok(Default::default()); |
|
} |
|
let field_ident = extract_ident(field)?; |
|
|
|
|
|
let doc_comment = Literal::string(&format!("Set the `{field_ident}` field using a builder pattern.")); |
|
|
|
let (function_input_ty, assignment) = find_type_and_assignment(field)?; |
|
|
|
|
|
Ok(quote::quote_spanned!(field.span()=> |
|
#[doc = #doc_comment] |
|
pub fn #field_ident(mut self, #field_ident: #function_input_ty) -> Self{ |
|
self.#field_ident = #assignment; |
|
self |
|
} |
|
)) |
|
} |
|
|
|
pub fn derive_widget_builder_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> { |
|
let input = syn::parse2::<DeriveInput>(input_item)?; |
|
|
|
let struct_name_ident = input.ident; |
|
|
|
|
|
let fields = match &input.data { |
|
Data::Enum(enum_data) => return Err(syn::Error::new_spanned(enum_data.enum_token, "Derive widget builder is not supported for enums")), |
|
Data::Union(union_data) => return Err(syn::Error::new_spanned(union_data.union_token, "Derive widget builder is not supported for unions")), |
|
Data::Struct(struct_data) => &struct_data.fields, |
|
}; |
|
|
|
|
|
let builder_functions = fields.iter().map(construct_builder).collect::<Result<Vec<_>, _>>()?; |
|
|
|
|
|
let widget_holder_fn = if !has_attribute(&input.attrs, "not_widget_holder") { |
|
|
|
let widget_holder_doc_comment = Literal::string(&format!("Wrap {struct_name_ident} as a WidgetHolder.")); |
|
|
|
|
|
quote::quote! { |
|
#[doc = #widget_holder_doc_comment] |
|
pub fn widget_holder(self) -> crate::messages::layout::utility_types::layout_widget::WidgetHolder{ |
|
crate::messages::layout::utility_types::layout_widget::WidgetHolder::new( crate::messages::layout::utility_types::layout_widget::Widget::#struct_name_ident(self)) |
|
} |
|
} |
|
} else { |
|
quote::quote!() |
|
}; |
|
|
|
|
|
let new_fn = { |
|
|
|
let new_doc_comment = Literal::string(&format!("Create a new {struct_name_ident}, based on default values.")); |
|
|
|
let is_constructor = |field: &Field| has_attribute(&field.attrs, "constructor"); |
|
|
|
let idents = fields.iter().filter(|field| is_constructor(field)).map(extract_ident).collect::<Result<Vec<_>, _>>()?; |
|
let types_and_assignments = fields.iter().filter(|field| is_constructor(field)).map(find_type_and_assignment).collect::<Result<Vec<_>, _>>()?; |
|
let (types, assignments): (Vec<_>, Vec<_>) = types_and_assignments.into_iter().unzip(); |
|
|
|
let construction = if idents.is_empty() { |
|
quote::quote!(Default::default()) |
|
} else { |
|
let default = (idents.len() != fields.len()).then_some(quote::quote!(..Default::default())).unwrap_or_default(); |
|
quote::quote! { |
|
Self { |
|
#(#idents: #assignments,)* |
|
#default |
|
} |
|
} |
|
}; |
|
|
|
quote::quote! { |
|
#[doc = #new_doc_comment] |
|
pub fn new(#(#idents: #types),*) -> Self { |
|
#construction |
|
} |
|
} |
|
}; |
|
|
|
|
|
Ok(quote::quote! { |
|
impl #struct_name_ident { |
|
#new_fn |
|
|
|
#(#builder_functions)* |
|
|
|
#widget_holder_fn |
|
} |
|
}) |
|
} |
|
|