Repository: idanarye/rust-typed-builder (Rust)

Issue:
Improve `field_defaults` semantics
### Description

Here is the current documentation for the `field_defaults(...)` attribute on `TypedBuilder` structs.

> is structured like the #[builder(...)] attribute you can put on the fields and sets default options for fields of the type. If specific field need to revert some options to the default defaults they can prepend ! to the option they need to revert, and it would ignore the field defaults for that option in that field.

I have the following proposal. 

1. For the attributes `strip_option` and `strip_bool` there should be a way to only apply them to fields that are valid. Currently, when setting `strip_option` you need to use `!` to negate it on non-option fields. This should not be required. Perhaps `ignore_invalid` as a sub attribute flag to prevent this being a breaking change. 
2. In #150, support was added for fallback methods with this syntax `strip_option(fallback = custom_name_opt)`. This currently doesn't work for `field_defaults`. There should be an option available for `strip_option` which allows setting a `fallback_prefix` and `fallback_suffix` which adds the fallback to all optional methods automatically. 


### Possible API

```rust
use typed_builder::TypedBuilder;

#[derive(TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(ignore_invalid, fallback_suffix = "_opt")))]
struct Foo {
    // Defaults to None, options-stripping is performed and a fallback 
    // method of `x_opt` is available.
    x: Option<i32>,

    // Defaults to 0, no longer need `!strip_option`:
    y: i32,

    // Defaults to Some(13), option-stripping is performed and a fallback 
    // method of `z_opt` is available:
    #[builder(default = Some(13))]
    z: Option<i32>,
}
```



Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37a22ba4..7b69b97f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ## [Unreleased]
 
