Skip to content
Open
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
2 changes: 2 additions & 0 deletions bon-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ default = []
# See the docs on this feature in the `bon`'s crate `Cargo.toml`
experimental-overwritable = []

experimental-build-from = []

# See the docs on this feature in the `bon`'s crate `Cargo.toml`
implied-bounds = []

Expand Down
158 changes: 158 additions & 0 deletions bon-macros/src/builder/builder_gen/build_from.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use crate::builder::builder_gen::{BuilderGenCtx, member::Member};

Check warning on line 1 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / cargo-fmt

Diff in /home/runner/work/bon/bon/bon-macros/src/builder/builder_gen/build_from.rs
use crate::util::prelude::*;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{Type, spanned::Spanned};

Check warning on line 5 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / cargo-fmt

Diff in /home/runner/work/bon/bon/bon-macros/src/builder/builder_gen/build_from.rs

pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result<TokenStream> {
let mut tokens = TokenStream::new();
let ctor_args: Vec<_> = ctx
.members
.iter()
.map(|m| {
let ident = m.orig_ident();
quote! { #ident }
})
.collect();

let base_name = ctx.finish_fn.ident.clone();

if ctx.build_from {
tokens.extend(emit_build_from_method(
false,
&base_name,
target_ty,
&ctx.members,
&ctor_args,
));
}

if ctx.build_from_clone {
tokens.extend(emit_build_from_method(
true,
&base_name,
target_ty,
&ctx.members,
&ctor_args,
)?);
}

Ok(tokens)
}

fn emit_build_from_method(
clone: bool,
base_name: &Ident,
target_ty: &Type,
members: &[Member],
ctor_args: &[TokenStream],
) -> Result<TokenStream> {
let doc = if clone {
"Fills unset builder fields from an owned value of the target type and builds it."
} else {
"Fills unset builder fields from a reference to the target type and builds it."
};

let method_name = if clone {
format_ident!("{}_from_clone", base_name)
} else {
format_ident!("{}_from", base_name)
};

let arg_type = if clone {
quote!(&#target_ty)
} else {
quote!(#target_ty)
};

let arg_pat = if clone {
quote!(mut from)
} else {
quote!(from)
};

let ctor_path = extract_ctor_ident_path(target_ty, target_ty.span())?;
let field_vars = field_vars_from_members(members, clone);

Ok(quote! {
#[inline(always)]
#[doc = #doc]
pub fn #method_name(self, #arg_pat: #arg_type) -> #target_ty {
#( #field_vars )*
#ctor_path {
#( #ctor_args, )*
}
Comment on lines +82 to +84
Copy link
Collaborator

@Veetaha Veetaha Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatelly, this approach won't work in case if the builder is generated with the #[builder] macro on top of a function. We have the body.generate() in finish_fn.rs here that generates the proper body:

let body = &self.finish_fn.body.generate(self);

It's only requirement is that members are assigned to variables of the same name in scope. Plus we need to inherit the potential async / unsafe / const modifiers and maybe handle the case of the Result from the original function

}
})
}

fn field_vars_from_members(members: &[Member], clone: bool) -> Vec<TokenStream> {
members
.iter()
.map(|member| {
let ident = member.orig_ident();
let ty = member.norm_ty();
let default_expr = quote! { ::core::default::Default::default() };

match member {
Member::Field(_) | Member::StartFn(_) => quote! {
let #ident: #ty = self.#ident;
},
Member::Named(member) => {
let index = &member.index;
if clone {
quote! {
let #ident: #ty = match self.__unsafe_private_named.#index {
Some(value) => value,
None => from.#ident.clone(),
};
}
} else {
quote! {
let #ident: #ty = match self.__unsafe_private_named.#index {
Some(value) => value,
None => from.#ident,
};
}
}
}
Member::FinishFn(_) => {
if clone {
quote! {
let #ident: #ty = from.#ident.clone();
}
} else {
quote! {
let #ident: #ty = from.#ident;
}
}
}
Member::Skip(_) => quote! {
let #ident: #ty = #default_expr;
},
}
})
.collect()
}

pub(crate) fn extract_ctor_ident_path(ty: &Type, span: Span) -> Result<TokenStream> {
use quote::quote_spanned;

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu, --locked)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly, --locked)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta, --locked)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta)

unused import: `quote::quote_spanned`

Check warning on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / cargo-miri

unused import: `quote::quote_spanned`

Check warning on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / cargo-miri (--locked)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos, --locked)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows)

unused import: `quote::quote_spanned`

Check failure on line 139 in bon-macros/src/builder/builder_gen/build_from.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows, --locked)

