Skip to content

Commit

Permalink
Merge pull request #116 from ModProg/mutators
Browse files Browse the repository at this point in the history
Implement mutators
  • Loading branch information
idanarye committed Oct 15, 2023
2 parents 75ebc4b + 06d2dc5 commit cdbff99
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ 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 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
72 changes: 65 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ use core::ops::FnOnce;
///
/// # Customization with attributes
///
/// In addition to putting `#[derive(TypedBuilder)]` on a type, you can specify a `#[builder()]`
/// In addition to putting `#[derive(TypedBuilder)]` on a type, you can specify a `#[builder(...)]`
/// attribute on the type, and on any fields in it.
///
/// On the **type**, the following values are permitted:
Expand All @@ -78,9 +78,9 @@ use core::ops::FnOnce;
/// - `build_method(...)`: customize the final build method
///
/// All have the same fields:
/// - `vis = ""`: sets the visibility of the build method, default is `pub`
/// - `name = `: sets the fn name of the build method, default is `build`
/// - `doc = ""` replaces the default documentation that will be generated for the
/// - `vis = "..."`: sets the visibility of the build method, default is `pub`
/// - `name = ...`: sets the fn name of the build method, default is `build`
/// - `doc = "..."` replaces the default documentation that will be generated for the
/// `build()` method of the builder type. Setting this implies `doc`.
///
///
Expand Down Expand Up @@ -122,22 +122,34 @@ use core::ops::FnOnce;
/// 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:
///
/// - `default`: make the field optional, defaulting to `Default::default()`. This requires that
/// the field type implement `Default`. Mutually exclusive with any other form of default.
///
/// - `default = `: make the field optional, defaulting to the expression ``.
/// - `default = ...`: make the field optional, defaulting to the expression `...`.
///
/// - `default_code = ""`: make the field optional, defaulting to the expression ``. Note that
/// - `default_code = "..."`: make the field optional, defaulting to the expression `...`. Note that
/// you need to enclose it in quotes, which allows you to use it together with other custom
/// derive proc-macro crates that complain about "expected literal".
/// 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).
///
/// - `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
/// - `doc = "..."`: sets the documentation for the field's setter on the builder type. This will be
/// of no value unless you enable docs for the builder type with `#[builder(doc)]` or similar on
/// the type.
///
Expand Down Expand Up @@ -174,6 +186,52 @@ use core::ops::FnOnce;
/// later-defined fields.
/// **Warning** - Use this feature with care! If the field that mutates the previous field in
/// its `default` expression is set via a setter, that mutation will not happen.
///
/// # 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,
/// that the mutator accesses need to be delcared using `#[mutator(requires = [field1, field2, ...])]`.
/// The mutator will only be availible to call when they are set.
///
/// Mutators on a field, result in them automatically making the field required, i.e., it needs to be
/// marked as `via_mutators`, or its setter be called. Appart from that, they behave identically.
///
/// ```
/// use typed_builder::TypedBuilder;
///
/// #[derive(PartialEq, Debug, TypedBuilder)]
/// #[builder(mutators(
/// // Mutator has only acces to fields marked as `via_mutators`.
/// fn inc_a(&mut self, a: i32){
/// self.a += a;
/// }
/// // Mutator has access to `x` additionally.
/// #[mutator(requires = [x])]
/// fn x_into_b(&mut self) {
/// self.b.push(self.x)
/// }
/// ))]
/// struct Struct {
/// // Does not require explicit `requires = [x]`, as the field
/// // the mutator is specifed on, is required implicitly.
/// #[builder(mutators(
/// fn x_into_b_field(self) {
/// self.b.push(self.x)
/// }
/// ))]
/// 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().x_into_b_field().inc_a(2).build(),
/// Struct {x: 2, a: 3, b: vec![2, 2, 2]});
/// ```
pub use typed_builder_macro::TypedBuilder;

#[doc(hidden)]
Expand Down
89 changes: 89 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,3 +821,92 @@ fn test_mutable_defaults() {

assert_eq!(foo, Foo { x: Some(10), y: 10 });
}

