Skip to content

Commit 07a55c3

Browse files
committed
Add generic parameter conversion methods for builders
Add support for generating setter functions that allow changing generic type parameters on builders via the `#[builder(generics(setters(...)))]` attribute. This enables users to write custom builder methods that transform the generic parameters before setting field values.
1 parent 9f43a1f commit 07a55c3

18 files changed

Lines changed: 464 additions & 46 deletions

File tree

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,26 @@ jobs:
145145
cd bon && cargo test ${{ matrix.locked }} --no-default-features \
146146
--features=experimental-overwritable,alloc,implied-bounds
147147
148+
- run: |
149+
cd bon && cargo test ${{ matrix.locked }} \
150+
--features=experimental-generics-setters
151+
152+
- run: |
153+
cd bon && cargo test ${{ matrix.locked }} --no-default-features \
154+
--features=experimental-generics-setters
155+
156+
- run: |
157+
cd bon && cargo test ${{ matrix.locked }} --no-default-features \
158+
--features=experimental-generics-setters,alloc
159+
160+
- run: |
161+
cd bon && cargo test ${{ matrix.locked }} --no-default-features \
162+
--features=experimental-generics-setters,implied-bounds
163+
164+
- run: |
165+
cd bon && cargo test ${{ matrix.locked }} --no-default-features \
166+
--features=experimental-generics-setters,alloc,implied-bounds
167+
148168
test-msrv:
149169
runs-on: ${{ matrix.os }}-latest
150170

benchmarks/compilation/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ edition = "2021"
1212
version = "0.1.0"
1313

1414
[dependencies]
15-
bon = { path = "../../bon", optional = true, features = ["experimental-overwritable"] }
15+
bon = { path = "../../bon", optional = true, features = ["experimental-overwritable", "experimental-generics-setters"] }
1616
cfg-if = "1.0"
1717
derive_builder = { version = "0.20", optional = true }
1818
typed-builder = { version = "0.23", optional = true }

bon-macros/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ std = []
7171
# See the docs on this feature in the `bon`'s crate `Cargo.toml`
7272
experimental-overwritable = []
7373

