Skip to content

Commit

Permalink
Add mutators on item level
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Sep 26, 2023
1 parent 94383e7 commit 3d284ec
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 24 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `#[builder(mutators(...))]` to generate functions on `Builder` able to
mutate fields
- `#[builder(via_mutator)]` on fields to allow defining fields initialized
during `::builder()` for use with `mutators`

### Changed
- Internal refactor of attribute parsing - results in better error messages and
Expand Down
44 changes: 44 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ use core::ops::FnOnce;
/// #[derive(Default)]
/// struct Point { x: f32, y: f32 }
/// ```
/// - `mutators(...)` takes functions, that can mutate fields inside of the builder.
/// See [mutators](#mutators) for details.
///
/// On each **field**, the following values are permitted:
///
Expand All @@ -135,6 +137,12 @@ use core::ops::FnOnce;
/// Note that if `...` contains a string, you can use raw string literals to avoid escaping the
/// double quotes - e.g. `#[builder(default_code = r#""default text".to_owned()"#)]`.
///
/// - `via_mutators`: initialize the field when constructing the builder, useful in combination
/// with [mutators](#mutators).
///
/// - `via_mutators = …` or `via_mutators(init = …)`: initialies the field with the expression `…`
/// when constructing the builder, useful in combination with [mutators](#mutators).
///
/// - `setter(...)`: settings for the field setters. The following values are permitted inside:
///
/// - `doc = "…"`: sets the documentation for the field's setter on the builder type. This will be
Expand Down Expand Up @@ -168,6 +176,42 @@ use core::ops::FnOnce;
/// - `suffix = "..."` appends the setter method with the specified suffix. For example, setting
/// `suffix = "_value"` results in setters like `x_value` or `y_value`. This option is combinable
/// with `prefix = "..."`.
///
/// # Mutators
/// Set fields can be mutated using mutators, these can be defind via `mutators(...)`.
///
/// Fields annotated with `#[builder(via_mutators)]` are always available to mutators. Additional
/// fields can be specified via `self: (field1, field2)` in the mutator's function signature.
/// Mutators that require non `via_mutators` fields, will only be availible if these fields are set.
///
/// ```
/// use typed_builder::TypedBuilder;
///
/// #[derive(PartialEq, Debug, TypedBuilder)]
/// #[builder(mutators(
/// // &mut is optional
/// fn inc_a(&mut self, a: i32){
/// self.a += a;
/// }
/// // (x) defines what fields to require
/// // &mut cannot be used in combination with :(field)
/// fn x_into_b(self: (x)) {
/// self.b.push(self.x)
/// }
/// ))]
/// struct Struct {
/// x: i32,
/// #[builder(via_mutators(init = 1))]
/// a: i32,
/// #[builder(via_mutators)]
/// b: Vec<i32>
/// }
///
/// // Mutators do not enforce only being called once
/// assert_eq!(
/// Struct::builder().x(2).x_into_b().x_into_b().inc_a(2).build(),
/// Struct {x: 2, a: 3, b: vec![2, 2]});
/// ```
pub use typed_builder_macro::TypedBuilder;

#[doc(hidden)]
Expand Down
35 changes: 35 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -818,3 +818,38 @@ fn test_preinitialized_fields() {
let foo = Foo::builder().x(1).build();
assert_eq!(foo, Foo { x: 1, y: 0, z: 2, w: 2 });
}

#[test]
fn test_mutators_item() {
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(mutators(
fn inc_x(self: (x)) {
self.x += 1;
}
fn inc_x_by(self: (x), x: i32) {
self.x += x;
}
fn inc_preset(self) {
self.y += 1;
self.z += 1;
self.w += 1;
}
fn inc_y_by_x(self:(x)) {
self.y += self.x;
}
))]
struct Foo {
x: i32,
#[builder(via_mutators)]
y: i32,
#[builder(via_mutators = 2)]
z: i32,
#[builder(via_mutators(init = 2))]
w: i32,
}

