Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

## 0.21.3 - 2025-09-05

### Added
- New optional alternate `transform` syntax using a full fn, to allow support for custom lifetimes, generics and a where clause to custom builder method.

Example:
```rust
#[derive(TypedBuilder)]
struct Foo {
#[builder(
setter(
fn transform<'a, M>(value: impl IntoValue<'a, String, M>) -> String
where
M: std::fmt::Display
{
value.into_value()
},
)
)]
s: String,
}
```

## 0.21.2 - 2025-08-21
### Fixed
- Recognize `TypeGroup` when checking for `Option`.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = [".", "./typed-builder-macro"]

[workspace.package]
description = "Compile-time type-checked builder derive"
version = "0.21.2"
version = "0.21.3"
authors = ["IdanArye <[email protected]>", "Chris Morgan <[email protected]>"]
edition = "2021"
license = "MIT OR Apache-2.0"
Expand All @@ -27,4 +27,4 @@ keywords.workspace = true
categories.workspace = true

[dependencies]
typed-builder-macro = { path = "typed-builder-macro", version = "=0.21.2" }
typed-builder-macro = { path = "typed-builder-macro", version = "=0.21.3" }
16 changes: 15 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,21 @@ use core::ops::FnOnce;
/// - `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
/// when the setter is called.
/// when the setter is called. `transform` can also be provided in full `fn` syntax,
/// to allow custom lifetimes, a generic and a where clause.
/// Example:
/// ```rust
/// #[builder(
/// setter(
/// fn transform<'a, M>(value: impl IntoValue<'a, String, M>) -> String
/// where
/// M: 'a,
/// {
/// value.into_value()
/// },
/// )
/// )]
/// ```
///
/// - `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
Expand Down
68 changes: 67 additions & 1 deletion 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 @@ -742,7 +742,7 @@
}

#[test]
fn test_field_setter_transform() {
fn test_field_setter_transform_closure() {
#[derive(PartialEq)]
struct Point {
x: i32,
Expand All @@ -763,6 +763,72 @@
);
}

#[test]
fn test_field_setter_transform_fn() {
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(
fn transform<'a, M>(value: impl IntoValue<'a, String, M>) -> String
where
M: 'a,
{
value.into_value()
},
)
)]
s: String,
}

// Check where clause
#[derive(TypedBuilder)]
struct Bar {
#[builder(
setter(
fn transform<A>(value: A) -> String
where
A: std::fmt::Display,
{
value.to_string()
},
)
)]
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());

assert_eq!(Bar::builder().s(42).build().s, "42".to_owned());
}