#[test]
fn test_preinitialized_fields() {
#[derive(Debug, PartialEq, TypedBuilder)]
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).build();
assert_eq!(foo, Foo { x: 1, y: 0, z: 2, w: 2 });
}

#[test]
fn test_mutators_item() {
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(mutators(
#[mutator(requires = [x])]
fn inc_x(self) {
self.x += 1;
}
#[mutator(requires = [x])]
fn inc_x_by(self, x: i32) {
self.x += x;
}
fn inc_preset(self) {
self.y += 1;
self.z += 1;
self.w += 1;
}
#[mutator(requires = [x])]
fn inc_y_by_x(self) {
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 });
}

#[test]
fn test_mutators_field() {
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(mutators())]
struct Foo {
#[builder(mutators(
fn inc_x(self) {
self.x += 1;
}
#[mutator(requires = [y])]
fn inc_y_by_x(self) {
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 });
}
48 changes: 43 additions & 5 deletions typed-builder-macro/src/field_info.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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};
use crate::util::{
expr_to_lit_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, ApplyMeta, AttrArg, KeyValue, Mutator,
};

#[derive(Debug)]
pub struct FieldInfo<'a> {
Expand All @@ -21,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 {
Expand Down Expand Up @@ -112,9 +114,11 @@ impl<'a> FieldInfo<'a> {
#[derive(Debug, Default, Clone)]
pub struct FieldBuilderAttr<'a> {
pub default: Option<syn::Expr>,
pub via_mutators: Option<syn::Expr>,
pub deprecated: Option<&'a syn::Attribute>,
pub setter: SetterSettings,
pub mutators: Vec<ItemFn>,
/// Functions that are able to mutate fields in the builder that are already set
pub mutators: Vec<Mutator>,
pub mutable_during_default_resolution: Option<Span>,
}

Expand All @@ -131,7 +135,7 @@ pub struct SetterSettings {
}

impl<'a> FieldBuilderAttr<'a> {
pub fn with(mut self, attrs: &'a [syn::Attribute]) -> Result<Self, Error> {
pub fn with(mut self, attrs: &'a [syn::Attribute], name: &Ident) -> Result<Self, Error> {
for attr in attrs {
let list = match &attr.meta {
syn::Meta::List(list) => {
Expand Down Expand Up @@ -162,6 +166,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)
Expand Down Expand Up @@ -243,6 +251,36 @@ impl ApplyMeta for FieldBuilderAttr<'_> {
&mut self.mutable_during_default_resolution,
"made mutable during default resolution",
),
"via_mutators" => {
match expr {
AttrArg::Flag(ident) => {
self.via_mutators =
Some(syn::parse2(quote_spanned!(ident.span() => ::core::default::Default::default())).unwrap());
}
AttrArg::KeyValue(key_value) => {
self.via_mutators = Some(key_value.value);
}
AttrArg::Not { .. } => {
self.via_mutators = None;
}
AttrArg::Sub(sub) => {
let paren_span = sub.paren.span.span();
let mut args = sub.args()?.into_iter();
let Some(KeyValue { name, value, .. }) = args.next() else {
return Err(Error::new(paren_span, "Expected `init = ...`"));
};
if name != "init" {
return Err(Error::new_spanned(name, "Expected `init`"));
}
if let Some(remaining) = args.next() {
return Err(Error::new_spanned(remaining, "Expected only one argument (`init = ...`)"));
}
self.via_mutators = Some(value);
}
}
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()),
Expand Down
12 changes: 10 additions & 2 deletions typed-builder-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,27 @@ fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
let struct_info = struct_info::StructInfo::new(ast, fields.named.iter())?;
let builder_creation = struct_info.builder_creation_impl()?;
let fields = struct_info
.included_fields()
.setter_fields()
.map(|f| struct_info.field_impl(f))
.collect::<Result<TokenStream, _>>()?;
let required_fields = struct_info
.included_fields()
.setter_fields()
.filter(|f| f.builder_attr.default.is_none())
.map(|f| struct_info.required_field_impl(f));
let mutators = struct_info
.fields
.iter()
.flat_map(|f| &f.builder_attr.mutators)
.chain(&struct_info.builder_attr.mutators)
.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
Loading

0 comments on commit cdbff99

Please sign in to comment.