let foo = Foo::builder().x(1).inc_x().inc_preset().build();
assert_eq!(foo, Foo { x: 2, y: 1, z: 3, w: 3 });
let foo = Foo::builder().x(1).inc_x_by(4).inc_y_by_x().build();
assert_eq!(foo, Foo { x: 5, y: 5, z: 2, w: 2 });
}
7 changes: 7 additions & 0 deletions typed-builder-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@ fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
.setter_fields()
.filter(|f| f.builder_attr.default.is_none())
.map(|f| struct_info.required_field_impl(f));
let mutators = struct_info
.builder_attr
.mutators
.iter()
.map(|m| struct_info.mutator_impl(m))
.collect::<Result<TokenStream, _>>()?;
let build_method = struct_info.build_method_impl();

quote! {
#builder_creation
#fields
#(#required_fields)*
#mutators
#build_method
}
}
Expand Down
133 changes: 112 additions & 21 deletions typed-builder-macro/src/struct_info.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::collections::HashSet;

use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::{parse_quote, GenericArgument, ItemFn, Token};

use crate::field_info::{FieldBuilderAttr, FieldInfo};
use crate::util::{
empty_type, empty_type_tuple, first_visibility, modify_types_generics_hack, path_to_single_string, public_visibility,
strip_raw_ident_prefix, type_tuple, ApplyMeta, AttrArg,
strip_raw_ident_prefix, type_tuple, ApplyMeta, AttrArg, Mutator,
};

#[derive(Debug)]
Expand All @@ -28,6 +32,24 @@ impl<'a> StructInfo<'a> {
self.included_fields().filter(|f| f.builder_attr.via_mutators.is_none())
}

pub fn generic_argumnents(&self) -> Punctuated<GenericArgument, Token![,]> {
self.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = type_param.ident.to_token_stream();
syn::parse2(ident).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone()),
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.to_token_stream();
syn::parse2(ident).unwrap()
}
})
.collect()
}

pub fn new(ast: &'a syn::DeriveInput, fields: impl Iterator<Item = &'a syn::Field>) -> Result<StructInfo<'a>, Error> {
let builder_attr = TypeBuilderAttr::new(&ast.attrs)?;
let builder_name = builder_attr
Expand Down Expand Up @@ -200,22 +222,7 @@ impl<'a> StructInfo<'a> {
ty: field_type,
..
} = field;
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = type_param.ident.to_token_stream();
syn::parse2(ident).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone()),
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.to_token_stream();
syn::parse2(ident).unwrap()
}
})
.collect();
let mut ty_generics = self.generic_argumnents();
let mut target_generics_tuple = empty_type_tuple();
let mut ty_generics_tuple = empty_type_tuple();
let generics = {
Expand Down Expand Up @@ -284,11 +291,11 @@ impl<'a> StructInfo<'a> {
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[automatically_derived]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
impl #impl_generics #builder_name <#ty_generics> #where_clause {
#deprecated
#doc
#[allow(clippy::used_underscore_binding)]
pub fn #method_name (self, #param_list) -> #builder_name <#( #target_generics ),*> {
pub fn #method_name (self, #param_list) -> #builder_name <#target_generics> {
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -304,11 +311,11 @@ impl<'a> StructInfo<'a> {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[automatically_derived]
impl #impl_generics #builder_name < #( #target_generics ),* > #where_clause {
impl #impl_generics #builder_name <#target_generics> #where_clause {
#[deprecated(
note = #repeated_fields_error_message
)]
pub fn #method_name (self, _: #repeated_fields_error_type_name) -> #builder_name <#( #target_generics ),*> {
pub fn #method_name (self, _: #repeated_fields_error_type_name) -> #builder_name <#target_generics> {
self
}
}
Expand Down Expand Up @@ -405,6 +412,85 @@ impl<'a> StructInfo<'a> {
}
}

