Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bon-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ rustversion = "1.0"
[features]
default = []

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

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

Expand Down
92 changes: 92 additions & 0 deletions bon-macros/src/builder/builder_gen/getter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use proc_macro2::TokenStream;
use quote::quote;

use super::{BuilderGenCtx, IdentExt, NamedMember};

pub(crate) struct GetterCtx<'a> {
base: &'a BuilderGenCtx,
member: &'a NamedMember,
}

struct GetterItem {
name: syn::Ident,
vis: syn::Visibility,
docs: Vec<syn::Attribute>,
}

impl<'a> GetterCtx<'a> {
pub(crate) fn new(base: &'a BuilderGenCtx, member: &'a NamedMember) -> Self {
Self { base, member }
}

pub(crate) fn getter_method(&self) -> TokenStream {
let Some(GetterItem { name, vis, docs }) = GetterItem::new(self) else {

Check failure on line 23 in bon-macros/src/builder/builder_gen/getter.rs

View workflow job for this annotation

GitHub Actions / test-msrv (ubuntu)

`let...else` statements are unstable

Check failure on line 23 in bon-macros/src/builder/builder_gen/getter.rs

View workflow job for this annotation

GitHub Actions / test-msrv (windows)

`let...else` statements are unstable

Check failure on line 23 in bon-macros/src/builder/builder_gen/getter.rs

View workflow job for this annotation

GitHub Actions / test-msrv (macos)

`let...else` statements are unstable
return quote! {};
};

let index = &self.member.index;
let ty = self.member.underlying_norm_ty();

let (return_type, body) = if self.member.is_required() {
(
quote! { &#ty },
quote! { unsafe { ::std::option::Option::unwrap_unchecked(self.__unsafe_private_named.#index.as_ref()) } },
)
} else {
(
quote! { ::core::option::Option<&#ty> },
quote! { self.__unsafe_private_named.#index.as_ref() },
)
};

let state_var = &self.base.state_var;
let member_pascal = &self.member.name.pascal;
let state_mod = &self.base.state_mod.ident;

quote! {
#( #docs )*
#[allow(
// This is intentional. We want the builder syntax to compile away
clippy::inline_always,
clippy::missing_const_for_fn,
)]
#[inline(always)]
#vis fn #name(&self) -> #return_type
where #state_var::#member_pascal: #state_mod::IsSet,
{
#body
}
}
}
}

impl GetterItem {
fn new(ctx: &GetterCtx<'_>) -> Option<Self> {
let GetterCtx { member, base } = ctx;

let spanned_keyed_config = member.config.getter.as_ref()?;

let common_name = spanned_keyed_config.name();
let common_vis = spanned_keyed_config.vis();
let common_docs = spanned_keyed_config.docs();

Some(Self {
name: common_name.cloned().unwrap_or_else(|| {
syn::Ident::new(
&format!("get_{}", member.name.snake.raw_name()),
member.name.snake.span(),
)
}),
vis: common_vis.unwrap_or(&base.builder_type.vis).clone(),
docs: common_docs
.map(<[syn::Attribute]>::to_vec)
.unwrap_or_else(|| {
const HEADER: &str = "_**Getter.**_\n\n";

std::iter::once(syn::parse_quote!(#[doc = #HEADER]))
.chain(member.docs.iter().cloned())
.collect()
}),
})
}
}
54 changes: 54 additions & 0 deletions bon-macros/src/builder/builder_gen/member/config/getter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use darling::FromMeta;

use super::{Result, SpannedKey};

#[derive(Debug, Default)]
pub(crate) struct GetterConfig {
name: Option<SpannedKey<syn::Ident>>,
vis: Option<SpannedKey<syn::Visibility>>,

docs: Option<SpannedKey<Vec<syn::Attribute>>>,
}

impl FromMeta for GetterConfig {
fn from_meta(meta: &syn::Meta) -> Result<Self> {
if let syn::Meta::Path(_) = meta {
return Ok(Self::default());
}

// Reject empty parens such as `#[builder(getter())]`
crate::parsing::require_non_empty_paren_meta_list_or_name_value(meta)?;

// Nested `Parsed` struct used as a helper for parsing the verbose form
#[derive(FromMeta)]
struct Parsed {
name: Option<SpannedKey<syn::Ident>>,
vis: Option<SpannedKey<syn::Visibility>>,

#[darling(rename = "doc", default, with = parse_docs, map = Some)]

Check warning on line 28 in bon-macros/src/builder/builder_gen/member/config/getter.rs

View workflow job for this annotation

GitHub Actions / cargo-miri

`if let` assigns a shorter lifetime since Edition 2024
docs: Option<SpannedKey<Vec<syn::Attribute>>>,
}

let Parsed { name, vis, docs } = Parsed::from_meta(meta)?;

Ok(Self { name, vis, docs })
}
}

impl GetterConfig {
pub(crate) fn name(&self) -> Option<&syn::Ident> {
self.name.as_ref().map(|n| &n.value)
}

pub(crate) fn vis(&self) -> Option<&syn::Visibility> {
self.vis.as_ref().map(|v| &v.value)
}

pub(crate) fn docs(&self) -> Option<&[syn::Attribute]> {
self.docs.as_ref().map(|a| &a.value).map(|a| &**a)
}
}

fn parse_docs(meta: &syn::Meta) -> Result<SpannedKey<Vec<syn::Attribute>>> {
crate::parsing::parse_docs_without_self_mentions("builder struct's impl block", meta)
}
43 changes: 41 additions & 2 deletions bon-macros/src/builder/builder_gen/member/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod blanket;
mod getter;
mod setters;
mod with;

pub(crate) use blanket::*;
pub(crate) use getter::*;
pub(crate) use setters::*;
pub(crate) use with::*;

Expand Down Expand Up @@ -33,6 +35,12 @@ pub(crate) struct MemberConfig {
#[darling(with = parse_optional_expr, map = Some)]
pub(crate) field: Option<SpannedKey<Option<syn::Expr>>>,

/// Make the member gettable by reference.
///
/// This takes the same attributes as the setter fns; `name`, `vis`, and `doc`
/// and produces a getter method that returns `&T` for the member.
pub(crate) getter: Option<SpannedKey<GetterConfig>>,

/// Accept the value for the member in the finishing function parameters.
pub(crate) finish_fn: darling::util::Flag,

Expand Down Expand Up @@ -82,6 +90,7 @@ pub(crate) struct MemberConfig {
enum ParamName {
Default,
Field,
Getter,
FinishFn,
Into,
Name,
Expand All @@ -98,6 +107,7 @@ impl fmt::Display for ParamName {
let str = match self {
Self::Default => "default",
Self::Field => "field",
Self::Getter => "getter",
Self::FinishFn => "finish_fn",
Self::Into => "into",
Self::Name => "name",
Expand Down Expand Up @@ -162,6 +172,7 @@ impl MemberConfig {
let Self {
default,
field,
getter,
finish_fn,
into,
name,
Expand All @@ -176,6 +187,7 @@ impl MemberConfig {
let attrs = [
(default.is_some(), ParamName::Default),
(field.is_some(), ParamName::Field),
(getter.is_some(), ParamName::Getter),
(finish_fn.is_present(), ParamName::FinishFn),
(into.is_present(), ParamName::Into),
(name.is_some(), ParamName::Name),
Expand Down Expand Up @@ -212,15 +224,42 @@ impl MemberConfig {
self.validate_mutually_allowed(
ParamName::StartFn,
self.start_fn.span(),
&[ParamName::Into],
&[ParamName::Into, ParamName::Getter],
)?;
}

if self.finish_fn.is_present() {
self.validate_mutually_allowed(
ParamName::FinishFn,
self.finish_fn.span(),
&[ParamName::Into],
&[ParamName::Into, ParamName::Getter],
)?;
}

if let Some(getter) = &self.getter {
if !cfg!(feature = "experimental-getter") {
bail!(
&getter.key.span(),
"`getter` attribute is experimental and requires \
\"experimental-getter\" cargo feature to be enabled; \
we would be glad to make this attribute stable if you find it useful; \
please leave a 👍 reaction under the issue https://github.com/elastio/bon/issues/221 \
Copy link

@beyera beyera Dec 1, 2024

Choose a reason for hiding this comment

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

I believe you want to reference the new stabilize tracking issue here instead.

Suggested change
please leave a 👍 reaction under the issue https://github.com/elastio/bon/issues/221 \
please leave a 👍 reaction under the issue https://github.com/elastio/bon/issues/225 \

Copy link
Collaborator

@Veetaha Veetaha Dec 1, 2024

Choose a reason for hiding this comment

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

Yeah, I updated this code locally, although I can't push my changes to Kindness-Works:laz/221-getters even though there is this sentence in the PR metadata:
image

$ git push Kindness-Works pr/lazkindness/222:laz/221-getters
ERROR: Permission to Kindness-Works/bon.git denied to Veetaha.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights

Maybe you have an org-wise rejection for writes from external contributors?

Anyway, I'll force-merge this PR and submit my amendments in a separate PR

Opened a followup PR #226

cc @lazkindness

to help us measure the impact on this feature. If you have \
a use case for this attribute, then open an issue/discussion on \
https://github.com/elastio/bon/issues.",
);
}

self.validate_mutually_allowed(
ParamName::Getter,
getter.key.span(),
&[
ParamName::With,
ParamName::Into,
ParamName::Name,
ParamName::Setters,
ParamName::Required,
],
)?;
}

Expand Down
7 changes: 7 additions & 0 deletions bon-macros/src/builder/builder_gen/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod builder_decl;
mod builder_derives;
mod finish_fn;
mod getter;
mod member;
mod models;
mod setters;
Expand All @@ -11,6 +12,7 @@ mod top_level_config;
pub(crate) mod input_fn;
pub(crate) mod input_struct;

use getter::GetterCtx;
pub(crate) use top_level_config::TopLevelConfig;

use crate::util::prelude::*;
Expand Down Expand Up @@ -107,6 +109,10 @@ impl BuilderGenCtx {
.map(|member| SettersCtx::new(self, member).setter_methods())
.collect::<Result<Vec<_>>>()?;

let getter_methods = self
.named_members()
.map(|member| GetterCtx::new(self, member).getter_method());

let generics_decl = &self.generics.decl_without_defaults;
let generic_args = &self.generics.args;
let where_clause = &self.generics.where_clause;
Expand All @@ -128,6 +134,7 @@ impl BuilderGenCtx {
{
#finish_fn
#(#setter_methods)*
#(#getter_methods)*
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion bon-sandbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ targets = ["x86_64-unknown-linux-gnu"]
workspace = true

[dependencies]
bon = { path = "../bon", version = "=3.1.1", features = ["experimental-overwritable"] }
bon = { path = "../bon", version = "=3.1.1", features = ["experimental-overwritable", "experimental-getter"] }
buildstructor = "0.5"
derive_builder = "0.20"
typed-builder = "0.20"
17 changes: 17 additions & 0 deletions bon-sandbox/src/getter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use bon::{builder, Builder};

#[builder]
pub fn full_name_fn(#[builder(getter)] first_name: &str, last_name: &str) -> String {
format!("{first_name} {last_name}")
}

#[derive(Builder)]
pub struct FullName {
#[builder(getter)]
pub first_name: String,
#[builder(getter(name = get_the_last_name, vis = "pub(crate)", doc {
/// Docs on the getter
}))]
pub last_name: String,
pub no_getter: String,
}
1 change: 1 addition & 0 deletions bon-sandbox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod attr_default;
pub mod attr_with;
pub mod docs_comparison;
pub mod functions;
pub mod getter;
pub mod macro_rules_wrapper_test;
pub mod missing_docs_test;
pub mod overrides;
Expand Down
14 changes: 14 additions & 0 deletions bon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,17 @@ implied-bounds = ["bon-macros/implied-bounds"]
# this attribute. It would also be cool if you could leave a comment under that issue
# describing your use case for it.
experimental-overwritable = ["bon-macros/experimental-overwritable"]

# 🔬 Experimental! There may be breaking changes to this feature between *minor* releases,
# however, compatibility within patch releases is guaranteed though.
#
# This feature enables the #[builder(getter)] attribute that can be used to
# allow getting references to already set fields in the builder.
#
# See more info at https://bon-rs.com/reference/builder/member/getter.
#
# We are considering stabilizing this attribute if you have a use for it. Please leave
# a 👍 reaction under the issue https://github.com/elastio/bon/issues/221 if you need
Copy link

Choose a reason for hiding this comment

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

Same here?

Suggested change
# a 👍 reaction under the issue https://github.com/elastio/bon/issues/221 if you need
# a 👍 reaction under the issue https://github.com/elastio/bon/issues/225 if you need

# this attribute. It would also be cool if you could leave a comment under that issue
# describing your use case for it.
experimental-getter = ["bon-macros/experimental-getter"]
Loading
Loading