From fca0fbcfe7bed2e509b485f4e4094d7aaca6de0f Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Thu, 3 Jul 2025 01:08:15 -0400 Subject: [PATCH 01/12] It appears to work --- .../src/builder/builder_gen/build_from.rs | 119 ++++++++++++++++++ .../src/builder/builder_gen/input_fn/mod.rs | 2 + .../src/builder/builder_gen/input_struct.rs | 2 + bon-macros/src/builder/builder_gen/mod.rs | 7 ++ bon-macros/src/builder/builder_gen/models.rs | 8 ++ .../builder_gen/top_level_config/mod.rs | 10 +- bon/tests/integration/builder/build_from.rs | 26 ++++ bon/tests/integration/builder/mod.rs | 1 + 8 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 bon-macros/src/builder/builder_gen/build_from.rs create mode 100644 bon/tests/integration/builder/build_from.rs diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs new file mode 100644 index 00000000..e319601a --- /dev/null +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -0,0 +1,119 @@ +use crate::builder::builder_gen::{BuilderGenCtx, member::Member}; +use crate::util::prelude::*; +use quote::quote; +use syn::Type; + +pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> TokenStream { + let mut tokens = TokenStream::new(); + + let field_vars: Vec<_> = ctx + .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; + quote! { + let #ident: #ty = match self.__unsafe_private_named.#index { + Some(value) => value, + None => from.#ident.clone(), + }; + } + } + Member::FinishFn(_) => quote! { + let #ident: #ty = from.#ident.clone(); + }, + Member::Skip(_) => quote! { + let #ident: #ty = #default_expr; + }, + } + }) + .collect(); + + let ctor_args: Vec<_> = ctx + .members + .iter() + .map(|m| { + let ident = m.orig_ident(); + quote! { #ident } + }) + .collect(); + + if ctx.build_from { + tokens.extend(emit_build_from_method( + false, + target_ty, + &field_vars, + &ctor_args, + )); + } + + if ctx.build_from_clone { + tokens.extend(emit_build_from_method( + true, + target_ty, + &field_vars, + &ctor_args, + )); + } + + tokens +} + +fn emit_build_from_method( + clone: bool, + target_ty: &Type, + field_vars: &[TokenStream], + ctor_args: &[TokenStream], +) -> 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 { + quote!(build_from_clone) + } else { + quote!(build_from) + }; + + let arg_type = if clone { + quote!(#target_ty) + } else { + quote!(&#target_ty) + }; + + let arg_pat = if clone { + quote!(mut from) + } else { + quote!(from) + }; + + // Fix: Convert `target_ty` to a path segment with no generics + let ctor_path = match target_ty { + Type::Path(type_path) => { + let ident = &type_path.path.segments.last().unwrap().ident; + quote!(#ident) + } + _ => quote!(#target_ty), // fallback for non-path types + }; + + quote! { + #[inline(always)] + #[doc = #doc] + pub fn #method_name(self, #arg_pat: #arg_type) -> #target_ty { + #( #field_vars )* + #ctor_path { + #( #ctor_args, )* + } + } + } +} diff --git a/bon-macros/src/builder/builder_gen/input_fn/mod.rs b/bon-macros/src/builder/builder_gen/input_fn/mod.rs index bac8e598..3726363a 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/mod.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/mod.rs @@ -422,6 +422,8 @@ impl<'a> FnInputCtx<'a> { state_mod: self.config.state_mod, start_fn: self.start_fn, finish_fn, + build_from: self.config.build_from, + build_from_clone: self.config.build_from_clone, }) } } diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index 69b2d1f8..7ac5ef22 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -244,6 +244,8 @@ impl StructInputCtx { state_mod: self.config.state_mod, start_fn, finish_fn, + build_from: self.config.build_from, + build_from_clone: self.config.build_from_clone, }) } } diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 016c41f7..9079cf62 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -1,3 +1,4 @@ +mod build_from; mod builder_decl; mod builder_derives; mod finish_fn; @@ -144,6 +145,11 @@ impl BuilderGenCtx { let allows = allow_warnings_on_member_types(); + let build_froms = match &self.finish_fn.output { + syn::ReturnType::Type(_, ty) => build_from::emit(self, ty), + syn::ReturnType::Default => quote! {}, + }; + Ok(quote! { #allows // Ignore dead code warnings because some setter/getter methods may @@ -158,6 +164,7 @@ impl BuilderGenCtx { #where_clause { #finish_fn + #build_froms #(#accessor_methods)* #generic_setter_methods } diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index e7e21c75..74db9c91 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -183,6 +183,8 @@ pub(crate) struct BuilderGenCtx { pub(super) state_mod: StateMod, pub(super) start_fn: StartFn, pub(super) finish_fn: FinishFn, + pub(super) build_from: bool, + pub(super) build_from_clone: bool, } pub(super) struct BuilderGenCtxParams<'a> { @@ -212,6 +214,8 @@ pub(super) struct BuilderGenCtxParams<'a> { pub(super) state_mod: ItemSigConfig, pub(super) start_fn: StartFnParams, pub(super) finish_fn: FinishFnParams, + pub(super) build_from: bool, + pub(super) build_from_clone: bool, } impl BuilderGenCtx { @@ -231,6 +235,8 @@ impl BuilderGenCtx { state_mod, start_fn, finish_fn, + build_from, + build_from_clone, } = params; let builder_type = BuilderType { @@ -385,6 +391,8 @@ impl BuilderGenCtx { state_mod, start_fn, finish_fn, + build_from, + build_from_clone, }) } } diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index 5fc29675..e80ddbae 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -6,11 +6,11 @@ pub(crate) use on::OnConfig; 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 { ItemSigConfigParsing::new(meta, Some("builder struct's impl block")).parse() @@ -65,6 +65,12 @@ pub(crate) struct TopLevelConfig { /// Specifies configuration for generic parameter conversion methods. #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] pub(crate) generics: Option>, + + #[darling(default)] + pub(crate) build_from: bool, + + #[darling(default)] + pub(crate) build_from_clone: bool, } impl TopLevelConfig { diff --git a/bon/tests/integration/builder/build_from.rs b/bon/tests/integration/builder/build_from.rs new file mode 100644 index 00000000..7aaa2a1e --- /dev/null +++ b/bon/tests/integration/builder/build_from.rs @@ -0,0 +1,26 @@ +use crate::prelude::*; + +#[derive(Builder, Clone)] +#[builder(build_from, build_from_clone)] +struct User { + name: String, + age: u8, +} + +#[test] +fn test_build_from_works() { + let jon = User::builder().name("Jon".into()).age(25).build(); + let alice = User::builder().name("Alice".into()).build_from(&jon); + assert_eq!(alice.age, jon.age); + assert_eq!(alice.name, "Alice"); +} + +#[test] +fn test_build_from_clone_works() { + let jon = User::builder().name("Jon".into()).age(25).build(); + let alice = User::builder() + .name("Alice".into()) + .build_from_clone(jon.clone()); + assert_eq!(alice.age, jon.age); + assert_eq!(alice.name, "Alice"); +} diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index 7a749797..95f33ea1 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -16,6 +16,7 @@ mod attr_setters; mod attr_skip; mod attr_top_level_start_fn; mod attr_with; +mod build_from; mod cfgs; mod generics; #[cfg(feature = "experimental-generics-setters")] From 94eda0ed7e4199636b405a020841f416e85c4d22 Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Thu, 3 Jul 2025 01:20:50 -0400 Subject: [PATCH 02/12] Cleaning up comments --- bon-macros/src/builder/builder_gen/build_from.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index e319601a..832ded45 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -97,13 +97,13 @@ fn emit_build_from_method( quote!(from) }; - // Fix: Convert `target_ty` to a path segment with no generics + // Convert `target_ty` to a path segment with no generics let ctor_path = match target_ty { Type::Path(type_path) => { let ident = &type_path.path.segments.last().unwrap().ident; quote!(#ident) } - _ => quote!(#target_ty), // fallback for non-path types + _ => quote!(#target_ty), }; quote! { From 3a3d32661b0b6182f1972df513ad582fba2b0984 Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Thu, 3 Jul 2025 01:24:15 -0400 Subject: [PATCH 03/12] Fixing clippy errors --- bon-macros/src/builder/builder_gen/build_from.rs | 11 +++++------ .../src/builder/builder_gen/top_level_config/on.rs | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index 832ded45..339cb03d 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -98,12 +98,11 @@ fn emit_build_from_method( }; // Convert `target_ty` to a path segment with no generics - let ctor_path = match target_ty { - Type::Path(type_path) => { - let ident = &type_path.path.segments.last().unwrap().ident; - quote!(#ident) - } - _ => quote!(#target_ty), + let ctor_path = if let Type::Path(type_path) = target_ty { + let ident = &type_path.path.segments.last().unwrap().ident; + quote!(#ident) + } else { + quote!(#target_ty) }; quote! { diff --git a/bon-macros/src/builder/builder_gen/top_level_config/on.rs b/bon-macros/src/builder/builder_gen/top_level_config/on.rs index 0289e2a0..3897c373 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/on.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/on.rs @@ -1,6 +1,6 @@ use crate::util::prelude::*; -use darling::util::Flag; use darling::FromMeta; +use darling::util::Flag; use syn::parse::Parse; use syn::spanned::Spanned; use syn::visit::Visit; From 3e216fa57d6e5a4b2451d19827b8b25b307f4c74 Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Thu, 3 Jul 2025 02:30:23 -0400 Subject: [PATCH 04/12] Added error handling. --- .../src/builder/builder_gen/build_from.rs | 43 +++++++++++++------ bon-macros/src/builder/builder_gen/mod.rs | 2 +- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index 339cb03d..7417273e 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -1,9 +1,10 @@ use crate::builder::builder_gen::{BuilderGenCtx, member::Member}; use crate::util::prelude::*; +use proc_macro2::Span; use quote::quote; -use syn::Type; +use syn::{Type, spanned::Spanned}; -pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> TokenStream { +pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result { let mut tokens = TokenStream::new(); let field_vars: Vec<_> = ctx @@ -61,10 +62,10 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> TokenStream { target_ty, &field_vars, &ctor_args, - )); + )?); } - tokens + Ok(tokens) } fn emit_build_from_method( @@ -72,7 +73,7 @@ fn emit_build_from_method( target_ty: &Type, field_vars: &[TokenStream], ctor_args: &[TokenStream], -) -> TokenStream { +) -> Result { let doc = if clone { "Fills unset builder fields from an owned value of the target type and builds it." } else { @@ -97,15 +98,9 @@ fn emit_build_from_method( quote!(from) }; - // Convert `target_ty` to a path segment with no generics - let ctor_path = if let Type::Path(type_path) = target_ty { - let ident = &type_path.path.segments.last().unwrap().ident; - quote!(#ident) - } else { - quote!(#target_ty) - }; + let ctor_path = extract_ctor_ident_path(target_ty, target_ty.span())?; - quote! { + Ok(quote! { #[inline(always)] #[doc = #doc] pub fn #method_name(self, #arg_pat: #arg_type) -> #target_ty { @@ -114,5 +109,25 @@ fn emit_build_from_method( #( #ctor_args, )* } } - } + }) +} + +pub(crate) fn extract_ctor_ident_path(ty: &Type, span: Span) -> Result { + use 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 ident = path + .segments + .last() + .ok_or_else(|| err!(&span, "expected a named type, but found an empty path"))? + .ident + .clone(); + + Ok(quote_spanned! { span => #ident }) } diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 9079cf62..0504b637 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -146,7 +146,7 @@ impl BuilderGenCtx { let allows = allow_warnings_on_member_types(); let build_froms = match &self.finish_fn.output { - syn::ReturnType::Type(_, ty) => build_from::emit(self, ty), + syn::ReturnType::Type(_, ty) => build_from::emit(self, ty)?, syn::ReturnType::Default => quote! {}, }; From 26b5c9f3f6b1734beb463bf92904017834492bc8 Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Thu, 3 Jul 2025 02:33:57 -0400 Subject: [PATCH 05/12] Adding docs. --- bon/src/__/ide.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/bon/src/__/ide.rs b/bon/src/__/ide.rs index 404b40a7..1abb5cad 100644 --- a/bon/src/__/ide.rs +++ b/bon/src/__/ide.rs @@ -105,6 +105,40 @@ pub mod builder_top_level { pub use core::future::IntoFuture; } + /// See the docs at + pub const build_from: Option = None; + + /// See the docs at + pub mod build_from { + use super::*; + + /// See the docs at + pub const name: Identifier = Identifier; + + /// See the docs at + pub const vis: VisibilityString = VisibilityString; + + /// See the docs at + pub const doc: DocComments = DocComments; + } + + /// See the docs at + pub const build_from_clone: Option = None; + + /// See the docs at + pub mod build_from_clone { + use super::*; + + /// See the docs at + pub const name: Identifier = Identifier; + + /// See the docs at + pub const vis: VisibilityString = VisibilityString; + + /// See the docs at + pub const doc: DocComments = DocComments; + } + /// The real name of this parameter is `crate` (without the underscore). /// It's hinted with an underscore due to the limitations of the current /// completions limitation. This will be fixed in the future. From cf2fa741fa488f5b8b77cbde1e1971f1021011b8 Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Tue, 8 Jul 2025 13:48:36 -0400 Subject: [PATCH 06/12] Addressing PR comments - Changed logic for build_from and build_from_clone, so that cloning only happens in the latter and the former just takes ownership. - Added support for function methods. - Added tests to cover more builder use cases --- bon-macros/Cargo.toml | 2 + .../src/builder/builder_gen/build_from.rs | 103 +++++++++++------- .../src/builder/builder_gen/input_fn/mod.rs | 2 + .../src/builder/builder_gen/input_struct.rs | 2 + .../builder/builder_gen/member/config/mod.rs | 23 ++++ bon-macros/src/builder/builder_gen/mod.rs | 18 ++- bon-macros/src/builder/builder_gen/models.rs | 13 ++- .../builder_gen/top_level_config/mod.rs | 2 + .../builder_gen/top_level_config/on.rs | 12 ++ bon/Cargo.toml | 2 + bon/tests/integration/builder/build_from.rs | 45 ++++++-- bon/tests/integration/builder/mod.rs | 1 + 12 files changed, 171 insertions(+), 54 deletions(-) diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index 7f64d60f..2602bb47 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -71,6 +71,8 @@ std = [] # 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` experimental-generics-setters = [] diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index 7417273e..d9bcd815 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -1,43 +1,11 @@ use crate::builder::builder_gen::{BuilderGenCtx, member::Member}; use crate::util::prelude::*; -use proc_macro2::Span; +use proc_macro2::{Ident, Span}; use quote::quote; use syn::{Type, spanned::Spanned}; pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result { let mut tokens = TokenStream::new(); - - let field_vars: Vec<_> = ctx - .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; - quote! { - let #ident: #ty = match self.__unsafe_private_named.#index { - Some(value) => value, - None => from.#ident.clone(), - }; - } - } - Member::FinishFn(_) => quote! { - let #ident: #ty = from.#ident.clone(); - }, - Member::Skip(_) => quote! { - let #ident: #ty = #default_expr; - }, - } - }) - .collect(); - let ctor_args: Vec<_> = ctx .members .iter() @@ -47,11 +15,14 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result }) .collect(); + let base_name = ctx.finish_fn.ident.clone(); + if ctx.build_from { tokens.extend(emit_build_from_method( false, + &base_name, target_ty, - &field_vars, + &ctx.members, &ctor_args, )); } @@ -59,8 +30,9 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result if ctx.build_from_clone { tokens.extend(emit_build_from_method( true, + &base_name, target_ty, - &field_vars, + &ctx.members, &ctor_args, )?); } @@ -70,8 +42,9 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result fn emit_build_from_method( clone: bool, + base_name: &Ident, target_ty: &Type, - field_vars: &[TokenStream], + members: &[Member], ctor_args: &[TokenStream], ) -> Result { let doc = if clone { @@ -81,15 +54,15 @@ fn emit_build_from_method( }; let method_name = if clone { - quote!(build_from_clone) + format_ident!("{}_from_clone", base_name) } else { - quote!(build_from) + format_ident!("{}_from", base_name) }; let arg_type = if clone { - quote!(#target_ty) - } else { quote!(&#target_ty) + } else { + quote!(#target_ty) }; let arg_pat = if clone { @@ -99,6 +72,7 @@ fn emit_build_from_method( }; 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)] @@ -112,6 +86,55 @@ fn emit_build_from_method( }) } +fn field_vars_from_members(members: &[Member], clone: bool) -> Vec { + 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 { use quote::quote_spanned; diff --git a/bon-macros/src/builder/builder_gen/input_fn/mod.rs b/bon-macros/src/builder/builder_gen/input_fn/mod.rs index 3726363a..eef69fc3 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/mod.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/mod.rs @@ -422,7 +422,9 @@ impl<'a> FnInputCtx<'a> { state_mod: self.config.state_mod, start_fn: self.start_fn, finish_fn, + #[cfg(feature = "experimental-build-from")] build_from: self.config.build_from, + #[cfg(feature = "experimental-build-from")] build_from_clone: self.config.build_from_clone, }) } diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index 7ac5ef22..61053e42 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -244,7 +244,9 @@ impl StructInputCtx { state_mod: self.config.state_mod, start_fn, finish_fn, + #[cfg(feature = "experimental-build-from")] build_from: self.config.build_from, + #[cfg(feature = "experimental-build-from")] build_from_clone: self.config.build_from_clone, }) } diff --git a/bon-macros/src/builder/builder_gen/member/config/mod.rs b/bon-macros/src/builder/builder_gen/member/config/mod.rs index 2b68bb1a..f0dae39e 100644 --- a/bon-macros/src/builder/builder_gen/member/config/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/config/mod.rs @@ -65,6 +65,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`. The /// member no longer has the default of `None`. It also becomes a required /// member unless a separate `#[builder(default = ...)]` attribute is @@ -102,6 +105,7 @@ enum ParamName { Into, Name, Overwritable, + BuildFrom, Required, Setters, Skip, @@ -119,6 +123,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", @@ -184,6 +189,7 @@ impl MemberConfig { into, name, overwritable, + build_from, required, setters, skip, @@ -199,6 +205,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), @@ -231,6 +238,22 @@ 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, + getter.key.span(), + &[ParamName::Overwritable], + )?; + } + if self.start_fn.is_present() { self.validate_mutually_allowed( ParamName::StartFn, diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 0504b637..ff02ee99 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -1,4 +1,6 @@ +#[cfg(feature = "experimental-build-from")] mod build_from; + mod builder_decl; mod builder_derives; mod finish_fn; @@ -145,9 +147,19 @@ impl BuilderGenCtx { let allows = allow_warnings_on_member_types(); - let build_froms = match &self.finish_fn.output { - syn::ReturnType::Type(_, ty) => build_from::emit(self, ty)?, - syn::ReturnType::Default => quote! {}, + 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! { diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index 74db9c91..edb25a85 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -183,7 +183,9 @@ 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, } @@ -214,7 +216,9 @@ 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, } @@ -235,9 +239,12 @@ impl BuilderGenCtx { state_mod, start_fn, finish_fn, - build_from, - build_from_clone, + .. } = 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, @@ -391,7 +398,9 @@ impl BuilderGenCtx { state_mod, start_fn, finish_fn, + #[cfg(feature = "experimental-build-from")] build_from, + #[cfg(feature = "experimental-build-from")] build_from_clone, }) } diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index e80ddbae..246bbdc3 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -66,9 +66,11 @@ pub(crate) struct TopLevelConfig { #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] pub(crate) generics: Option>, + #[cfg(feature = "experimental-build-from")] #[darling(default)] pub(crate) build_from: bool, + #[cfg(feature = "experimental-build-from")] #[darling(default)] pub(crate) build_from_clone: bool, } diff --git a/bon-macros/src/builder/builder_gen/top_level_config/on.rs b/bon-macros/src/builder/builder_gen/top_level_config/on.rs index 3897c373..00db4957 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/on.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/on.rs @@ -10,6 +10,8 @@ pub(crate) struct OnConfig { pub(crate) type_pattern: syn::Type, pub(crate) into: Flag, pub(crate) overwritable: Flag, + #[allow(dead_code)] + pub(crate) build_from: Flag, pub(crate) required: Flag, pub(crate) setters: OnSettersConfig, } @@ -42,6 +44,7 @@ impl Parse for OnConfig { struct Parsed { into: Flag, overwritable: Flag, + build_from: Flag, required: Flag, #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] @@ -73,6 +76,14 @@ impl Parse for OnConfig { )); } + if !cfg!(feature = "experimental-build-from") && parsed.build_from.is_present() { + return Err(syn::Error::new( + parsed.build_from.span(), + "🔬 `build_from` attribute is experimental and requires \ + \"experimental-build-from\" cargo feature to be enabled.", + )); + } + struct FindAttr { attr: Option, } @@ -107,6 +118,7 @@ impl Parse for OnConfig { type_pattern, into: parsed.into, overwritable: parsed.overwritable, + build_from: parsed.build_from, required: parsed.required, setters: parsed.setters, }) diff --git a/bon/Cargo.toml b/bon/Cargo.toml index 288fc736..7fbdd9e4 100644 --- a/bon/Cargo.toml +++ b/bon/Cargo.toml @@ -92,6 +92,8 @@ experimental-overwritable = ["bon-macros/experimental-overwritable"] # generate methods for converting generic parameters on builders. experimental-generics-setters = ["bon-macros/experimental-generics-setters"] +experimental-build-from = ["bon-macros/experimental-build-from"] + # Legacy experimental attribute. It's left here for backwards compatibility, # and it will be removed in the next major release. # diff --git a/bon/tests/integration/builder/build_from.rs b/bon/tests/integration/builder/build_from.rs index 7aaa2a1e..76f1d5b1 100644 --- a/bon/tests/integration/builder/build_from.rs +++ b/bon/tests/integration/builder/build_from.rs @@ -2,25 +2,52 @@ use crate::prelude::*; #[derive(Builder, Clone)] #[builder(build_from, build_from_clone)] -struct User { +struct Sut { name: String, age: u8, } #[test] fn test_build_from_works() { - let jon = User::builder().name("Jon".into()).age(25).build(); - let alice = User::builder().name("Alice".into()).build_from(&jon); - assert_eq!(alice.age, jon.age); + let jon = Sut::builder().name("Jon".into()).age(25).build(); + let alice = Sut::builder().name("Alice".into()).build_from(jon); + assert_eq!(alice.age, 25); assert_eq!(alice.name, "Alice"); } #[test] fn test_build_from_clone_works() { - let jon = User::builder().name("Jon".into()).age(25).build(); - let alice = User::builder() - .name("Alice".into()) - .build_from_clone(jon.clone()); - assert_eq!(alice.age, jon.age); + let jon = Sut::builder().name("Jon".into()).age(25).build(); + let alice = Sut::builder().name("Alice".into()).build_from_clone(&jon); + assert_eq!(alice.age, 25); + assert_eq!(alice.name, "Alice"); +} + +#[builder(build_from, build_from_clone)] +fn create_user(name: String, age: u8) -> Sut { + Sut { name, age } +} + +#[test] +fn test_function_build_from_works() { + let jon = create_user().name("Jon".into()).age(25).call(); + let alice = create_user().name("Alice".into()).call_from(jon); + assert_eq!(alice.age, 25); + assert_eq!(alice.name, "Alice"); +} + +#[bon] +impl Sut { + #[builder(build_from, build_from_clone)] + fn from_parts(name: String, age: u8) -> Self { + Self { name, age } + } +} + +#[test] +fn test_method_build_from_clone_works() { + let jon = Sut::from_parts().name("Jon".into()).age(25).call(); + let alice = Sut::from_parts().name("Alice".into()).call_from_clone(&jon); + assert_eq!(alice.age, 25); assert_eq!(alice.name, "Alice"); } diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index 95f33ea1..158211b8 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -16,6 +16,7 @@ mod attr_setters; mod attr_skip; mod attr_top_level_start_fn; mod attr_with; +#[cfg(feature = "experimental-build-from")] mod build_from; mod cfgs; mod generics; From 2c4e6b9c6d7b23dcb20237d7ccc536e59eea121c Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Fri, 11 Jul 2025 17:52:57 -0400 Subject: [PATCH 07/12] Started ItemSigConfig change, fixed span issue. --- .../src/builder/builder_gen/build_from.rs | 6 +++-- .../builder_gen/top_level_config/mod.rs | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index d9bcd815..a28060f4 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -145,12 +145,14 @@ pub(crate) fn extract_ctor_ident_path(ty: &Type, span: Span) -> Result #ident }) + ident.set_span(span); + + Ok(quote! { #ident }) } diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index 246bbdc3..e571691c 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -28,6 +28,24 @@ fn parse_start_fn(meta: &syn::Meta) -> Result { ItemSigConfigParsing::new(meta, None).parse() } +#[cfg(feature = "experimental-build-from")] +fn parse_build_from(meta: &syn::Meta) -> Result { + 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 { + 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`. @@ -67,12 +85,12 @@ pub(crate) struct TopLevelConfig { pub(crate) generics: Option>, #[cfg(feature = "experimental-build-from")] - #[darling(default)] - pub(crate) build_from: bool, + #[darling(default, with = parse_build_from)] + pub(crate) build_from: ItemSigConfig, #[cfg(feature = "experimental-build-from")] - #[darling(default)] - pub(crate) build_from_clone: bool, + #[darling(default, with = parse_build_from_clone)] + pub(crate) build_from_clone: ItemSigConfig, } impl TopLevelConfig { From 32c7bec47c119fbbf18bd6428b1ef9ccc35f0aec Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Sun, 14 Jun 2026 19:22:00 -0400 Subject: [PATCH 08/12] Removing duplicate code and fixing documentation. --- bon-macros/src/builder/builder_gen/build_from.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index a28060f4..b07d0b41 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -48,9 +48,9 @@ fn emit_build_from_method( ctor_args: &[TokenStream], ) -> Result { 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." + } else { + "Fills unset builder fields from an owned value of the target type and builds it." }; let method_name = if clone { From f24c7f67bc343bdaab50d6986bec97c3b44ead0a Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Sun, 14 Jun 2026 20:20:16 -0400 Subject: [PATCH 09/12] Passed clippy checks. --- .../src/builder/builder_gen/build_from.rs | 27 +++++++++++-------- bon-macros/src/builder/builder_gen/mod.rs | 11 +++++--- bon-macros/src/builder/builder_gen/models.rs | 8 +++--- .../builder_gen/top_level_config/mod.rs | 20 ++++++++------ 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index b07d0b41..669eed02 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -2,7 +2,7 @@ use crate::builder::builder_gen::{BuilderGenCtx, member::Member}; use crate::util::prelude::*; use proc_macro2::{Ident, Span}; use quote::quote; -use syn::{Type, spanned::Spanned}; +use syn::{Type, ext::IdentExt, spanned::Spanned}; pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result { let mut tokens = TokenStream::new(); @@ -17,23 +17,25 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result let base_name = ctx.finish_fn.ident.clone(); - if ctx.build_from { + if ctx.build_from.is_some() { tokens.extend(emit_build_from_method( false, &base_name, target_ty, &ctx.members, &ctor_args, - )); + ctx.build_from.as_ref(), + )?); } - if ctx.build_from_clone { + if ctx.build_from_clone.is_some() { tokens.extend(emit_build_from_method( true, &base_name, target_ty, &ctx.members, &ctor_args, + ctx.build_from_clone.as_ref(), )?); } @@ -46,6 +48,7 @@ fn emit_build_from_method( target_ty: &Type, members: &[Member], ctor_args: &[TokenStream], + config: Option<&crate::parsing::ItemSigConfig>, ) -> Result { let doc = if clone { "Fills unset builder fields from a reference to the target type and builds it." @@ -53,11 +56,15 @@ fn emit_build_from_method( "Fills unset builder fields from an owned value of the target type and builds it." }; - let method_name = if clone { - format_ident!("{}_from_clone", base_name) - } else { - format_ident!("{}_from", base_name) - }; + let method_name: Ident = config + .and_then(|cfg| cfg.name.as_ref().map(|spanned_key| spanned_key.unraw())) + .unwrap_or_else(|| { + if clone { + format_ident!("{}_from_clone", base_name) + } else { + format_ident!("{}_from", base_name) + } + }); let arg_type = if clone { quote!(&#target_ty) @@ -136,8 +143,6 @@ fn field_vars_from_members(members: &[Member], clone: bool) -> Vec } pub(crate) fn extract_ctor_ident_path(ty: &Type, span: Span) -> Result { - use quote::quote_spanned; - let path = ty.as_path_no_qself().ok_or_else(|| { err!( &span, diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index ff02ee99..08c08694 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -150,12 +150,15 @@ impl BuilderGenCtx { 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! {}, + if self.build_from.is_some() || self.build_from_clone.is_some() { + match &self.finish_fn.output { + syn::ReturnType::Type(_, ty) => build_from::emit(self, ty)?, + syn::ReturnType::Default => quote! {}, + } + } else { + quote! {} } } - #[cfg(not(feature = "experimental-build-from"))] { quote! {} diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index edb25a85..370b690b 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -184,9 +184,9 @@ pub(crate) struct BuilderGenCtx { pub(super) start_fn: StartFn, pub(super) finish_fn: FinishFn, #[cfg(feature = "experimental-build-from")] - pub(super) build_from: bool, + pub(super) build_from: Option, #[cfg(feature = "experimental-build-from")] - pub(super) build_from_clone: bool, + pub(super) build_from_clone: Option, } pub(super) struct BuilderGenCtxParams<'a> { @@ -217,9 +217,9 @@ pub(super) struct BuilderGenCtxParams<'a> { pub(super) start_fn: StartFnParams, pub(super) finish_fn: FinishFnParams, #[cfg(feature = "experimental-build-from")] - pub(super) build_from: bool, + pub(super) build_from: Option, #[cfg(feature = "experimental-build-from")] - pub(super) build_from_clone: bool, + pub(super) build_from_clone: Option, } impl BuilderGenCtx { diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index e571691c..648dabb1 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -29,21 +29,25 @@ fn parse_start_fn(meta: &syn::Meta) -> Result { } #[cfg(feature = "experimental-build-from")] -fn parse_build_from(meta: &syn::Meta) -> Result { - ItemSigConfigParsing { +fn parse_build_from(meta: &syn::Meta) -> Result> { + let config = ItemSigConfigParsing { meta, reject_self_mentions: Some("builder struct's impl block"), } - .parse() + .parse()?; + + Ok(Some(config)) } #[cfg(feature = "experimental-build-from")] -fn parse_build_from_clone(meta: &syn::Meta) -> Result { - ItemSigConfigParsing { +fn parse_build_from_clone(meta: &syn::Meta) -> Result> { + let config = ItemSigConfigParsing { meta, reject_self_mentions: Some("builder struct's impl block"), } - .parse() + .parse()?; + + Ok(Some(config)) } #[derive(Debug, FromMeta)] @@ -86,11 +90,11 @@ pub(crate) struct TopLevelConfig { #[cfg(feature = "experimental-build-from")] #[darling(default, with = parse_build_from)] - pub(crate) build_from: ItemSigConfig, + pub(crate) build_from: Option, #[cfg(feature = "experimental-build-from")] #[darling(default, with = parse_build_from_clone)] - pub(crate) build_from_clone: ItemSigConfig, + pub(crate) build_from_clone: Option, } impl TopLevelConfig { From 191d8c1c604a9c04c78d1498ed5b20f51705328e Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Sun, 14 Jun 2026 22:25:55 -0400 Subject: [PATCH 10/12] Clippy passes --- .../builder/builder_gen/member/config/mod.rs | 8 ----- bon-macros/src/builder/builder_gen/models.rs | 31 ++++++++++++++--- .../builder_gen/top_level_config/mod.rs | 33 ++++--------------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/member/config/mod.rs b/bon-macros/src/builder/builder_gen/member/config/mod.rs index f0dae39e..1c487e8d 100644 --- a/bon-macros/src/builder/builder_gen/member/config/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/config/mod.rs @@ -246,14 +246,6 @@ impl MemberConfig { ); } - if let Some(getter) = &self.getter { - self.validate_mutually_exclusive( - ParamName::Getter, - getter.key.span(), - &[ParamName::Overwritable], - )?; - } - if self.start_fn.is_present() { self.validate_mutually_allowed( ParamName::StartFn, diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index 370b690b..1f692979 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -5,6 +5,12 @@ use crate::parsing::{BonCratePath, ItemSigConfig, SpannedKey}; use crate::util::prelude::*; use std::borrow::Cow; +#[cfg(feature = "experimental-build-from")] +use darling::util::Override; + +#[cfg(feature = "experimental-build-from")] +use crate::parsing::ItemSigConfigParsing; + pub(super) trait FinishFnBody { /// Generate the `finish` function body from the ready-made variables. /// The generated function body may assume that there are variables @@ -217,9 +223,9 @@ pub(super) struct BuilderGenCtxParams<'a> { pub(super) start_fn: StartFnParams, pub(super) finish_fn: FinishFnParams, #[cfg(feature = "experimental-build-from")] - pub(super) build_from: Option, + pub(super) build_from: Option>, #[cfg(feature = "experimental-build-from")] - pub(super) build_from_clone: Option, + pub(super) build_from_clone: Option>, } impl BuilderGenCtx { @@ -239,12 +245,27 @@ impl BuilderGenCtx { state_mod, start_fn, finish_fn, - .. + #[cfg(feature = "experimental-build-from")] + build_from, + #[cfg(feature = "experimental-build-from")] + build_from_clone, } = params; + #[cfg(feature = "experimental-build-from")] - let build_from = params.build_from; + let build_from = build_from + .map(|wrapped_override| match wrapped_override { + Override::Inherit => Ok(ItemSigConfig::default()), + Override::Explicit(meta) => ItemSigConfigParsing::new(&meta, None).parse(), + }) + .transpose()?; + #[cfg(feature = "experimental-build-from")] - let build_from_clone = params.build_from_clone; + let build_from_clone = build_from_clone + .map(|wrapped_override| match wrapped_override { + Override::Inherit => Ok(ItemSigConfig::default()), + Override::Explicit(meta) => ItemSigConfigParsing::new(&meta, None).parse(), + }) + .transpose()?; let builder_type = BuilderType { ident: builder_type.ident, diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index 648dabb1..296980eb 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -12,6 +12,9 @@ use syn::ItemFn; use syn::parse::Parser; use syn::punctuated::Punctuated; +#[cfg(feature = "experimental-build-from")] +use darling::util::Override; + fn parse_finish_fn(meta: &syn::Meta) -> Result { ItemSigConfigParsing::new(meta, Some("builder struct's impl block")).parse() } @@ -28,28 +31,6 @@ fn parse_start_fn(meta: &syn::Meta) -> Result { ItemSigConfigParsing::new(meta, None).parse() } -#[cfg(feature = "experimental-build-from")] -fn parse_build_from(meta: &syn::Meta) -> Result> { - let config = ItemSigConfigParsing { - meta, - reject_self_mentions: Some("builder struct's impl block"), - } - .parse()?; - - Ok(Some(config)) -} - -#[cfg(feature = "experimental-build-from")] -fn parse_build_from_clone(meta: &syn::Meta) -> Result> { - let config = ItemSigConfigParsing { - meta, - reject_self_mentions: Some("builder struct's impl block"), - } - .parse()?; - - Ok(Some(config)) -} - #[derive(Debug, FromMeta)] pub(crate) struct TopLevelConfig { /// Specifies whether the generated functions should be `const`. @@ -89,12 +70,12 @@ pub(crate) struct TopLevelConfig { pub(crate) generics: Option>, #[cfg(feature = "experimental-build-from")] - #[darling(default, with = parse_build_from)] - pub(crate) build_from: Option, + #[darling(default)] + pub(crate) build_from: Option>, #[cfg(feature = "experimental-build-from")] - #[darling(default, with = parse_build_from_clone)] - pub(crate) build_from_clone: Option, + #[darling(default)] + pub(crate) build_from_clone: Option>, } impl TopLevelConfig { From f3e269161bc3686adbda8cd019bf68d84b49320e Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Sun, 14 Jun 2026 23:00:44 -0400 Subject: [PATCH 11/12] Updated test pass --- .../src/builder/builder_gen/build_from.rs | 27 ++----- bon/tests/integration/builder/build_from.rs | 77 +++++++++++++++++++ 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/build_from.rs b/bon-macros/src/builder/builder_gen/build_from.rs index 669eed02..27ded1dd 100644 --- a/bon-macros/src/builder/builder_gen/build_from.rs +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -14,9 +14,7 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result quote! { #ident } }) .collect(); - let base_name = ctx.finish_fn.ident.clone(); - if ctx.build_from.is_some() { tokens.extend(emit_build_from_method( false, @@ -27,7 +25,6 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result ctx.build_from.as_ref(), )?); } - if ctx.build_from_clone.is_some() { tokens.extend(emit_build_from_method( true, @@ -38,7 +35,6 @@ pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result ctx.build_from_clone.as_ref(), )?); } - Ok(tokens) } @@ -55,7 +51,6 @@ fn emit_build_from_method( } else { "Fills unset builder fields from an owned value of the target type and builds it." }; - let method_name: Ident = config .and_then(|cfg| cfg.name.as_ref().map(|spanned_key| spanned_key.unraw())) .unwrap_or_else(|| { @@ -65,22 +60,18 @@ fn emit_build_from_method( 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] @@ -100,7 +91,6 @@ fn field_vars_from_members(members: &[Member], clone: bool) -> Vec 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; @@ -149,15 +139,10 @@ pub(crate) fn extract_ctor_ident_path(ty: &Type, span: Span) -> Result { + pub(crate) value: T, + pub(crate) flag: bool, + } + } + #[derive(Builder)] + #[builder(build_from)] + struct Source { + value: String, + flag: bool, + } + let src = Source::builder() + .value("Veetah".to_string()) + .flag(true) + .build(); + + let actual = models::Sut:: { + value: src.value, + flag: src.flag, + }; + assert_eq!(actual.value, "Veetah"); + } +} + +#[test] +fn test_build_from_custom_vis_and_docs() { + { + pub(crate) mod external { + use crate::prelude::*; + + #[derive(Builder, Clone)] + #[builder(build_from)] + pub(crate) struct Source { + pub(crate) id: u32, + } + } + let base = external::Source::builder().id(99).build(); + let actual = external::Source::builder().build_from(base); + assert_eq!(actual.id, 99); + } +} + +#[test] +fn test_build_from_nested_path_and_generics_enforcement() { + { + pub(crate) mod complex_namespace { + pub(crate) mod deeply_nested { + pub(crate) struct TargetSut { + pub(crate) data: String, + pub(crate) extra: T, + } + } + } + #[builder(build_from)] + fn create_complex_target( + data: String, + extra: u32, + ) -> complex_namespace::deeply_nested::TargetSut { + complex_namespace::deeply_nested::TargetSut { data, extra } + } + let base = complex_namespace::deeply_nested::TargetSut { + data: "BaseData".to_string(), + extra: 100u32, + }; + let actual = create_complex_target() + .data("Veetah".to_string()) + .call_from(base); + assert_eq!(actual.data, "Veetah"); + assert_eq!(actual.extra, 100); + } +} From 3c0c48d708e93a7b8bde43dd65b585c9a4f6af15 Mon Sep 17 00:00:00 2001 From: Bernard Igiri Date: Mon, 15 Jun 2026 20:33:19 -0400 Subject: [PATCH 12/12] Adding trait for item_sig.rs. --- bon-macros/src/parsing/item_sig.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bon-macros/src/parsing/item_sig.rs b/bon-macros/src/parsing/item_sig.rs index 6a0f47f1..ea9c9e0c 100644 --- a/bon-macros/src/parsing/item_sig.rs +++ b/bon-macros/src/parsing/item_sig.rs @@ -96,3 +96,9 @@ impl<'a> ItemSigConfigParsing<'a> { Ok(config) } } + +impl FromMeta for ItemSigConfig { + fn from_meta(meta: &syn::Meta) -> darling::Result { + ItemSigConfigParsing::new(meta, None).parse() + } +}