Skip to content

Commit

Permalink
Merge pull request #150 from ifiokjr/strip_option
Browse files Browse the repository at this point in the history
feat: add `strip_option(fallback = field_opt)`
  • Loading branch information
idanarye authored Aug 22, 2024
2 parents 55e10ea + 51e1022 commit 60ae4b6
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 42 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
///
Expand Down Expand Up @@ -359,5 +364,41 @@ impl<T> Optional<T> 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<i32>,
/// }
/// ```
///
/// 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<i32>,
/// }
/// ```
///
/// 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<i32>,
/// }
/// ```
fn _compile_fail_tests() {}
24 changes: 24 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32>,
}

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<i32>,
}

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)]
Expand Down
75 changes: 72 additions & 3 deletions typed-builder-macro/src/field_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ pub struct SetterSettings {
pub doc: Option<syn::Expr>,
pub skip: Option<Span>,
pub auto_into: Option<Span>,
pub strip_option: Option<Span>,
pub strip_option: Option<Strip>,
pub strip_bool: Option<Span>,
pub transform: Option<Transform>,
pub prefix: Option<String>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -345,6 +379,41 @@ impl ApplyMeta for SetterSettings {
}
}

#[derive(Debug, Clone)]
pub struct Strip {
pub fallback: Option<syn::Ident>,
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)>,
Expand Down
53 changes: 40 additions & 13 deletions typed-builder-macro/src/struct_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,18 @@ impl<'a> StructInfo<'a> {
fn field_impl(&self, field: &FieldInfo) -> syn::Result<TokenStream> {
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::<Vec<_>>();
let reconstructing = self.included_fields().map(|f| f.name).collect::<Vec<_>>();

let &FieldInfo {
name: field_name,
Expand Down Expand Up @@ -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)
Expand All @@ -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]
Expand All @@ -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)]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit 60ae4b6

Please sign in to comment.