-
-
Notifications
You must be signed in to change notification settings - Fork 34
Support #[builder(getter(...))] attribute
#222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e0c535b
834978e
60eb3e9
8c4940d
710f9ec
2871335
f04525a
9aa0a6a
0fa1ba6
bb484c4
7212fa0
c0cca06
6babf8e
9db3e48
2ed3e51
a6a007b
56ca0cd
a8fc70d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| 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() | ||
| }), | ||
| }) | ||
| } | ||
| } | ||
| 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)] | ||
| 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) | ||
| } | ||
| 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::*; | ||||||
|
|
||||||
|
|
@@ -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, | ||||||
|
|
||||||
|
|
@@ -82,6 +90,7 @@ pub(crate) struct MemberConfig { | |||||
| enum ParamName { | ||||||
| Default, | ||||||
| Field, | ||||||
| Getter, | ||||||
| FinishFn, | ||||||
| Into, | ||||||
| Name, | ||||||
|
|
@@ -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", | ||||||
|
|
@@ -162,6 +172,7 @@ impl MemberConfig { | |||||
| let Self { | ||||||
| default, | ||||||
| field, | ||||||
| getter, | ||||||
| finish_fn, | ||||||
| into, | ||||||
| name, | ||||||
|
|
@@ -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), | ||||||
|
|
@@ -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 \ | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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, | ||||||
| ], | ||||||
| )?; | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| 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, | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here?
Suggested change
|
||||||
| # 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"] | ||||||

Uh oh!
There was an error while loading. Please reload this page.