diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index 1a1cd37e..49caa093 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -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 = [] 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..a28060f4 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/build_from.rs @@ -0,0 +1,158 @@ +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}; + +pub(super) fn emit(ctx: &BuilderGenCtx, target_ty: &Type) -> Result { + 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 { + 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, )* + } + } + }) +} + +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; + + 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 }) +} 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 9547cf48..ded713d2 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/mod.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/mod.rs @@ -412,6 +412,10 @@ 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 caeb3037..a8e03a24 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -238,6 +238,10 @@ 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 a345f6fd..67b22bf7 100644 --- a/bon-macros/src/builder/builder_gen/member/config/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/config/mod.rs @@ -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`. The /// member no longer has the default of `None`. It also becomes a required /// member unless a separate `#[builder(default = ...)]` attribute is @@ -98,6 +101,7 @@ enum ParamName { Into, Name, Overwritable, + BuildFrom, Required, Setters, Skip, @@ -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", @@ -180,6 +185,7 @@ impl MemberConfig { into, name, overwritable, + build_from, required, setters, skip, @@ -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), @@ -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, diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index d2fbba5f..3461b686 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "experimental-build-from")] +mod build_from; + mod builder_decl; mod builder_derives; mod finish_fn; @@ -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] @@ -145,6 +163,7 @@ impl BuilderGenCtx { #where_clause { #finish_fn + #build_froms #(#accessor_methods)* } }) diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs index 4462b3d2..34b0024c 100644 --- a/bon-macros/src/builder/builder_gen/models.rs +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -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> { @@ -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 { @@ -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, @@ -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, }) } } 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 a917f522..4404d6aa 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 @@ -4,11 +4,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 { @@ -42,6 +42,24 @@ fn parse_start_fn(meta: &syn::Meta) -> Result { .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`. @@ -75,6 +93,14 @@ pub(crate) struct TopLevelConfig { /// 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 { 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 5ae1852a..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 @@ -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; @@ -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,13 +76,21 @@ 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, } impl Visit<'_> for FindAttr { fn visit_attribute(&mut self, attr: &'_ syn::Attribute) { - self.attr.get_or_insert(attr.span()); + self.attr.get_or_insert_with(|| attr.span()); } } @@ -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 82afe1e4..b69a1c26 100644 --- a/bon/Cargo.toml +++ b/bon/Cargo.toml @@ -85,6 +85,9 @@ implied-bounds = ["bon-macros/implied-bounds"] # describing your use case for it. experimental-overwritable = ["bon-macros/experimental-overwritable"] + +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/src/__/ide.rs b/bon/src/__/ide.rs index a0809f81..77a97b8a 100644 --- a/bon/src/__/ide.rs +++ b/bon/src/__/ide.rs @@ -101,6 +101,40 @@ pub mod builder_top_level { pub use core::convert::Into; } + /// 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. diff --git a/bon/tests/integration/builder/build_from.rs b/bon/tests/integration/builder/build_from.rs new file mode 100644 index 00000000..76f1d5b1 --- /dev/null +++ b/bon/tests/integration/builder/build_from.rs @@ -0,0 +1,53 @@ +use crate::prelude::*; + +#[derive(Builder, Clone)] +#[builder(build_from, build_from_clone)] +struct Sut { + name: String, + age: u8, +} + +#[test] +fn test_build_from_works() { + 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 = 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 3a365a29..f8489544 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -14,6 +14,8 @@ 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; mod init_order;