+### Added
+- Added `ignore_invalid` option to `strip_option` to skip stripping for non-Option fields
+- Added `fallback_prefix` and `fallback_suffix` options to `strip_option` for customizing fallback method names
+- Added support for field defaults with `strip_option` and its fallback options
+
+### Changed
+- Improved handling of `strip_option` to work better with field defaults
+- Made `strip_option` more flexible with non-Option fields when `ignore_invalid` is set
+
 ## 0.20.1 - 2025-03-14
 ### Fixed
 - Fix mutator with type parameter using associated type (see issue #157)
@@ -44,7 +53,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ### Added
 - `#[builder(mutators(...))]` to generate functions on builder to mutate fields
-- `#[builder(via_mutator)]` on fields to allow defining fields initialized 
+- `#[builder(via_mutator)]` on fields to allow defining fields initialized
   during `::builder()` for use with `mutators`
 - `mutable_during_default_resolution` to allow `default` expression mutate
   previous fields.
diff --git a/src/lib.rs b/src/lib.rs
index 81441ba7..3aa415ca 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -166,9 +166,32 @@ use core::ops::FnOnce;
 ///     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(...)`.
+///     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(...)`.
+///
+///     The `setter(strip_option)` attribute supports several `field_defaults` features:
+///
+///     - `ignore_invalid`: Skip stripping for non-Option fields instead of causing a compile error
+///     - `fallback_prefix`: Add a prefix to every fallback method name
+///     - `fallback_suffix`: Add a suffix to every fallback method name
+///
+///     Example:
+///
+///     ```
+///     use typed_builder::TypedBuilder;
+///
+///     #[derive(TypedBuilder)]
+///     #[builder(field_defaults(setter(strip_option(
+///         ignore_invalid,
+///         fallback_prefix = "opt_",
+///         fallback_suffix = "_val"
+///     ))))]
+///     struct Foo {
+///         x: Option<i32>,  // Can use .x(42) or .opt_x_val(None)
+///         y: i32,          // Uses .y(42) only since it's not an Option
+///     }
+///     ```
 ///
 ///   - `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`.
diff --git a/typed-builder-macro/src/field_info.rs b/typed-builder-macro/src/field_info.rs
index f621c854..1b22800d 100644
--- a/typed-builder-macro/src/field_info.rs
+++ b/typed-builder-macro/src/field_info.rs
@@ -354,12 +354,21 @@ impl ApplyMeta for SetterSettings {
 #[derive(Debug, Clone)]
 pub struct Strip {
     pub fallback: Option<syn::Ident>,
+    pub fallback_prefix: Option<String>,
+    pub fallback_suffix: Option<String>,
+    pub ignore_invalid: bool,
     span: Span,
 }
 
 impl Strip {
     fn new(span: Span) -> Self {
-        Self { fallback: None, span }
+        Self {
+            fallback: None,
+            fallback_prefix: None,
+            fallback_suffix: None,
+            ignore_invalid: false,
+            span,
+        }
     }
 }
 
@@ -378,9 +387,43 @@ impl ApplyMeta for Strip {
                 self.fallback = Some(ident);
                 Ok(())
             }
+            "fallback_prefix" => {
+                if self.fallback_prefix.is_some() {
+                    return Err(Error::new_spanned(
+                        expr.name(),
+                        format!("Duplicate fallback_prefix parameter {:?}", expr.name().to_string()),
+                    ));
+                }
+
+                self.fallback_prefix = Some(expr.key_value()?.parse_value::<syn::LitStr>()?.value());
+                Ok(())
+            }
+            "fallback_suffix" => {
+                if self.fallback_suffix.is_some() {
+                    return Err(Error::new_spanned(
+                        expr.name(),
+                        format!("Duplicate fallback_suffix parameter {:?}", expr.name().to_string()),
+                    ));
+                }
+
+                self.fallback_suffix = Some(expr.key_value()?.parse_value::<syn::LitStr>()?.value());
+                Ok(())
+            }
+            "ignore_invalid" => {
+                if self.ignore_invalid {
+                    return Err(Error::new_spanned(
+                        expr.name(),
+                        format!("Duplicate ignore_invalid parameter {:?}", expr.name().to_string()),
+                    ));
+                }
+
+                expr.flag()?;
+                self.ignore_invalid = true;
+                Ok(())
+            }
             _ => Err(Error::new_spanned(
                 expr.name(),
-                format!("Invalid parameter used {:?}", expr.name().to_string()),
+                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 dbcc3597..baaa4a72 100644
--- a/typed-builder-macro/src/struct_info.rs
+++ b/typed-builder-macro/src/struct_info.rs
@@ -261,13 +261,28 @@ impl<'a> StructInfo<'a> {
 
         let deprecated = &field.builder_attr.deprecated;
 
-        // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
-        // nesting is different, so we have to do this little dance.
+        let options_was_stripped;
         let arg_type = if field.builder_attr.setter.strip_option.is_some() && field.builder_attr.setter.transform.is_none() {
-            field
-                .type_from_inside_option()
-                .ok_or_else(|| Error::new_spanned(field_type, "can't `strip_option` - field is not `Option<...>`"))?
+            if let Some(inner_type) = field.type_from_inside_option() {
+                options_was_stripped = true;
+                inner_type
+            } else if field
+                .builder_attr
+                .setter
+                .strip_option
+                .as_ref()
+                .map_or(false, |s| s.ignore_invalid)
+            {
+                options_was_stripped = false;
+                field_type
+            } else {
+                return Err(Error::new_spanned(
+                    field_type,
+                    "can't `strip_option` - field is not `Option<...>`",
+                ));
+            }
         } else {
+            options_was_stripped = false;
             field_type
         };
         let (arg_type, arg_expr) = if field.builder_attr.setter.auto_into.is_some() {
@@ -283,13 +298,19 @@ impl<'a> StructInfo<'a> {
             .as_ref()
             .and_then(|strip_bool| strip_bool.fallback.as_ref())
             .map(|fallback| (fallback.clone(), quote!(#field_name: #field_type), quote!(#arg_expr)));
-        let strip_option_fallback = field
-            .builder_attr
-            .setter
-            .strip_option
-            .as_ref()
-            .and_then(|strip_option| strip_option.fallback.as_ref())
-            .map(|fallback| (fallback.clone(), quote!(#field_name: #field_type), quote!(#arg_expr)));
+
+        let strip_option_fallback = field.builder_attr.setter.strip_option.as_ref().and_then(|strip_option| {
+            if let Some(ref fallback) = strip_option.fallback {
+                Some((fallback.clone(), quote!(#field_name: #field_type), quote!(#arg_expr)))
+            } else if strip_option.fallback_prefix.is_none() && strip_option.fallback_suffix.is_none() {
+                None
+            } else {
+                let prefix = strip_option.fallback_prefix.as_deref().unwrap_or_default();
+                let suffix = strip_option.fallback_suffix.as_deref().unwrap_or_default();
+                let fallback_name = syn::Ident::new(&format!("{}{}{}", prefix, field_name, suffix), field_name.span());
+                Some((fallback_name, quote!(#field_name: #field_type), quote!(#arg_expr)))
+            }
+        });
 
         let (param_list, arg_expr) = if field.builder_attr.setter.strip_bool.is_some() {
             (quote!(), quote!(true))
@@ -298,7 +319,11 @@ impl<'a> StructInfo<'a> {
             let body = &transform.body;
             (quote!(#(#params),*), quote!({ #body }))
         } else if field.builder_attr.setter.strip_option.is_some() {
-            (quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
+            if options_was_stripped {
+                (quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
+            } else {
+                (quote!(#field_name: #arg_type), arg_expr)
+            }
         } else {
             (quote!(#field_name: #arg_type), arg_expr)
         };


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index be714b59..eddd1fb4 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -532,6 +532,84 @@ fn test_field_defaults_setter_options() {
     assert!(Foo::builder().x(1).y(2).build() == Foo { x: Some(1), y: 2 });
 }
 
+#[test]
+fn test_field_defaults_strip_option_ignore_invalid() {
+    #[derive(TypedBuilder)]
+    #[builder(field_defaults(setter(strip_option(ignore_invalid))))]
+    struct Foo {
+        x: Option<i32>,
+        y: i32,
+        #[builder(default = Some(13))]
+        z: Option<i32>,
+    }
+
+    let foo = Foo::builder().x(42).y(10).build();
+
+    assert_eq!(foo.x, Some(42));
+    assert_eq!(foo.y, 10);
+    assert_eq!(foo.z, Some(13));
+}
+
+#[test]
+fn test_field_defaults_strip_option_fallback_suffix() {
+    #[derive(TypedBuilder)]
+    #[builder(field_defaults(setter(strip_option(fallback_suffix = "_opt"))))]
+    struct Foo {
+        x: Option<i32>,
+        #[builder(default = Some(13))]
+        z: Option<i32>,
+    }
+
+    let foo1 = Foo::builder().x(42).build();
+
+    let foo2 = Foo::builder().x_opt(None).build();
+
+    assert_eq!(foo1.x, Some(42));
+    assert_eq!(foo1.z, Some(13));
+    assert_eq!(foo2.x, None);
+    assert_eq!(foo2.z, Some(13));
+}
+
+#[test]
+fn test_field_defaults_strip_option_fallback_prefix() {
+    #[derive(TypedBuilder)]
+    #[builder(field_defaults(setter(strip_option(fallback_prefix = "opt_"))))]
+    struct Foo {
+        x: Option<i32>,
+        #[builder(default = Some(13))]
+        z: Option<i32>,
+    }
+
+    let foo1 = Foo::builder().x(42).build();
+
+    let foo2 = Foo::builder().opt_x(None).build();
+
+    assert_eq!(foo1.x, Some(42));
+    assert_eq!(foo1.z, Some(13));
+    assert_eq!(foo2.x, None);
+    assert_eq!(foo2.z, Some(13));
+}
+
+#[test]
+fn test_field_defaults_strip_option_fallback_prefix_and_suffix() {
+    #[derive(TypedBuilder)]
+    #[builder(field_defaults(setter(strip_option(fallback_prefix = "opt_", fallback_suffix = "_val"))))]
+    struct Foo {
+        x: Option<i32>,
+        #[builder(default = Some(13))]
+        z: Option<i32>,
+    }
+
+    let foo1 = Foo::builder().x(42).build();
+
+    let foo2 = Foo::builder().opt_x_val(None).build();
+
+    assert_eq!(foo1.x, Some(42));
+    assert_eq!(foo1.z, Some(13));
+    assert_eq!(foo2.x, None);
+    assert_eq!(foo2.z, Some(13));
+}
+
 #[test]
 fn test_clone_builder() {
     #[derive(PartialEq, Default)]