diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..056b6f1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,198 @@ +use attribute_derive::FromAttr; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use syn::{ + Expr, ExprStruct, Ident, ItemStruct, LitBool, Type, Visibility, parse_macro_input, token::Pub, +}; + +enum Default { + None, + Impl, + Value(Expr), +} + +struct MaybeOptionalField { + visibility: Visibility, + name: Ident, + default: Default, + auto_into: bool, + ty: Type, +} + +#[derive(FromAttr)] +#[from_attr(ident = builder)] +struct BuilderFieldOptions { + vis: Option<Visibility>, + optional: bool, + default: Option<Expr>, + into: bool, +} + +#[proc_macro_derive(Builder, attributes(builder))] +pub fn derive_builder(input: TokenStream) -> TokenStream { + let structure = parse_macro_input!(input as ItemStruct); + let struct_name = &structure.ident; + let builder_name = format_ident!("{}Builder", structure.ident); + let fields = structure + .fields + .into_iter() + .map(|field| { + let attrs = BuilderFieldOptions::from_attributes(field.attrs).unwrap(); + MaybeOptionalField { + visibility: attrs.vis.unwrap_or(field.vis), + name: field.ident.unwrap(), + default: match (attrs.optional, attrs.default) { + (true, None) => Default::Impl, + (_, Some(expr)) => Default::Value(expr), + (false, None) => Default::None, + }, + auto_into: attrs.into, + ty: field.ty, + } + }) + .collect::<Box<_>>(); + + let builder_visibility = fields + .iter() + .all(|field| matches!(field.visibility, Visibility::Public(_))) + .then_some(Visibility::Public(Pub { + span: Span::call_site(), + })); + let field_names = fields + .iter() + .map(|field| field.name.clone()) + .collect::<Box<_>>(); + let field_types = fields + .iter() + .map(|field| field.ty.clone()) + .collect::<Box<_>>(); + let const_generics = fields + .iter() + .filter(|field| matches!(field.default, Default::None)) + .map(|field| Ident::new(&field.name.to_string().to_uppercase(), field.name.span())) + .collect::<Box<_>>(); + let trues = (0..const_generics.len()) + .map(|_| LitBool::new(true, Span::call_site())) + .collect::<Box<_>>(); + let falses = (0..const_generics.len()) + .map(|_| LitBool::new(false, Span::call_site())) + .collect::<Box<_>>(); + + let mut j = 0; + let setters = fields + .iter() + .enumerate() + .map(|(i, field)| { + let visibility = &field.visibility; + let setter_name = &field.name; + let ty = &field.ty; + let param_ty = if field.auto_into { quote! {#ty} } else { quote! { impl ::core::convert::Into<#ty> } }; + let field_name = &field.name; + let fields_head = &field_names[0..i]; + let fields_tail = &field_names[i+1..]; + if matches!(field.default, Default::None) { + let generics_head = &const_generics[0..j]; + let generics_tail = &const_generics[j+1..]; + j += 1; + quote! { + #visibility fn #setter_name(self, value: #param_ty>) -> #builder_name<#(#generics_head,)* true, #(#generics_tail),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: Some(value.into()), + #(#fields_tail: self.#fields_tail,)* + } + } + } + } else { + let maybe_setter_name = format_ident!("maybe_{}", &field.name); + let clear_name = format_ident!("clear_{}", &field.name); + quote! { + #visibility fn #setter_name(self, value: #param_ty) -> #builder_name<#(#const_generics),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: Some(value.into()), + #(#fields_tail: self.#fields_tail,)* + } + } + + #visibility fn #maybe_setter_name(self, value: core::option::Option<#param_ty>) -> #builder_name<#(#const_generics),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: value.map(Into::into), + #(#fields_tail: self.#fields_tail,)* + } + } + + #visibility fn #clear_name(self) -> #builder_name<#(#const_generics),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: None, + #(#fields_tail: self.#fields_tail,)* + } + } + } + } + }) + .collect::<Box<_>>(); + let build_fields = fields.iter().map(|field| { + let field_name = &field.name; + match &field.default { + Default::Value(default_value) => { + quote! { #field_name: self.#field_name.unwrap_or_else(|| #default_value) } + } + Default::Impl => quote! { #field_name: self.#field_name.unwrap_or_default() }, + Default::None => quote! { #field_name: unsafe { self.#field_name.unwrap_unchecked() } }, + } + }); + + quote! { + #builder_visibility struct #builder_name<#(const #const_generics: bool),*> { + #(#field_names: Option<#field_types>,)* + } + + impl<#(const #const_generics: bool),*> core::default::Default for #builder_name<#(#const_generics),*> { + fn default() -> Self { + Self { + #(#field_names: None,)* + } + } + } + + impl #struct_name { + #builder_visibility fn builder() -> #builder_name<#(#falses),*> { + #builder_name::default() + } + } + + impl<#(const #const_generics: bool),*> #builder_name<#(#const_generics),*> { + #(#setters)* + } + + impl #builder_name<#(#trues),*> { + #builder_visibility fn build(self) -> #struct_name { + #struct_name { + #(#build_fields,)* + } + } + } + } + .into() +} + +#[proc_macro] +pub fn build(input: TokenStream) -> TokenStream { + let structure = parse_macro_input!(input as ExprStruct); + let struct_name = &structure.path; + + let field_names = structure.fields.iter().map(|field| &field.member); + let field_values = structure.fields.iter().map(|field| &field.expr); + + quote! { + #struct_name + ::builder() + #(.#field_names(#field_values))* + .build() + } + .into() +} |