unused import: `quote::quote_spanned`

let path = ty.as_path_no_qself().ok_or_else(|| {
err!(
&span,
"expected a concrete type path (like `MyStruct`) for constructor"
)
})?;

let mut ident = path
.segments
.last()
.ok_or_else(|| err!(&span, "expected a named type, but found an empty path"))?
.ident
.clone();

ident.set_span(span);

Ok(quote! { #ident })
}
4 changes: 4 additions & 0 deletions bon-macros/src/builder/builder_gen/input_fn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,10 @@
state_mod: self.config.state_mod,
start_fn: self.start_fn,
finish_fn,
#[cfg(feature = "experimental-build-from")]
build_from: self.config.build_from,

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu, --locked)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly, --locked)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta, --locked)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / cargo-miri

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / cargo-miri (--locked)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos, --locked)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows)

mismatched types

Check failure on line 416 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows, --locked)

mismatched types
#[cfg(feature = "experimental-build-from")]
build_from_clone: self.config.build_from_clone,

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu, --locked)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly, --locked)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta, --locked)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / cargo-miri

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / cargo-miri (--locked)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos, --locked)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows)

mismatched types

Check failure on line 418 in bon-macros/src/builder/builder_gen/input_fn/mod.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows, --locked)

mismatched types
})
}
}
Expand Down
4 changes: 4 additions & 0 deletions bon-macros/src/builder/builder_gen/input_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@
state_mod: self.config.state_mod,
start_fn,
finish_fn,
#[cfg(feature = "experimental-build-from")]
build_from: self.config.build_from,

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu, --locked)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly, --locked)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta, --locked)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / cargo-miri

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / cargo-miri (--locked)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos, --locked)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows)

mismatched types

Check failure on line 242 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows, --locked)

mismatched types
#[cfg(feature = "experimental-build-from")]
build_from_clone: self.config.build_from_clone,

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (ubuntu, --locked)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly, --locked)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (nightly)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta, --locked)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-unstable (beta)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / cargo-miri

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / cargo-miri (--locked)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (macos, --locked)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows)

mismatched types

Check failure on line 244 in bon-macros/src/builder/builder_gen/input_struct.rs

View workflow job for this annotation

GitHub Actions / test-stable (windows, --locked)

