Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#[builder(field)] #207

Merged
merged 12 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 123 additions & 0 deletions bon-macros/src/builder/builder_gen/builder_decl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::builder::builder_gen::NamedMember;
use crate::util::prelude::*;

impl super::BuilderGenCtx {
pub(super) fn builder_decl(&self) -> TokenStream {
let builder_vis = &self.builder_type.vis;
let builder_ident = &self.builder_type.ident;
let generics_decl = &self.generics.decl_with_defaults;
let where_clause = &self.generics.where_clause;
let phantom_data = self.phantom_data();
let state_mod = &self.state_mod.ident;
let phantom_field = &self.ident_pool.phantom;
let receiver_field = &self.ident_pool.receiver;
let start_fn_args_field = &self.ident_pool.start_fn_args;
let named_members_field = &self.ident_pool.named_members;

// The fields can't be hidden using Rust's privacy syntax.
// The details about this are described in the blog post:
// https://bon-rs.com/blog/the-weird-of-function-local-types-in-rust.
//
// We could use `#[cfg(not(rust_analyzer))]` to hide the private fields in IDE.
// However, RA would then not be able to type-check the generated code, which
// may or may not be a problem, because the main thing is that the type signatures
// would still work in RA.
let private_field_attrs = {
// The message is defined separately to make it single-line in the
// generated code. This simplifies the task of removing unnecessary
// attributes from the generated code when preparing for demo purposes.
let deprecated_msg = "\
this field should not be used directly; it's an implementation detail \
if you found yourself needing it, then you are probably doing something wrong; \
feel free to open an issue/discussion in our GitHub repository \
(https://github.com/elastio/bon) or ask for help in our Discord server \
(https://bon-rs.com/discord)";

quote! {
#[doc(hidden)]
#[deprecated = #deprecated_msg]
}
};

let receiver_field = self.receiver().map(|receiver| {
let ty = &receiver.without_self_keyword;
quote! {
#private_field_attrs
#receiver_field: #ty,
}
});

let must_use_message = format!(
"the builder does nothing until you call `{}()` on it to finish building",
self.finish_fn.ident
);

let allows = super::allow_warnings_on_member_types();

let mut start_fn_arg_types = self
.start_fn_args()
.map(|member| &member.base.ty.norm)
.peekable();

let start_fn_args_field = start_fn_arg_types.peek().is_some().then(|| {
quote! {
#private_field_attrs
#start_fn_args_field: (#(#start_fn_arg_types,)*),
}
});

let named_members_types = self.named_members().map(NamedMember::underlying_norm_ty);

let docs = &self.builder_type.docs;
let state_var = &self.state_var;

let custom_fields_idents = self.custom_fields().map(|field| &field.ident);
let custom_fields_types = self.custom_fields().map(|field| &field.norm_ty);

quote! {
#[must_use = #must_use_message]
#(#docs)*
#allows
#[allow(
// We use `__private` prefix for all fields intentionally to hide them
clippy::struct_field_names,

// This lint doesn't emerge until you manually expand the macro. Just
// because `bon` developers need to expand the macros a lot it makes
// sense to just silence it to avoid some noise. This lint is triggered
// by the big PhantomData type generated by the macro
clippy::type_complexity
)]
#builder_vis struct #builder_ident<
#(#generics_decl,)*
// Having the `State` trait bound on the struct declaration is important
// for future proofing. It will allow us to use this bound in the `Drop`
// implementation of the builder if we ever add one. @Veetaha already did
// some experiments with `MaybeUninit` that requires a custom drop impl,
// so this could be useful in the future.
//
// On the flip side, if we have a custom `Drop` impl, then partially moving
// the builder will be impossible. So.. it's a trade-off, and it's probably
// not a big deal to remove this bound from here if we feel like it.
#state_var: #state_mod::State = #state_mod::Empty
>
#where_clause
{
#private_field_attrs
#phantom_field: #phantom_data,

#receiver_field
#start_fn_args_field

#( #custom_fields_idents: #custom_fields_types, )*

#private_field_attrs
#named_members_field: (
#(
::core::option::Option<#named_members_types>,
)*
),
}
}
}
}
54 changes: 40 additions & 14 deletions bon-macros/src/builder/builder_gen/builder_derives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ impl BuilderGenCtx {
}
});