pub fn mutator_impl(
&self,
mutator @ Mutator {
fun: mutator_fn,
required_fields,
required_fields_span,
}: &Mutator,
) -> syn::Result<TokenStream> {
let StructInfo { ref builder_name, .. } = *self;

let mut required_fields: HashSet<_> = required_fields.iter().collect();

let mut ty_generics = self.generic_argumnents();
let mut destructuring = TokenStream::new();
let mut ty_generics_tuple = empty_type_tuple();
let mut generics = self.generics.clone();
let mut mutator_ty_fields = Punctuated::<_, Token![,]>::new();
let mut mutator_destructure_fields = Punctuated::<_, Token![,]>::new();
for f @ FieldInfo { name, ty, .. } in self.included_fields() {
if f.builder_attr.via_mutators.is_some() || required_fields.remove(f.name) {
ty_generics_tuple.elems.push(f.tuplized_type_ty_param());
mutator_ty_fields.push(quote!(#name: #ty));
mutator_destructure_fields.push(name);
quote!((#name,),).to_tokens(&mut destructuring);
} else {
generics.params.push(f.generic_ty_param());
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push(generic_argument.clone());
quote!(#name,).to_tokens(&mut destructuring);
}
}
ty_generics.push(syn::GenericArgument::Type(ty_generics_tuple.into()));
let (impl_generics, _, where_clause) = generics.split_for_impl();

let mutator_struct_name = Ident::new("TypedBuilderFieldMutator", *required_fields_span);

let ItemFn { attrs, vis, .. } = mutator_fn;
let sig = mutator.outer_sig(parse_quote!(#builder_name <#ty_generics>));
let fn_name = &sig.ident;
let mutator_args = mutator.arguments();

Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[automatically_derived]
impl #impl_generics #builder_name <#ty_generics> #where_clause {
#(#attrs)*
#[allow(clippy::used_underscore_binding)]
#vis #sig {
struct #mutator_struct_name {
#mutator_ty_fields
}
impl #mutator_struct_name {
#mutator_fn
}

let __args = (#mutator_args);

let ( #destructuring ) = self.fields;
let mut __mutator = #mutator_struct_name{ #mutator_destructure_fields };

// This dance is required to keep mutator args and destrucutre fields from interfering.
{
let (#mutator_args) = __args;
__mutator.#fn_name(#mutator_args);
}

let #mutator_struct_name {
#mutator_destructure_fields
} = __mutator;

#builder_name {
fields: ( #destructuring ),
phantom: self.phantom,
}
}
}
})
}

fn build_method_name(&self) -> TokenStream {
self.builder_attr.build_method.common.get_name().unwrap_or(quote!(build))
}
Expand Down Expand Up @@ -650,6 +736,9 @@ pub struct TypeBuilderAttr<'a> {
pub field_defaults: FieldBuilderAttr<'a>,

pub crate_module_path: syn::Path,

/// Functions that are able to mutate fields in the builder that are already set
pub mutators: Vec<Mutator>,
}

impl Default for TypeBuilderAttr<'_> {
Expand All @@ -661,6 +750,7 @@ impl Default for TypeBuilderAttr<'_> {
build_method: Default::default(),
field_defaults: Default::default(),
crate_module_path: syn::parse_quote!(::typed_builder),
mutators: Default::default(),
}
}
}
Expand Down Expand Up @@ -721,6 +811,7 @@ impl ApplyMeta for TypeBuilderAttr<'_> {
self.doc = true;
Ok(())
}
"mutators" => expr.sub_attr()?.undelimited().map(|fns| self.mutators.extend(fns)),
"field_defaults" => self.field_defaults.apply_sub_attr(expr),
"builder_method" => self.builder_method.apply_sub_attr(expr),
"builder_type" => self.builder_type.apply_sub_attr(expr),
Expand Down
Loading

0 comments on commit 3d284ec

Please sign in to comment.