mismatched types
})
}
}
Expand Down
15 changes: 15 additions & 0 deletions bon-macros/src/builder/builder_gen/member/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub(crate) struct MemberConfig {
/// this option to see if it's worth it.
pub(crate) overwritable: darling::util::Flag,

/// Allows the use of `build_from` and `build_from_clone` methods.
pub(crate) build_from: darling::util::Flag,

/// Disables the special handling for a member of type `Option<T>`. The
/// member no longer has the default of `None`. It also becomes a required
/// member unless a separate `#[builder(default = ...)]` attribute is
Expand Down Expand Up @@ -98,6 +101,7 @@ enum ParamName {
Into,
Name,
Overwritable,
BuildFrom,
Required,
Setters,
Skip,
Expand All @@ -115,6 +119,7 @@ impl fmt::Display for ParamName {
Self::Into => "into",
Self::Name => "name",
Self::Overwritable => "overwritable",
Self::BuildFrom => "build_from",
Self::Required => "required",
Self::Setters => "setters",
Self::Skip => "skip",
Expand Down Expand Up @@ -180,6 +185,7 @@ impl MemberConfig {
into,
name,
overwritable,
build_from,
required,
setters,
skip,
Expand All @@ -195,6 +201,7 @@ impl MemberConfig {
(into.is_present(), ParamName::Into),
(name.is_some(), ParamName::Name),
(overwritable.is_present(), ParamName::Overwritable),
(build_from.is_present(), ParamName::BuildFrom),
(required.is_present(), ParamName::Required),
(setters.is_some(), ParamName::Setters),
(skip.is_some(), ParamName::Skip),
Expand Down Expand Up @@ -227,6 +234,14 @@ impl MemberConfig {
);
}

if !cfg!(feature = "experimental-build-from") && self.build_from.is_present() {
bail!(
&self.build_from.span(),
"🔬 `build_from` attribute is experimental and requires \
\"experimental-build-from\" cargo feature to be enabled.",
);
}

if let Some(getter) = &self.getter {
self.validate_mutually_exclusive(
ParamName::Getter,
Expand Down
19 changes: 19 additions & 0 deletions bon-macros/src/builder/builder_gen/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(feature = "experimental-build-from")]
mod build_from;

mod builder_decl;
mod builder_derives;
mod finish_fn;
Expand Down Expand Up @@ -134,6 +137,21 @@ impl BuilderGenCtx {

let allows = allow_warnings_on_member_types();

let build_froms = {
#[cfg(feature = "experimental-build-from")]
{
match &self.finish_fn.output {
syn::ReturnType::Type(_, ty) => build_from::emit(self, ty)?,
syn::ReturnType::Default => quote! {},
}
}

#[cfg(not(feature = "experimental-build-from"))]
{
quote! {}
}
};

Ok(quote! {
#allows
#[automatically_derived]
Expand All @@ -145,6 +163,7 @@ impl BuilderGenCtx {
#where_clause
{
#finish_fn
#build_froms
#(#accessor_methods)*
}
})
Expand Down
17 changes: 17 additions & 0 deletions bon-macros/src/builder/builder_gen/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ pub(crate) struct BuilderGenCtx {
pub(super) state_mod: StateMod,
pub(super) start_fn: StartFn,
pub(super) finish_fn: FinishFn,
#[cfg(feature = "experimental-build-from")]
pub(super) build_from: bool,
#[cfg(feature = "experimental-build-from")]
pub(super) build_from_clone: bool,
}

pub(super) struct BuilderGenCtxParams<'a> {
Expand Down Expand Up @@ -201,6 +205,10 @@ pub(super) struct BuilderGenCtxParams<'a> {
pub(super) state_mod: ItemSigConfig,
pub(super) start_fn: StartFnParams,
pub(super) finish_fn: FinishFnParams,
#[cfg(feature = "experimental-build-from")]
pub(super) build_from: bool,
#[cfg(feature = "experimental-build-from")]
pub(super) build_from_clone: bool,
}

impl BuilderGenCtx {
Expand All @@ -219,7 +227,12 @@ impl BuilderGenCtx {
state_mod,
start_fn,
finish_fn,
..
} = params;
#[cfg(feature = "experimental-build-from")]
let build_from = params.build_from;
#[cfg(feature = "experimental-build-from")]
let build_from_clone = params.build_from_clone;

let builder_type = BuilderType {
ident: builder_type.ident,
Expand Down Expand Up @@ -370,6 +383,10 @@ impl BuilderGenCtx {
state_mod,
start_fn,
finish_fn,
#[cfg(feature = "experimental-build-from")]
build_from,
#[cfg(feature = "experimental-build-from")]
build_from_clone,
})
}
}
Expand Down
30 changes: 28 additions & 2 deletions bon-macros/src/builder/builder_gen/top_level_config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
mod on;

pub(crate) use on::OnConfig;

Check warning on line 4 in bon-macros/src/builder/builder_gen/top_level_config/mod.rs

View workflow job for this annotation

GitHub Actions / cargo-fmt

Diff in /home/runner/work/bon/bon/bon-macros/src/builder/builder_gen/top_level_config/mod.rs
use crate::parsing::{BonCratePath, ItemSigConfig, ItemSigConfigParsing, SpannedKey};
use crate::util::prelude::*;
use darling::ast::NestedMeta;
use darling::FromMeta;
use darling::ast::NestedMeta;
use syn::ItemFn;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::ItemFn;

fn parse_finish_fn(meta: &syn::Meta) -> Result<ItemSigConfig> {
ItemSigConfigParsing {
Expand Down Expand Up @@ -42,6 +42,24 @@
.parse()
}

#[cfg(feature = "experimental-build-from")]
fn parse_build_from(meta: &syn::Meta) -> Result<ItemSigConfig> {
ItemSigConfigParsing {
meta,
reject_self_mentions: Some("builder struct's impl block"),
}
.parse()
}

#[cfg(feature = "experimental-build-from")]
fn parse_build_from_clone(meta: &syn::Meta) -> Result<ItemSigConfig> {
ItemSigConfigParsing {
meta,
reject_self_mentions: Some("builder struct's impl block"),
}
.parse()
}

#[derive(Debug, FromMeta)]
pub(crate) struct TopLevelConfig {
/// Specifies whether the generated functions should be `const`.
Expand Down Expand Up @@ -75,6 +93,14 @@
/// Specifies the derives to apply to the builder.
#[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)]
pub(crate) derive: DerivesConfig,

#[cfg(feature = "experimental-build-from")]
#[darling(default, with = parse_build_from)]
pub(crate) build_from: ItemSigConfig,

#[cfg(feature = "experimental-build-from")]
#[darling(default, with = parse_build_from_clone)]
pub(crate) build_from_clone: ItemSigConfig,
}

impl TopLevelConfig {
Expand Down
Loading
Loading