#[test]
fn test_build_method() {
#[derive(PartialEq, TypedBuilder)]
Expand Down
88 changes: 66 additions & 22 deletions typed-builder-macro/src/field_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::ops::Deref;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote_spanned;
use syn::{parse::Error, spanned::Spanned};
use syn::{Expr, ExprBlock};

use crate::mutator::Mutator;
use crate::util::{expr_to_lit_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, ApplyMeta, AttrArg};
Expand Down Expand Up @@ -248,6 +249,7 @@ impl ApplyMeta for FieldBuilderAttr<'_> {
Ok(())
}
AttrArg::Sub(_) => Err(expr.incorrect_type()),
AttrArg::Fn(_) => Err(expr.incorrect_type()),
},
"default_code" => {
use std::str::FromStr;
Expand Down Expand Up @@ -295,6 +297,7 @@ impl ApplyMeta for FieldBuilderAttr<'_> {
self.via_mutators = Some(via_mutators);
}
}
AttrArg::Fn(_) => return Err(expr.incorrect_type()),
}
Ok(())
}
Expand All @@ -318,8 +321,16 @@ impl ApplyMeta for SetterSettings {
Ok(())
}
"transform" => {
self.transform = if let Some(key_value) = expr.key_value_or_not()? {
Some(parse_transform_closure(key_value.name.span(), key_value.parse_value()?)?)
self.transform = if let AttrArg::Fn(func) = expr {
Some(parse_transform(func.span(), FnOrClosure::Fn(func))?)
} else if let Some(key_value) = expr.key_value_or_not()? {
let span = key_value.name.span();
let expr = key_value.parse_value::<Expr>()?;
let closure = match expr {
syn::Expr::Closure(closure) => closure,
_ => return Err(Error::new_spanned(expr, "Expected closure")),
};
Some(parse_transform(span, FnOrClosure::Closure(closure))?)
} else {
None
};
Expand Down Expand Up @@ -441,34 +452,67 @@ impl ApplyMeta for Strip {
pub struct Transform {
pub params: Vec<(syn::Pat, syn::Type)>,
pub body: syn::Expr,
pub generics: Option<syn::Generics>,
span: Span,
}

fn parse_transform_closure(span: Span, expr: syn::Expr) -> Result<Transform, Error> {
let closure = match expr {
syn::Expr::Closure(closure) => closure,
_ => return Err(Error::new_spanned(expr, "Expected closure")),
};
if let Some(kw) = &closure.asyncness {
return Err(Error::new(kw.span, "Transform closure cannot be async"));
}
if let Some(kw) = &closure.capture {
return Err(Error::new(kw.span, "Transform closure cannot be move"));
}
enum FnOrClosure {
Fn(syn::ItemFn),
Closure(syn::ExprClosure),
}

fn parse_transform(span: Span, expr: FnOrClosure) -> Result<Transform, Error> {
let mut generics: Option<syn::Generics> = None;
let (params, body) = match expr {
FnOrClosure::Fn(func) => {
if let Some(kw) = &func.sig.asyncness {
return Err(Error::new(kw.span, "Transform function cannot be async"));
}
generics = Some(func.sig.generics);
let params = func
.sig
.inputs
.into_iter()
.map(|input| match input {
syn::FnArg::Typed(pat_type) => Ok((*pat_type.pat, *pat_type.ty)),
syn::FnArg::Receiver(_) => Err(Error::new_spanned(input, "Transform function cannot have self parameter")),
})
.collect::<Result<Vec<_>, _>>()?;
(
params,
Expr::Block(ExprBlock {
attrs: Vec::new(),
label: None,
block: *func.block,
}),
)
}
FnOrClosure::Closure(closure) => {
if let Some(kw) = &closure.asyncness {
return Err(Error::new(kw.span, "Transform closure cannot be async"));
}
if let Some(kw) = &closure.capture {
return Err(Error::new(kw.span, "Transform closure cannot be move"));
}

let params = closure
.inputs
.into_iter()
.map(|input| match input {
syn::Pat::Type(pat_type) => Ok((*pat_type.pat, *pat_type.ty)),
_ => Err(Error::new_spanned(input, "Transform closure must explicitly declare types")),
})
.collect::<Result<Vec<_>, _>>()?;
let params = closure
.inputs
.into_iter()
.map(|input| match input {
syn::Pat::Type(pat_type) => Ok((*pat_type.pat, *pat_type.ty)),
_ => Err(Error::new_spanned(input, "Transform closure must explicitly declare types")),
})
.collect::<Result<Vec<_>, _>>()?;

(params, *closure.body)
}
};

Ok(Transform {
params,
body: *closure.body,
body,
span,
generics,
})
}

Expand Down
32 changes: 23 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,22 @@ 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, method_where_clause) = if field.builder_attr.setter.strip_bool.is_some() {
(quote!(), quote!(), quote!(true), quote!())
} 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 = transform.generics.as_ref().map_or(quote!(), |g| g.to_token_stream());
let method_where_clause = transform
.generics
.as_ref()
.and_then(|g| g.where_clause.as_ref())
.map_or(quote!(), |w| w.to_token_stream());
(method_generics, quote!(#(#params),*), quote!({ #body }), method_where_clause)
} else if option_was_stripped {
(quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
(quote!(), quote!(#field_name: #arg_type), quote!(Some(#arg_expr)), quote!())
} else {
(quote!(#field_name: #arg_type), arg_expr)
(quote!(), quote!(#field_name: #arg_type), arg_expr, quote!())
};

let repeated_fields_error_type_name = syn::Ident::new(
Expand All @@ -344,7 +350,9 @@ 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>
#method_where_clause
{
let #field_name = (#arg_expr,);
let ( #(#destructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -362,7 +370,9 @@ 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>
#method_where_clause
{
let #field_name = (#arg_expr,);
let ( #(#destructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -382,7 +392,9 @@ 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>
#method_where_clause
{
let #field_name = (#arg_expr,);
let ( #(#destructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -405,7 +417,9 @@ 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>
#method_where_clause
{
self
}
}
Expand Down
Loading
Loading