Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
- `#[builder(setter(transform_generics = "<...>"))]` attribute for adding custom generics and lifetimes to the closure provided to `#[builder(setter(transform = ...))]`

## 0.21.2 - 2025-08-21
### Fixed
- Recognize `TypeGroup` when checking for `Option`.
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ use core::ops::FnOnce;
/// transformed into the field type using the expression `expr`. The transformation is performed
/// when the setter is called.
///
/// - `transform_generics = <'a, A, B, C, ...>`: used in conjunction with `transform`,
/// these generics will be added to the builder method, and therefore available in the provided
/// `transform` setter. Using prefix underscores, e.g. `'__a`/`__A` is recommended
/// to avoid name clashes with others on the builder struct.
///
/// - `prefix = "..."` prepends the setter method with the specified prefix. For example, setting
/// `prefix = "with_"` results in setters like `with_x` or `with_y`. This option is combinable
/// with `suffix = "..."`.
Expand Down
44 changes: 44 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@
#[allow(clippy::items_after_statements)]
fn test_clone_builder_with_generics() {
#[derive(PartialEq, Default)]
struct Uncloneable;

Check warning on line 650 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, nightly)

struct `Uncloneable` is never constructed

#[derive(PartialEq, TypedBuilder)]
struct Foo<T> {
Expand Down Expand Up @@ -763,6 +763,50 @@
);
}

#[test]
fn test_field_setter_transform_with_generics() {
struct MBaseCase;

struct MClosure;

// Lifetime is not needed, just added to test.
trait IntoValue<'a, T, M> {
fn into_value(self) -> T;
}

impl<T, I> IntoValue<'_, T, MBaseCase> for I
where
I: Into<T>,
{
fn into_value(self) -> T {
self.into()
}
}

impl<T, F> IntoValue<'_, T, MClosure> for F
where
F: FnOnce() -> T,
{
fn into_value(self) -> T {
self()
}
}

#[derive(TypedBuilder)]
struct Foo {
#[builder(
setter(
transform_generics = "<'__a, __Marker>",
transform = |value: impl IntoValue<'__a, String, __Marker>| value.into_value()
)
)]
s: String,
}

assert_eq!(Foo::builder().s("foo").build().s, "foo".to_owned());
assert_eq!(Foo::builder().s(|| "foo".to_owned()).build().s, "foo".to_owned());
}

#[test]
fn test_build_method() {
#[derive(PartialEq, TypedBuilder)]
Expand Down
11 changes: 11 additions & 0 deletions typed-builder-macro/src/field_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub struct SetterSettings {
pub strip_option: Option<Strip>,
pub strip_bool: Option<Strip>,
pub transform: Option<Transform>,
pub transform_generics: Option<syn::Generics>,
pub prefix: Option<String>,
pub suffix: Option<String>,
}
Expand Down Expand Up @@ -325,6 +326,16 @@ impl ApplyMeta for SetterSettings {
};
Ok(())
}
"transform_generics" => {
self.transform_generics = if let Some(key_value) = expr.key_value_or_not()? {
let lit_str: syn::LitStr = syn::parse2(key_value.value)?;
let generics: syn::Generics = lit_str.parse()?;
Some(generics)
} else {
None
};
Ok(())
}
"prefix" => {
self.prefix = if let Some(key_value) = expr.key_value_or_not()? {
Some(expr_to_lit_string(&key_value.parse_value()?)?)
Expand Down
25 changes: 16 additions & 9 deletions typed-builder-macro/src/struct_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,16 +315,23 @@ impl<'a> StructInfo<'a> {
}
});

let (param_list, arg_expr) = if field.builder_attr.setter.strip_bool.is_some() {
(quote!(), quote!(true))
let (method_generics, param_list, arg_expr) = if field.builder_attr.setter.strip_bool.is_some() {
(quote!(), 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 }))
let method_generics = field
.builder_attr
.setter
.transform_generics
.as_ref()
.map_or(quote!(), |g| g.to_token_stream());

(method_generics, quote!(#(#params),*), quote!({ #body }))
} else if option_was_stripped {
(quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
(quote!(), quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
} else {
(quote!(#field_name: #arg_type), arg_expr)
(quote!(), quote!(#field_name: #arg_type), arg_expr)
};

let repeated_fields_error_type_name = syn::Ident::new(
Expand All @@ -344,7 +351,7 @@ impl<'a> StructInfo<'a> {
#deprecated
#doc
#[allow(clippy::used_underscore_binding, clippy::no_effect_underscore_binding)]
pub fn #method_name (self, #param_list) -> #builder_name <#target_generics> {
pub fn #method_name #method_generics (self, #param_list) -> #builder_name <#target_generics> {
let #field_name = (#arg_expr,);
let ( #(#destructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -362,7 +369,7 @@ impl<'a> StructInfo<'a> {
#deprecated
#doc
#[allow(clippy::used_underscore_binding, clippy::no_effect_underscore_binding)]
pub fn #method_name (self, #param_list) -> #builder_name <#target_generics> {
pub fn #method_name #method_generics (self, #param_list) -> #builder_name <#target_generics> {
let #field_name = (#arg_expr,);
let ( #(#destructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -382,7 +389,7 @@ impl<'a> StructInfo<'a> {
#deprecated
#doc
#[allow(clippy::used_underscore_binding, clippy::no_effect_underscore_binding)]
pub fn #method_name (self, #param_list) -> #builder_name <#target_generics> {
pub fn #method_name #method_generics (self, #param_list) -> #builder_name <#target_generics> {
let #field_name = (#arg_expr,);
let ( #(#destructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -405,7 +412,7 @@ impl<'a> StructInfo<'a> {
note = #repeated_fields_error_message
)]
#doc
pub fn #method_name (self, _: #repeated_fields_error_type_name) -> #builder_name <#target_generics> {
pub fn #method_name #method_generics (self, _: #repeated_fields_error_type_name) -> #builder_name <#target_generics> {
self
}
}
Expand Down
Loading