let clone_fields = self.custom_fields().map(|member| {
let member_ident = &member.ident;
let member_ty = &member.norm_ty;

quote! {
// The type hint here is necessary to get better error messages
// that point directly to the type that doesn't implement `Clone`
// in the input code using the span info from the type hint.
#member_ident: <#member_ty as #clone>::clone(&self.#member_ident)
}
});

let state_var = &self.state_var;

quote! {
Expand All @@ -139,6 +151,7 @@ impl BuilderGenCtx {
#phantom_field: ::core::marker::PhantomData,
#clone_receiver
#clone_start_fn_args
#( #clone_fields, )*

// We clone named members individually instead of cloning
// the entire tuple to improve error messages in case if
Expand All @@ -162,6 +175,32 @@ impl BuilderGenCtx {

let format_members = self.members.iter().filter_map(|member| {
match member {
Member::StartFn(member) => {
let member_index = &member.index;
let member_ident_str = member.base.ident.to_string();
let member_ty = &member.base.ty.norm;
Some(quote! {
output.field(
#member_ident_str,
#bon::__::derives::as_dyn_debug::<#member_ty>(
&self.#start_fn_args_field.#member_index
)
);
})
}
Member::Field(member) => {
let member_ident = &member.ident;
let member_ident_str = member_ident.to_string();
let member_ty = &member.norm_ty;
Some(quote! {
output.field(
#member_ident_str,
#bon::__::derives::as_dyn_debug::<#member_ty>(
&self.#member_ident
)
);
})
}
Member::Named(member) => {
let member_index = &member.index;
let member_ident_str = &member.name.snake_raw_str;
Expand All @@ -175,24 +214,11 @@ impl BuilderGenCtx {
}
})
}
Member::StartFnArg(member) => {
let member_index = &member.index;
let member_ident_str = member.base.ident.to_string();
let member_ty = &member.base.ty.norm;
Some(quote! {
output.field(
#member_ident_str,
#bon::__::derives::as_dyn_debug::<#member_ty>(
&self.#start_fn_args_field.#member_index
)
);
})
}

// The values for these members are computed only in the finishing
// function where the builder is consumed, and they aren't stored
// in the builder itself.
Member::FinishFnArg(_) | Member::Skipped(_) => None,
Member::FinishFn(_) | Member::Skip(_) => None,
}
});

Expand Down
21 changes: 13 additions & 8 deletions bon-macros/src/builder/builder_gen/finish_fn.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
use super::member::{Member, PositionalFnArgMember};
use super::member::{Member, PosFnMember};
use crate::util::prelude::*;

impl super::BuilderGenCtx {
fn finish_fn_member_expr(&self, member: &Member) -> TokenStream {
let member = match member {
Member::Named(member) => member,
Member::Skipped(member) => {
Member::Skip(member) => {
return member
.value
.as_ref()
.as_ref()
.map(|value| quote! { (|| #value)() })
.unwrap_or_else(|| quote! { ::core::default::Default::default() });
}
Member::StartFnArg(member) => {
Member::StartFn(member) => {
let index = &member.index;
let start_fn_args_field = &self.ident_pool.start_fn_args;

return quote! { self.#start_fn_args_field.#index };
}
Member::FinishFnArg(member) => {
return member.init_expr();
Member::FinishFn(member) => {
return member
.conversion()
.unwrap_or_else(|| member.ident.to_token_stream());
}
Member::Field(member) => {
let ident = &member.ident;
return quote! { self.#ident };
}
};

Expand Down Expand Up @@ -109,8 +114,8 @@ impl super::BuilderGenCtx {
let finish_fn_params = self
.members
.iter()
.filter_map(Member::as_finish_fn_arg)
.map(PositionalFnArgMember::fn_input_param);
.filter_map(Member::as_finish_fn)
.map(PosFnMember::fn_input_param);

let body = &self.finish_fn.body.generate(self);
let asyncness = &self.finish_fn.asyncness;
Expand Down
2 changes: 1 addition & 1 deletion bon-macros/src/builder/builder_gen/input_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ impl<'a> FnInputCtx<'a> {
clippy::too_many_arguments,

// It's fine to use many bool arguments in the function signature because
// all of the will be named at the call site
// all of them will be named at the call site when the builder is used.
clippy::fn_params_excessive_bools,
)]));

Expand Down
Loading
Loading