From ff068e8d53a1e457e63b9cbf91e96f8625133a06 Mon Sep 17 00:00:00 2001 From: Roland Fredenhagen Date: Tue, 26 Sep 2023 22:10:34 +0200 Subject: [PATCH] Add `mutators` on fields --- CHANGELOG.md | 3 +-- src/lib.rs | 19 +++++++++++++-- tests/tests.rs | 33 ++++++++++++++++++++++++++ typed-builder-macro/src/field_info.rs | 17 +++++++++---- typed-builder-macro/src/lib.rs | 5 ++-- typed-builder-macro/src/struct_info.rs | 9 +++---- typed-builder-macro/src/util.rs | 23 ++++++------------ 7 files changed, 76 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45fd34e0..21696ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- `#[builder(mutators(...))]` to generate functions on `Builder` able to - mutate fields +- `#[builder(mutators(...))]` to generate functions on builder to mutate fields - `#[builder(via_mutator)]` on fields to allow defining fields initialized during `::builder()` for use with `mutators` diff --git a/src/lib.rs b/src/lib.rs index 1188fabb..f25bead1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,6 +121,7 @@ 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. /// @@ -143,6 +144,9 @@ use core::ops::FnOnce; /// - `via_mutators = …` or `via_mutators(init = …)`: initialies the field with the expression `…` /// when constructing the builder, useful in combination with [mutators](#mutators). /// +/// - `mutators(...)` takes functions, that can mutate fields inside of the builder. +/// Mutators specified on a field, mark this field as required, see [mutators](#mutators) for details. +/// /// - `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 @@ -184,6 +188,10 @@ use core::ops::FnOnce; /// 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. /// +/// Mutators on a field, result in them automatically making the field required, i.e., its setter +/// needs to be executed or it needs to be marked as `via_mutators`. Appart from that, they behave +/// identically. +/// /// ``` /// use typed_builder::TypedBuilder; /// @@ -200,6 +208,13 @@ use core::ops::FnOnce; /// } /// ))] /// struct Struct { +/// // Does not require `self: (x)`, due to always +// // requiring, the field the mutator is specifed on. +/// #[builder(mutators( +/// fn x_into_b_field(self) { +/// self.b.push(self.x) +/// } +/// ))] /// x: i32, /// #[builder(via_mutators(init = 1))] /// a: i32, @@ -209,8 +224,8 @@ use core::ops::FnOnce; /// /// // 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]}); +/// Struct::builder().x(2).x_into_b().x_into_b().x_into_b_field().inc_a(2).build(), +/// Struct {x: 2, a: 3, b: vec![2, 2, 2]}); /// ``` pub use typed_builder_macro::TypedBuilder; diff --git a/tests/tests.rs b/tests/tests.rs index 7979e5e8..936aeaaf 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -853,3 +853,36 @@ fn test_mutators_item() { 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 }); } + +#[test] +fn test_mutators_field() { + #[derive(Debug, PartialEq, TypedBuilder)] + #[builder(mutators())] + struct Foo { + #[builder(mutators( + fn inc_x(self: (x)) { + self.x += 1; + } + fn inc_y_by_x(self: (y)) { + self.y += self.x; + } + ))] + x: i32, + #[builder(default)] + y: i32, + #[builder(via_mutators = 2, mutators( + fn inc_preset(self) { + self.z += 1; + self.w += 1; + } + ))] + 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: 0, z: 3, w: 3 }); + let foo = Foo::builder().x(1).y(1).inc_y_by_x().build(); + assert_eq!(foo, Foo { x: 1, y: 2, z: 2, w: 2 }); +} diff --git a/typed-builder-macro/src/field_info.rs b/typed-builder-macro/src/field_info.rs index 3b953d10..09a9a6aa 100644 --- a/typed-builder-macro/src/field_info.rs +++ b/typed-builder-macro/src/field_info.rs @@ -1,9 +1,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote_spanned; -use syn::{parse::Error, spanned::Spanned, ItemFn}; +use syn::{parse::Error, spanned::Spanned}; use crate::util::{ - expr_to_lit_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, ApplyMeta, AttrArg, KeyValue, + expr_to_lit_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, ApplyMeta, AttrArg, KeyValue, Mutator, }; #[derive(Debug)] @@ -23,7 +23,7 @@ impl<'a> FieldInfo<'a> { name, generic_ident: syn::Ident::new(&format!("__{}", strip_raw_ident_prefix(name.to_string())), Span::call_site()), ty: &field.ty, - builder_attr: field_defaults.with(&field.attrs)?, + builder_attr: field_defaults.with(&field.attrs, name)?, } .post_process() } else { @@ -117,7 +117,8 @@ pub struct FieldBuilderAttr<'a> { pub via_mutators: Option, pub deprecated: Option<&'a syn::Attribute>, pub setter: SetterSettings, - pub mutators: Vec, + /// Functions that are able to mutate fields in the builder that are already set + pub mutators: Vec, } #[derive(Debug, Default, Clone)] @@ -133,7 +134,7 @@ pub struct SetterSettings { } impl<'a> FieldBuilderAttr<'a> { - pub fn with(mut self, attrs: &'a [syn::Attribute]) -> Result { + pub fn with(mut self, attrs: &'a [syn::Attribute], name: &Ident) -> Result { for attr in attrs { let list = match &attr.meta { syn::Meta::List(list) => { @@ -164,6 +165,10 @@ impl<'a> FieldBuilderAttr<'a> { self.apply_subsections(list)?; } + for mutator in self.mutators.iter_mut() { + mutator.required_fields.insert(name.clone()); + } + self.inter_fields_conflicts()?; Ok(self) @@ -270,6 +275,8 @@ impl ApplyMeta for FieldBuilderAttr<'_> { } Ok(()) } + + "mutators" => expr.sub_attr()?.undelimited().map(|fns| self.mutators.extend(fns)), _ => Err(Error::new_spanned( expr.name(), format!("Unknown parameter {:?}", expr.name().to_string()), diff --git a/typed-builder-macro/src/lib.rs b/typed-builder-macro/src/lib.rs index 146b8fa7..fee20803 100644 --- a/typed-builder-macro/src/lib.rs +++ b/typed-builder-macro/src/lib.rs @@ -30,9 +30,10 @@ fn impl_my_derive(ast: &syn::DeriveInput) -> Result { .filter(|f| f.builder_attr.default.is_none()) .map(|f| struct_info.required_field_impl(f)); let mutators = struct_info - .builder_attr - .mutators + .fields .iter() + .flat_map(|f| &f.builder_attr.mutators) + .chain(&struct_info.builder_attr.mutators) .map(|m| struct_info.mutator_impl(m)) .collect::>()?; let build_method = struct_info.build_method_impl(); diff --git a/typed-builder-macro/src/struct_info.rs b/typed-builder-macro/src/struct_info.rs index e1479ecf..d3f35dd2 100644 --- a/typed-builder-macro/src/struct_info.rs +++ b/typed-builder-macro/src/struct_info.rs @@ -1,7 +1,5 @@ -use std::collections::HashSet; - use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::{format_ident, quote, ToTokens}; use syn::parse::Error; use syn::punctuated::Punctuated; use syn::{parse_quote, GenericArgument, ItemFn, Token}; @@ -417,12 +415,11 @@ impl<'a> StructInfo<'a> { mutator @ Mutator { fun: mutator_fn, required_fields, - required_fields_span, }: &Mutator, ) -> syn::Result { let StructInfo { ref builder_name, .. } = *self; - let mut required_fields: HashSet<_> = required_fields.iter().collect(); + let mut required_fields = required_fields.clone(); let mut ty_generics = self.generic_argumnents(); let mut destructuring = TokenStream::new(); @@ -446,7 +443,7 @@ impl<'a> StructInfo<'a> { 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 mutator_struct_name = format_ident!("TypedBuilderFieldMutator"); let ItemFn { attrs, vis, .. } = mutator_fn; let sig = mutator.outer_sig(parse_quote!(#builder_name <#ty_generics>)); diff --git a/typed-builder-macro/src/util.rs b/typed-builder-macro/src/util.rs index 006c6b5b..6a11959f 100644 --- a/typed-builder-macro/src/util.rs +++ b/typed-builder-macro/src/util.rs @@ -1,4 +1,4 @@ -use std::iter; +use std::{collections::HashSet, iter}; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, ToTokens}; @@ -308,12 +308,10 @@ pub trait ApplyMeta { } } - -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Mutator { pub fun: ItemFn, - pub required_fields: Vec, - pub required_fields_span: Span, + pub required_fields: HashSet, } impl Parse for Mutator { @@ -321,8 +319,7 @@ impl Parse for Mutator { let mut fun: ItemFn = input.parse()?; // Parse receiver - let requires; - let required_fields_span; + let required_fields; if let Some(FnArg::Receiver(receiver)) = fun.sig.inputs.first_mut() { // If `: (...)` was specified, this will define the required fields if receiver.colon_token.is_some() { @@ -338,17 +335,15 @@ impl Parse for Mutator { )) } }; - requires = fields + required_fields = fields .into_iter() .map(|field| match field { Type::Path(field) => field.path.require_ident().cloned(), other => Err(syn::Error::new_spanned(other, "expected field identifier")), }) .collect::>()?; - required_fields_span = receiver.ty.span(); } else { - requires = vec![]; - required_fields_span = receiver.self_token.span(); + required_fields = Default::default(); } *receiver = parse_quote!(&mut self); } else { @@ -363,11 +358,7 @@ impl Parse for Mutator { )); }; - Ok(Self { - fun, - required_fields: requires, - required_fields_span, - }) + Ok(Self { fun, required_fields }) } }