From 14f8b71490e0b2d3e3c28f6ebc60a8b23b96ffe5 Mon Sep 17 00:00:00 2001 From: Andrii Kuteiko Date: Fri, 19 Mar 2021 21:46:29 +0100 Subject: [PATCH] Add an option to extend collection-like fields --- derive_builder/tests/setter_extend.rs | 94 +++++++++++++++++++ .../src/macro_options/darling_opts.rs | 3 + derive_builder_core/src/setter.rs | 27 +++++- 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 derive_builder/tests/setter_extend.rs diff --git a/derive_builder/tests/setter_extend.rs b/derive_builder/tests/setter_extend.rs new file mode 100644 index 00000000..1701dda6 --- /dev/null +++ b/derive_builder/tests/setter_extend.rs @@ -0,0 +1,94 @@ +#[macro_use] +extern crate pretty_assertions; +#[macro_use] +extern crate derive_builder; + +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Default, Builder, Clone)] +struct Lorem { + #[builder(setter(each = "foo_append"))] + foo: String, + #[builder(setter(each = "bar"))] + bars: Vec, + #[builder(setter(each = "baz"))] + bazes: HashMap, +} + +#[derive(Debug, PartialEq, Default, Builder, Clone)] +#[builder(pattern = "mutable")] +struct Ipsum { + #[builder(setter(each = "foo_append"))] + foo: String, + #[builder(setter(each = "bar"))] + bars: Vec, + #[builder(setter(each = "baz"))] + bazes: HashMap, +} + +#[test] +fn extend_field() { + let x = LoremBuilder::default() + .foo("foo".into()) + .bar("bar".into()) + .bar("bar bar".into()) + .bar("bar bar bar".into()) + .foo_append('-') + .baz(("baz".into(), 1)) + .baz(("bazz".into(), 2)) + .baz(("bazzz".into(), 3)) + .foo_append("foo") + .build() + .unwrap(); + + assert_eq!( + x, + Lorem { + foo: "foo-foo".into(), + bars: vec!["bar".into(), "bar bar".into(), "bar bar bar".into()], + bazes: vec![("baz".into(), 1), ("bazz".into(), 2), ("bazzz".into(), 3)] + .into_iter() + .collect(), + } + ); +} + +#[test] +fn extend_field_mutable() { + let x = IpsumBuilder::default() + .foo("foo".into()) + .bar("bar".into()) + .bar("bar bar".into()) + .bar("bar bar bar".into()) + .foo_append('-') + .baz(("baz".into(), 1)) + .baz(("bazz".into(), 2)) + .baz(("bazzz".into(), 3)) + .foo_append("foo") + .build() + .unwrap(); + + assert_eq!( + x, + Ipsum { + foo: "foo-foo".into(), + bars: vec!["bar".into(), "bar bar".into(), "bar bar bar".into()], + bazes: vec![("baz".into(), 1), ("bazz".into(), 2), ("bazzz".into(), 3)] + .into_iter() + .collect(), + } + ); +} + +#[derive(Debug, PartialEq, Default, Builder, Clone)] +#[builder(setter(skip))] +struct Dolor { + #[builder(setter(each = "foo"))] + foos: Vec, +} + +#[test] +fn extend_field_enabled() { + let x = DolorBuilder::default().foo(1).foo(2).build().unwrap(); + assert_eq!(x, Dolor { foos: vec![1, 2] }); +} diff --git a/derive_builder_core/src/macro_options/darling_opts.rs b/derive_builder_core/src/macro_options/darling_opts.rs index 5c59a729..97e40efa 100644 --- a/derive_builder_core/src/macro_options/darling_opts.rs +++ b/derive_builder_core/src/macro_options/darling_opts.rs @@ -130,6 +130,7 @@ pub struct FieldLevelSetter { strip_option: Option, skip: Option, custom: Option, + each: Option, } impl FieldLevelSetter { @@ -156,6 +157,7 @@ impl FieldLevelSetter { || self.name.is_some() || self.into.is_some() || self.strip_option.is_some() + || self.each.is_some() { return Some(true); } @@ -555,6 +557,7 @@ impl<'a> FieldWithDefaults<'a> { generic_into: self.setter_into(), strip_option: self.setter_strip_option(), deprecation_notes: self.deprecation_notes(), + each: self.field.setter.each.as_ref(), } } diff --git a/derive_builder_core/src/setter.rs b/derive_builder_core/src/setter.rs index 3f8fcb04..67f99bbe 100644 --- a/derive_builder_core/src/setter.rs +++ b/derive_builder_core/src/setter.rs @@ -62,6 +62,8 @@ pub struct Setter<'a> { pub strip_option: bool, /// Emit deprecation notes to the user. pub deprecation_notes: &'a DeprecationNotes, + /// Emit extend method. + pub each: Option<&'a syn::Ident>, } impl<'a> ToTokens for Setter<'a> { @@ -135,7 +137,8 @@ impl<'a> ToTokens for Setter<'a> { let mut new = #self_into_return_ty; new.#field_ident = ::derive_builder::export::core::option::Option::Some(#into_value); new - })); + } + )); if self.try_setter { let try_ty_params = @@ -151,7 +154,26 @@ impl<'a> ToTokens for Setter<'a> { let mut new = #self_into_return_ty; new.#field_ident = ::derive_builder::export::core::option::Option::Some(converted); Ok(new) - })); + } + )); + } + + if let Some(ref ident_each) = self.each { + tokens.append_all(quote!( + #(#attrs)* + #[allow(unused_mut)] + #vis fn #ident_each (#self_param, item: VALUE) -> #return_ty + where + #ty: ::derive_builder::export::core::default::Default + ::derive_builder::export::core::iter::Extend, + { + #deprecation_notes + let mut new = #self_into_return_ty; + new.#field_ident + .get_or_insert_with(::derive_builder::export::core::default::Default::default) + .extend(::derive_builder::export::core::option::Option::Some(item)); + new + } + )); } } } @@ -222,6 +244,7 @@ macro_rules! default_setter { generic_into: false, strip_option: false, deprecation_notes: &Default::default(), + each: None, }; }; }