diff --git a/CHANGELOG.md b/CHANGELOG.md index 694c018e..80148f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added +- Add `#[builder(setter(strip_option(fallback = field_opt)))]` to add a fallback unstripped method to the builder struct. + ## 0.19.1 - 2024-07-14 ### Fixed - Fix mutators for generic fields (see issue #149) diff --git a/src/lib.rs b/src/lib.rs index 2a9a8588..647b08d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -165,6 +165,11 @@ use core::ops::FnOnce; /// one cannot set the field to `None` with the setter - so the only way to get it to be `None` /// is by using `#[builder(default)]` and not calling the field's setter. /// +/// - `strip_option(fallback = field_opt)`: for `Option<...>` fields only. As above this +/// still wraps the argument with `Some(...)`. The name given to the fallback method adds +/// another method to the builder without wrapping the argument in `Some`. You can now call +/// `field_opt(Some(...))` instead of `field(...)`. +/// /// - `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`. /// @@ -359,5 +364,41 @@ impl Optional for (T,) { /// /// #[deny(deprecated)] /// Foo::builder().value(42).build(); -///``` +/// ``` +/// +/// Handling invalid property for `strip_option` +/// +/// ```compile_fail +/// use typed_builder::TypedBuilder; +/// +/// #[derive(TypedBuilder)] +/// struct Foo { +/// #[builder(setter(strip_option(invalid_field = "should_fail")))] +/// value: Option, +/// } +/// ``` +/// +/// Handling multiple properties for `strip_option` +/// +/// ```compile_fail +/// use typed_builder::TypedBuilder; +/// +/// #[derive(TypedBuilder)] +/// struct Foo { +/// #[builder(setter(strip_option(fallback = value_opt, fallback = value_opt2)))] +/// value: Option, +/// } +/// ``` +/// +/// Handling alternative properties for `strip_option` +/// +/// ```compile_fail +/// use typed_builder::TypedBuilder; +/// +/// #[derive(TypedBuilder)] +/// struct Foo { +/// #[builder(setter(strip_option(type = value_opt, fallback = value_opt2)))] +/// value: Option, +/// } +/// ``` fn _compile_fail_tests() {} diff --git a/tests/tests.rs b/tests/tests.rs index 1ab670eb..ae879d0f 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -143,6 +143,30 @@ fn test_into_with_strip_option() { assert!(Foo::builder().x(1_u8).build() == Foo { x: Some(1) }); } +#[test] +fn test_strip_option_with_fallback() { + #[derive(PartialEq, TypedBuilder)] + struct Foo { + #[builder(setter(strip_option(fallback = x_opt)))] + x: Option, + } + + assert!(Foo::builder().x(1).build() == Foo { x: Some(1) }); + assert!(Foo::builder().x_opt(Some(1)).build() == Foo { x: Some(1) }); +} + +#[test] +fn test_into_with_strip_option_with_fallback() { + #[derive(PartialEq, TypedBuilder)] + struct Foo { + #[builder(setter(into, strip_option(fallback = x_opt)))] + x: Option, + } + + assert!(Foo::builder().x(1_u8).build() == Foo { x: Some(1) }); + assert!(Foo::builder().x_opt(Some(1)).build() == Foo { x: Some(1) }); +} + #[test] fn test_strip_bool() { #[derive(PartialEq, TypedBuilder)] diff --git a/typed-builder-macro/src/field_info.rs b/typed-builder-macro/src/field_info.rs index 82ddad3c..8d03bc10 100644 --- a/typed-builder-macro/src/field_info.rs +++ b/typed-builder-macro/src/field_info.rs @@ -127,7 +127,7 @@ pub struct SetterSettings { pub doc: Option, pub skip: Option, pub auto_into: Option, - pub strip_option: Option, + pub strip_option: Option, pub strip_bool: Option, pub transform: Option, pub prefix: Option, @@ -195,7 +195,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()), + ("strip_option", self.setter.strip_option.as_ref().map(|s| &s.span)), ("strip_bool", self.setter.strip_bool.as_ref()), ]; let mut conflicting_transformations = conflicting_transformations @@ -335,7 +335,41 @@ impl ApplyMeta for SetterSettings { } "skip" => expr.apply_flag_to_field(&mut self.skip, "skipped"), "into" => expr.apply_flag_to_field(&mut self.auto_into, "calling into() on the argument"), - "strip_option" => expr.apply_flag_to_field(&mut self.strip_option, "putting the argument in Some(...)"), + "strip_option" => { + let caption = "putting the argument in Some(...)"; + + match expr { + AttrArg::Sub(sub) => { + let span = sub.span(); + + if self.strip_option.is_none() { + let mut strip_option = Strip::new(span); + strip_option.apply_sub_attr(sub)?; + self.strip_option = Some(strip_option); + + Ok(()) + } else { + Err(Error::new(span, format!("Illegal setting - field is already {caption}"))) + } + } + AttrArg::Flag(flag) => { + if self.strip_option.is_none() { + self.strip_option = Some(Strip::new(flag.span())); + Ok(()) + } else { + Err(Error::new( + flag.span(), + format!("Illegal setting - field is already {caption}"), + )) + } + } + AttrArg::Not { .. } => { + self.strip_option = None; + Ok(()) + } + _ => Err(expr.incorrect_type()), + } + } "strip_bool" => expr.apply_flag_to_field(&mut self.strip_bool, "zero arguments setter, sets the field to true"), _ => Err(Error::new_spanned( expr.name(), @@ -345,6 +379,41 @@ impl ApplyMeta for SetterSettings { } } +#[derive(Debug, Clone)] +pub struct Strip { + pub fallback: Option, + span: Span, +} + +impl Strip { + fn new(span: Span) -> Self { + Self { fallback: None, span } + } +} + +impl ApplyMeta for Strip { + fn apply_meta(&mut self, expr: AttrArg) -> Result<(), Error> { + match expr.name().to_string().as_str() { + "fallback" => { + if self.fallback.is_some() { + return Err(Error::new_spanned( + expr.name(), + format!("Duplicate fallback parameter {:?}", expr.name().to_string()), + )); + } + + let ident: syn::Ident = expr.key_value().map(|kv| kv.parse_value())??; + self.fallback = Some(ident); + Ok(()) + } + _ => Err(Error::new_spanned( + expr.name(), + format!("Invalid parameter used {:?}", expr.name().to_string()), + )), + } + } +} + #[derive(Debug, Clone)] pub struct Transform { pub params: Vec<(syn::Pat, syn::Type)>, diff --git a/typed-builder-macro/src/struct_info.rs b/typed-builder-macro/src/struct_info.rs index a68ea1a5..3ace99bd 100644 --- a/typed-builder-macro/src/struct_info.rs +++ b/typed-builder-macro/src/struct_info.rs @@ -202,15 +202,18 @@ impl<'a> StructInfo<'a> { fn field_impl(&self, field: &FieldInfo) -> syn::Result { let StructInfo { ref builder_name, .. } = *self; - let descructuring = self.included_fields().map(|f| { - if f.ordinal == field.ordinal { - quote!(()) - } else { - let name = f.name; - name.to_token_stream() - } - }); - let reconstructing = self.included_fields().map(|f| f.name); + let destructuring = self + .included_fields() + .map(|f| { + if f.ordinal == field.ordinal { + quote!(()) + } else { + let name = f.name; + name.to_token_stream() + } + }) + .collect::>(); + let reconstructing = self.included_fields().map(|f| f.name).collect::>(); let &FieldInfo { name: field_name, @@ -273,13 +276,18 @@ impl<'a> StructInfo<'a> { (arg_type.to_token_stream(), field_name.to_token_stream()) }; + let mut strip_option_fallback: Option<(Ident, TokenStream, TokenStream)> = None; let (param_list, arg_expr) = if field.builder_attr.setter.strip_bool.is_some() { (quote!(), quote!(true)) } else if let Some(transform) = &field.builder_attr.setter.transform { let params = transform.params.iter().map(|(pat, ty)| quote!(#pat: #ty)); let body = &transform.body; (quote!(#(#params),*), quote!({ #body })) - } else if field.builder_attr.setter.strip_option.is_some() { + } else if let Some(ref strip_option) = field.builder_attr.setter.strip_option { + if let Some(ref fallback) = strip_option.fallback { + strip_option_fallback = Some((fallback.clone(), quote!(#field_name: #field_type), quote!(#arg_expr))); + } + (quote!(#field_name: #arg_type), quote!(Some(#arg_expr))) } else { (quote!(#field_name: #arg_type), arg_expr) @@ -297,6 +305,24 @@ 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 { + 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 + }; + Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] #[automatically_derived] @@ -306,12 +332,13 @@ impl<'a> StructInfo<'a> { #[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 ( #(#descructuring,)* ) = self.fields; + let ( #(#destructuring,)* ) = self.fields; #builder_name { fields: ( #(#reconstructing,)* ), phantom: self.phantom, } } + #fallback_method } #[doc(hidden)] #[allow(dead_code, non_camel_case_types, non_snake_case)] @@ -571,7 +598,7 @@ impl<'a> StructInfo<'a> { )); }); - let descructuring = self.included_fields().map(|f| f.name); + let destructuring = self.included_fields().map(|f| f.name); // The default of a field can refer to earlier-defined fields, which we handle by // writing out a bunch of `let` statements first, which can each refer to earlier ones. @@ -634,7 +661,7 @@ impl<'a> StructInfo<'a> { #build_method_doc #[allow(clippy::default_trait_access, clippy::used_underscore_binding, clippy::no_effect_underscore_binding)] #build_method_visibility fn #build_method_name #build_method_generic (self) -> #output_type #build_method_where_clause { - let ( #(#descructuring,)* ) = self.fields; + let ( #(#destructuring,)* ) = self.fields; #( #assignments )* #[allow(deprecated)]