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 @@ -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 = []

Expand Down
148 changes: 148 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,148 @@
use crate::builder::builder_gen::{BuilderGenCtx, member::Member};
use crate::util::prelude::*;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{Type, ext::IdentExt, spanned::Spanned};

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.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.is_some() {
tokens.extend(emit_build_from_method(
true,
&base_name,
target_ty,
&ctx.members,
&ctor_args,
ctx.build_from_clone.as_ref(),
)?);
}
Ok(tokens)
}

fn emit_build_from_method(
clone: bool,
base_name: &Ident,
target_ty: &Type,
members: &[Member],
ctor_args: &[TokenStream],
config: Option<&crate::parsing::ItemSigConfig>,
) -> Result<TokenStream> {
let doc = if clone {
"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: 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)
} 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<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> {
let path = ty.as_path_no_qself().ok_or_else(|| {
err!(
&span,
"expected a concrete type path (like `MyStruct`) for constructor"
)
})?;
let mut clean_path = path.clone();
if let Some(last_segment) = clean_path.segments.last_mut() {
last_segment.arguments = syn::PathArguments::None;
last_segment.ident.set_span(span);
}
Ok(quote! { #clean_path })
}
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 @@ -422,6 +422,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,
})
}
}
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 @@ -244,6 +244,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,
})
}
}
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 @@ -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<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 @@ -102,6 +105,7 @@ enum ParamName {
Into,
Name,
Overwritable,
BuildFrom,
Required,
Setters,
Skip,
Expand All @@ -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",
Expand Down Expand Up @@ -184,6 +189,7 @@ impl MemberConfig {
into,
name,
overwritable,
build_from,
required,
setters,
skip,
Expand All @@ -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),
Expand Down Expand Up @@ -231,6 +238,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 self.start_fn.is_present() {
self.validate_mutually_allowed(
ParamName::StartFn,
Expand Down
22 changes: 22 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 @@ -144,6 +147,24 @@ impl BuilderGenCtx {

let allows = allow_warnings_on_member_types();

let build_froms = {
#[cfg(feature = "experimental-build-from")]
{
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! {}
}
};

Ok(quote! {
#allows
// Ignore dead code warnings because some setter/getter methods may
Expand All @@ -158,6 +179,7 @@ impl BuilderGenCtx {
#where_clause
{
#finish_fn
#build_froms
#(#accessor_methods)*
#generic_setter_methods
}
Expand Down
38 changes: 38 additions & 0 deletions bon-macros/src/builder/builder_gen/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -183,6 +189,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: Option<ItemSigConfig>,
#[cfg(feature = "experimental-build-from")]
pub(super) build_from_clone: Option<ItemSigConfig>,
}

pub(super) struct BuilderGenCtxParams<'a> {
Expand Down Expand Up @@ -212,6 +222,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: Option<Override<syn::Meta>>,
#[cfg(feature = "experimental-build-from")]
pub(super) build_from_clone: Option<Override<syn::Meta>>,
}

impl BuilderGenCtx {
Expand All @@ -231,8 +245,28 @@ 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 = 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 = 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,
vis: builder_type.vis.unwrap_or(orig_item_vis),
Expand Down Expand Up @@ -385,6 +419,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
15 changes: 13 additions & 2 deletions bon-macros/src/builder/builder_gen/top_level_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ 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;

#[cfg(feature = "experimental-build-from")]
use darling::util::Override;

fn parse_finish_fn(meta: &syn::Meta) -> Result<ItemSigConfig> {
ItemSigConfigParsing::new(meta, Some("builder struct's impl block")).parse()
Expand Down Expand Up @@ -65,6 +68,14 @@ 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<SpannedKey<GenericsConfig>>,

#[cfg(feature = "experimental-build-from")]
#[darling(default)]
pub(crate) build_from: Option<Override<syn::Meta>>,

#[cfg(feature = "experimental-build-from")]
#[darling(default)]
pub(crate) build_from_clone: Option<Override<syn::Meta>>,
}

impl TopLevelConfig {
Expand Down
Loading