From 278c256bf3ab7991925ba29fdfb6c95cd7d83968 Mon Sep 17 00:00:00 2001 From: "Ifiok Jr." Date: Thu, 22 Aug 2024 15:08:52 +0100 Subject: [PATCH] feat: add `strip_bool(fallback = field_bool)` --- CHANGELOG.md | 1 + src/lib.rs | 40 +++++++++++++++++++++++ tests/tests.rs | 13 ++++++++ typed-builder-macro/src/field_info.rs | 45 ++++++++++++++++++++++---- typed-builder-macro/src/struct_info.rs | 31 ++++++++++++++++-- 5 files changed, 121 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80148f56..2ecfefc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Add `#[builder(setter(strip_option(fallback = field_opt)))]` to add a fallback unstripped method to the builder struct. +- Add `#[builder(setter(strip_bool(fallback = field_bool)))]` to add a fallback setter that takes the `bool` value to the builder struct. ## 0.19.1 - 2024-07-14 ### Fixed diff --git a/src/lib.rs b/src/lib.rs index 647b08d4..81441ba7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,6 +173,10 @@ use core::ops::FnOnce; /// - `strip_bool`: for `bool` fields only, this makes the setter receive no arguments and simply /// set the field's value to `true`. When used, the `default` is automatically set to `false`. /// +/// - `strip_bool(fallback = field_bool)`: for `bool` fields only. As above this +/// allows passing the boolean value. The name given to the fallback method adds +/// another method to the builder without where the bool value can be specified. +/// /// - `transform = |param1: Type1, param2: Type2 ...| expr`: this makes the setter accept /// `param1: Type1, param2: Type2 ...` instead of the field type itself. The parameters are /// transformed into the field type using the expression `expr`. The transformation is performed @@ -401,4 +405,40 @@ impl Optional for (T,) { /// value: Option, /// } /// ``` +/// +/// Handling invalid property for `strip_bool` +/// +/// ```compile_fail +/// use typed_builder::TypedBuilder; +/// +/// #[derive(TypedBuilder)] +/// struct Foo { +/// #[builder(setter(strip_bool(invalid_field = should_fail)))] +/// value: bool, +/// } +/// ``` +/// +/// Handling multiple propertes for `strip_bool` +/// +/// ```compile_fail +/// use typed_builder::TypedBuilder; +/// +/// #[derive(TypedBuilder)] +/// struct Foo { +/// #[builder(setter(strip_bool(fallback = value_bool, fallback = value_bool2)))] +/// value: bool, +/// } +/// ``` +/// +/// Handling alternative propertes for `strip_bool` +/// +/// ```compile_fail +/// use typed_builder::TypedBuilder; +/// +/// #[derive(TypedBuilder)] +/// struct Foo { +/// #[builder(setter(strip_bool(invalid = value_bool, fallback = value_bool2)))] +/// value: bool, +/// } +/// ``` fn _compile_fail_tests() {} diff --git a/tests/tests.rs b/tests/tests.rs index ae879d0f..c548cefb 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -179,6 +179,19 @@ fn test_strip_bool() { assert!(Foo::builder().build() == Foo { x: false }); } +#[test] +fn test_strip_bool_with_fallback() { + #[derive(PartialEq, TypedBuilder)] + struct Foo { + #[builder(setter(into, strip_bool(fallback = x_bool)))] + x: bool, + } + + assert!(Foo::builder().x().build() == Foo { x: true }); + assert!(Foo::builder().x_bool(false).build() == Foo { x: false }); + assert!(Foo::builder().build() == Foo { x: false }); +} + #[test] fn test_default() { #[derive(PartialEq, TypedBuilder)] diff --git a/typed-builder-macro/src/field_info.rs b/typed-builder-macro/src/field_info.rs index 8d03bc10..cfd266d1 100644 --- a/typed-builder-macro/src/field_info.rs +++ b/typed-builder-macro/src/field_info.rs @@ -89,10 +89,10 @@ impl<'a> FieldInfo<'a> { } fn post_process(mut self) -> Result { - if let Some(ref strip_bool_span) = self.builder_attr.setter.strip_bool { + if let Some(ref strip_bool) = self.builder_attr.setter.strip_bool { if let Some(default_span) = self.builder_attr.default.as_ref().map(Spanned::span) { let mut error = Error::new( - *strip_bool_span, + strip_bool.span, "cannot set both strip_bool and default - default is assumed to be false", ); error.combine(Error::new(default_span, "default set here")); @@ -102,7 +102,7 @@ impl<'a> FieldInfo<'a> { attrs: Default::default(), lit: syn::Lit::Bool(syn::LitBool { value: false, - span: *strip_bool_span, + span: strip_bool.span, }), })); } @@ -128,7 +128,7 @@ pub struct SetterSettings { pub skip: Option, pub auto_into: Option, pub strip_option: Option, - pub strip_bool: Option, + pub strip_bool: Option, pub transform: Option, pub prefix: Option, pub suffix: Option, @@ -196,7 +196,7 @@ impl<'a> FieldBuilderAttr<'a> { let conflicting_transformations = [ ("transform", self.setter.transform.as_ref().map(|t| &t.span)), ("strip_option", self.setter.strip_option.as_ref().map(|s| &s.span)), - ("strip_bool", self.setter.strip_bool.as_ref()), + ("strip_bool", self.setter.strip_bool.as_ref().map(|s| &s.span)), ]; let mut conflicting_transformations = conflicting_transformations .iter() @@ -370,7 +370,40 @@ impl ApplyMeta for SetterSettings { _ => Err(expr.incorrect_type()), } } - "strip_bool" => expr.apply_flag_to_field(&mut self.strip_bool, "zero arguments setter, sets the field to true"), + "strip_bool" => { + let caption = "zero arguments setter, sets the field to true"; + + match expr { + AttrArg::Sub(sub) => { + let span = sub.span(); + + if self.strip_bool.is_none() { + let mut strip_bool = Strip::new(span); + strip_bool.apply_sub_attr(sub)?; + self.strip_bool = Some(strip_bool); + Ok(()) + } else { + Err(Error::new(span, format!("Illegal setting - field is already {caption}"))) + } + } + AttrArg::Flag(flag) => { + if self.strip_bool.is_none() { + self.strip_bool = Some(Strip::new(flag.span())); + Ok(()) + } else { + Err(Error::new( + flag.span(), + format!("Illegal setting - field is already {caption}"), + )) + } + } + AttrArg::Not { .. } => { + self.strip_bool = None; + Ok(()) + } + _ => Err(expr.incorrect_type()), + } + } _ => Err(Error::new_spanned( expr.name(), format!("Unknown parameter {:?}", expr.name().to_string()), diff --git a/typed-builder-macro/src/struct_info.rs b/typed-builder-macro/src/struct_info.rs index 3ace99bd..46d09936 100644 --- a/typed-builder-macro/src/struct_info.rs +++ b/typed-builder-macro/src/struct_info.rs @@ -276,8 +276,14 @@ impl<'a> StructInfo<'a> { (arg_type.to_token_stream(), field_name.to_token_stream()) }; + let mut strip_bool_fallback: Option<(Ident, TokenStream, TokenStream)> = None; let mut strip_option_fallback: Option<(Ident, TokenStream, TokenStream)> = None; - let (param_list, arg_expr) = if field.builder_attr.setter.strip_bool.is_some() { + + let (param_list, arg_expr) = if let Some(ref strip_bool) = field.builder_attr.setter.strip_bool { + if let Some(ref fallback) = strip_bool.fallback { + strip_bool_fallback = Some((fallback.clone(), quote!(#field_name: #field_type), quote!(#arg_expr))); + } + (quote!(), quote!(true)) } else if let Some(transform) = &field.builder_attr.setter.transform { let params = transform.params.iter().map(|(pat, ty)| quote!(#pat: #ty)); @@ -305,7 +311,25 @@ impl<'a> StructInfo<'a> { let method_name = field.setter_method_name(); - let fallback_method = if let Some((method_name, param_list, arg_expr)) = strip_option_fallback { + let strip_option_fallback_method = if let Some((method_name, param_list, arg_expr)) = strip_option_fallback { + Some(quote! { + #deprecated + #doc + #[allow(clippy::used_underscore_binding, clippy::no_effect_underscore_binding)] + pub fn #method_name (self, #param_list) -> #builder_name <#target_generics> { + let #field_name = (#arg_expr,); + let ( #(#destructuring,)* ) = self.fields; + #builder_name { + fields: ( #(#reconstructing,)* ), + phantom: self.phantom, + } + } + }) + } else { + None + }; + + let strip_bool_fallback_method = if let Some((method_name, param_list, arg_expr)) = strip_bool_fallback { Some(quote! { #deprecated #doc @@ -338,7 +362,8 @@ impl<'a> StructInfo<'a> { phantom: self.phantom, } } - #fallback_method + #strip_option_fallback_method + #strip_bool_fallback_method } #[doc(hidden)] #[allow(dead_code, non_camel_case_types, non_snake_case)]