74+
# See the docs on this feature in the `bon`'s crate `Cargo.toml`
75+
experimental-generics-setters = []
76+
7477
# See the docs on this feature in the `bon`'s crate `Cargo.toml`
7578
implied-bounds = []
7679

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use super::models::BuilderGenCtx;
2+
use crate::parsing::ItemSigConfig;
3+
use crate::util::prelude::*;
4+
5+
pub(super) struct GenericSettersCtx<'a> {
6+
base: &'a BuilderGenCtx,
7+
config: &'a ItemSigConfig<String>,
8+
}
9+
10+
impl<'a> GenericSettersCtx<'a> {
11+
pub(super) fn new(base: &'a BuilderGenCtx, config: &'a ItemSigConfig<String>) -> Self {
12+
Self { base, config }
13+
}
14+
15+
pub(super) fn generic_setter_methods(&self) -> Result<TokenStream> {
16+
let methods = self
17+
.base
18+
.generics
19+
.decl_without_defaults
20+
.iter()
21+
.enumerate()
22+
.filter_map(|(index, param)| {
23+
// Only generate setters for type parameters, not lifetimes or const generics
24+
match param {
25+
syn::GenericParam::Type(type_param) => {
26+
Some(self.generic_setter_method(index, &type_param.ident))
27+
}
28+
_ => None,
29+
}
30+
})
31+
.collect::<Result<Vec<_>>>()?;
32+
33+
Ok(quote! {
34+
#(#methods)*
35+
})
36+
}
37+
38+
fn generic_setter_method(
39+
&self,
40+
param_index: usize,
41+
param_ident: &syn::Ident,
42+
) -> Result<TokenStream> {
43+
let builder_ident = &self.base.builder_type.ident;
44+
let state_var = &self.base.state_var;
45+
let where_clause = &self.base.generics.where_clause;
46+
47+
let method_name = self.method_name(param_ident)?;
48+
49+
let vis = self
50+
.config
51+
.vis
52+
.as_ref()
53+
.map(|v| &v.value)
54+
.unwrap_or(&self.base.builder_type.vis);
55+
56+
let docs = self.method_docs(param_ident);
57+
58+
// Build the generic arguments for the output type, where the current parameter
59+
// is replaced with a new type variable
60+
let new_type_var = format_ident!("{}2", param_ident);
61+
let output_generic_args = self
62+
.base
63+
.generics
64+
.args
65+
.iter()
66+
.enumerate()
67+
.map(|(i, arg)| {
68+
if i == param_index {
69+
quote!(#new_type_var)
70+
} else {
71+
quote!(#arg)
72+
}
73+
})
74+
.collect::<Vec<_>>();
75+
76+
let new_type_param: syn::GenericParam = syn::parse_quote!(#new_type_var);
77+
78+
let where_clause = where_clause.as_ref().map(|wc| quote!(#wc));
79+
80+
// Check which named members use this generic parameter
81+
let named_member_conversions = self
82+
.base
83+
.named_members()
84+
.enumerate()
85+
.map(|(idx, member)| {
86+
let uses_param = self.member_uses_generic_param(member, param_ident);
87+
if uses_param {
88+
// Field uses the generic parameter, so create a new None
89+
quote!(::core::option::Option::None)
90+
} else {
91+
// Field doesn't use the generic parameter, so move it from the tuple
92+
let index = syn::Index::from(idx);
93+
quote!(named.#index)
94+
}
95+
})
96+
.collect::<Vec<_>>();
97+
98+
let receiver_field = self.base.receiver().map(|receiver| {
99+
let ident = &receiver.field_ident;
100+
quote!(#ident: self.#ident,)
101+
});
102+
103+
let start_fn_fields = self.base.start_fn_args().map(|member| {
104+
let ident = &member.ident;
105+
quote!(#ident: self.#ident,)
106+
});
107+
108+
let custom_fields = self.base.custom_fields().map(|field| {
109+
let ident = &field.ident;
110+
quote!(#ident: self.#ident,)
111+
});
112+
113+
Ok(quote! {
114+
#(#docs)*
115+
#[inline(always)]
116+
#vis fn #method_name<#new_type_param>(
117+
self
118+
) -> #builder_ident<#(#output_generic_args,)* #state_var>
119+
#where_clause
120+
{
121+
let named = self.__unsafe_private_named;
122+
123+
#builder_ident {
124+
__unsafe_private_phantom: ::core::marker::PhantomData,
125+
#receiver_field
126+
#(#start_fn_fields)*
127+
#(#custom_fields)*
128+
__unsafe_private_named: (
129+
#(#named_member_conversions,)*
130+
),
131+
}
132+
}
133+
})
134+
}
135+
136+
/// Check if a member's type uses a specific generic parameter
137+
fn member_uses_generic_param(
138+
&self,
139+
member: &super::NamedMember,
140+
param_ident: &syn::Ident,
141+
) -> bool {
142+
let member_ty = member.underlying_norm_ty();
143+
self.type_uses_generic_param(member_ty, param_ident)
144+
}
145+
146+
/// Recursively check if a type uses a specific generic parameter
147+
fn type_uses_generic_param(&self, ty: &syn::Type, param_ident: &syn::Ident) -> bool {
148+
use syn::visit::Visit;
149+
150+
struct GenericParamVisitor<'a> {
151+
param_ident: &'a syn::Ident,
152+
found: bool,
153+
}
154+
155+
impl<'ast> Visit<'ast> for GenericParamVisitor<'_> {
156+
fn visit_path(&mut self, path: &'ast syn::Path) {
157+
// Check if the path is the generic parameter we're looking for
158+
if path.is_ident(self.param_ident) {
159+
self.found = true;
160+
return;
161+
}
162+
163+
// Continue visiting the rest of the path
164+
syn::visit::visit_path(self, path);
165+
}
166+
}
167+
168+
let mut visitor = GenericParamVisitor {
169+
param_ident,
170+
found: false,
171+
};
172+
visitor.visit_type(ty);
173+
visitor.found
174+
}
175+
176+
fn method_name(&self, param_ident: &syn::Ident) -> Result<syn::Ident> {
177+
let param_name = param_ident.to_string();
178+
let param_name_lower = param_name.to_lowercase();
179+
180+
let name_pattern = self
181+
.config
182+
.name
183+
.as_ref()
184+
.map(|n| n.value.as_str())
185+
.unwrap_or("conv_{}");
186+
187+
let method_name = name_pattern.replace("{}", &param_name_lower);
188+
189+
Ok(syn::Ident::new(&method_name, param_ident.span()))
190+
}
191+
192+
fn method_docs(&self, param_ident: &syn::Ident) -> Vec<syn::Attribute> {
193+
// If custom docs are provided, use them
194+
if let Some(ref docs) = self.config.docs {
195+
return docs.value.clone();
196+
}
197+
198+
// Otherwise, generate default documentation
199+
let param_name = param_ident.to_string();
200+
let doc = format!(
201+
"Convert the `{}` generic parameter to a different type.\n\
202+
\n\
203+
This method allows changing the type of the `{}` parameter on the builder, \
204+
which is useful when you need to build up values with different types at \
205+
different stages of construction.",
206+
param_name, param_name
207+
);
208+
209+
vec![syn::parse_quote!(#[doc = #doc])]
210+
}
211+
}

bon-macros/src/builder/builder_gen/input_fn/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ impl<'a> FnInputCtx<'a> {
401401
vis: self.config.builder_type.vis.map(SpannedKey::into_value),
402402
};
403403

404+
let generics_config = self.config.generics.map(SpannedKey::into_value);
405+
404406
BuilderGenCtx::new(BuilderGenCtxParams {
405407
bon: self.config.bon,
406408
namespace: Cow::Borrowed(self.namespace),
@@ -413,6 +415,7 @@ impl<'a> FnInputCtx<'a> {
413415

414416
assoc_method_ctx,
415417
generics,
418+
generics_config,
416419
orig_item_vis: self.fn_item.norm.vis,
417420

418421
builder_type,

bon-macros/src/builder/builder_gen/input_struct.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ impl StructInputCtx {
223223
let mut namespace = GenericsNamespace::default();
224224
namespace.visit_item_struct(&self.struct_item.orig);
225225

226+
let generics_config = self.config.generics.map(SpannedKey::into_value);
227+
226228
BuilderGenCtx::new(BuilderGenCtxParams {
227229
bon: self.config.bon,
228230
namespace: Cow::Owned(namespace),
@@ -235,6 +237,7 @@ impl StructInputCtx {
235237

236238
assoc_method_ctx,
237239
generics,
240+
generics_config,
238241
orig_item_vis: self.struct_item.norm.vis,
239242

240243
builder_type,

bon-macros/src/builder/builder_gen/member/config/setters.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ use syn::punctuated::Punctuated;
77
const DOCS_CONTEXT: &str = "builder struct's impl block";
88

99
fn parse_setter_fn(meta: &syn::Meta) -> Result<SpannedKey<ItemSigConfig>> {
10-
let params = ItemSigConfigParsing {
11-
meta,
12-
reject_self_mentions: Some(DOCS_CONTEXT),
13-
}
14-
.parse()?;
15-
10+
let params = ItemSigConfigParsing::new(meta, Some(DOCS_CONTEXT)).parse()?;
1611
SpannedKey::new(meta.path(), params)
1712
}
1813

bon-macros/src/builder/builder_gen/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod builder_decl;
22
mod builder_derives;
33
mod finish_fn;
4+
mod generic_setters;
45
mod getters;
56
mod member;
67
mod models;
@@ -14,6 +15,7 @@ pub(crate) mod input_struct;
1415
pub(crate) use top_level_config::TopLevelConfig;
1516

1617
use crate::util::prelude::*;
18+
use generic_setters::GenericSettersCtx;
1719
use getters::GettersCtx;
1820
use member::{CustomField, Member, MemberOrigin, NamedMember, PosFnMember, RawMember};
1921
use models::{AssocMethodCtxParams, AssocMethodReceiverCtx, BuilderGenCtx, FinishFnBody, Generics};
@@ -125,6 +127,14 @@ impl BuilderGenCtx {
125127
.into_iter()
126128
.flatten();
127129

130+
let generic_setter_methods = self
131+
.generics_config
132+
.as_ref()
133+
.and_then(|config| config.setters.as_ref())
134+
.map(|config| GenericSettersCtx::new(self, config).generic_setter_methods())
135+
.transpose()?
136+
.unwrap_or_default();
137+
128138
let generics_decl = &self.generics.decl_without_defaults;
129139
let generic_args = &self.generics.args;
130140
let where_clause = &self.generics.where_clause;
@@ -149,6 +159,7 @@ impl BuilderGenCtx {
149159
{
150160
#finish_fn
151161
#(#accessor_methods)*
162+
#generic_setter_methods
152163
}
153164
})
154165
}

bon-macros/src/builder/builder_gen/models.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::member::Member;
2-
use super::top_level_config::{DerivesConfig, OnConfig};
2+
use super::top_level_config::{DerivesConfig, GenericsConfig, OnConfig};
33
use crate::normalization::GenericsNamespace;
44
use crate::parsing::{BonCratePath, ItemSigConfig, SpannedKey};
55
use crate::util::prelude::*;
@@ -172,6 +172,7 @@ pub(crate) struct BuilderGenCtx {
172172
pub(super) on: Vec<OnConfig>,
173173

174174
pub(super) generics: Generics,
175+
pub(super) generics_config: Option<GenericsConfig>,
175176

176177
pub(super) assoc_method_ctx: Option<AssocMethodCtx>,
177178

@@ -200,6 +201,7 @@ pub(super) struct BuilderGenCtxParams<'a> {
200201

201202
/// Generics to apply to the builder type.
202203
pub(super) generics: Generics,
204+
pub(super) generics_config: Option<GenericsConfig>,
203205

204206
pub(super) assoc_method_ctx: Option<AssocMethodCtxParams>,
205207

@@ -219,6 +221,7 @@ impl BuilderGenCtx {
219221
const_,
220222
on,
221223
generics,
224+
generics_config,
222225
orig_item_vis,
223226
assoc_method_ctx,
224227
builder_type,
@@ -372,6 +375,7 @@ impl BuilderGenCtx {
372375
const_,
373376
on,
374377
generics,
378+
generics_config,
375379
assoc_method_ctx,
376380
builder_type,
377381
state_mod,

0 commit comments

Comments
 (0)