diff --git a/Cargo.lock b/Cargo.lock index ec3fd772..e803efcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,6 +351,7 @@ dependencies = [ "buildstructor", "itertools 0.13.0", "lazy-regex", + "macro_rules_attribute", "walkdir", ] @@ -538,6 +539,22 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "macro_rules_attribute" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a82271f7bc033d84bbca59a3ce3e4159938cb08a9c3aebbe54d215131518a13" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" + [[package]] name = "memchr" version = "2.7.4" @@ -601,6 +618,12 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pin-project-lite" version = "0.2.14" diff --git a/bon-macros/src/builder/builder_gen/input_func.rs b/bon-macros/src/builder/builder_gen/input_func.rs index d90bb3f7..b6b9dda2 100644 --- a/bon-macros/src/builder/builder_gen/input_func.rs +++ b/bon-macros/src/builder/builder_gen/input_func.rs @@ -430,10 +430,7 @@ impl FinishFuncBody for FnCallBody { let prefix = self .sig .receiver() - .map(|receiver| { - let self_token = &receiver.self_token; - quote!(#self_token.__private_receiver.) - }) + .map(|_| quote!(self.__private_receiver.)) .or_else(|| { let self_ty = &self.impl_ctx.as_deref()?.self_ty; Some(quote!(<#self_ty>::)) diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 160f4eac..d6f097d2 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -21,8 +21,9 @@ workspace = true bon = { path = "../bon" } [dev-dependencies] -anyhow = "1.0" -buildstructor = "0.5" +anyhow = "1.0" +buildstructor = "0.5" +macro_rules_attribute = "0.2" [build-dependencies] itertools = "0.13" diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index f238713a..f017090c 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -131,6 +131,10 @@ export default defineConfig({ text: "Into Conversions In-Depth", link: "/guide/patterns/into-conversions-in-depth", }, + { + text: "Shared Configuration", + link: "/guide/patterns/shared-configuration", + } ], }, { diff --git a/website/guide/compatibility.md b/website/guide/compatibility.md index 03b81221..68daffbe 100644 --- a/website/guide/compatibility.md +++ b/website/guide/compatibility.md @@ -181,7 +181,7 @@ If your existing code defines functions with positional parameters in its public See [this attribute's docs](../reference/builder#expose-positional-fn) for details. -**Example**: +**Example:** ```rust ignore use bon::builder; diff --git a/website/guide/patterns/shared-configuration.md b/website/guide/patterns/shared-configuration.md new file mode 100644 index 00000000..f550bb08 --- /dev/null +++ b/website/guide/patterns/shared-configuration.md @@ -0,0 +1,105 @@ +--- +outline: deep +--- + +# Shared Configuration + +On this page, you'll learn how to share common configurations for builders to avoid code duplication. + +## Problem statement + +As an example, let's suppose you want to enable [`Into` conversions](./into-conversions-in-depth) for specific types across all your builders and maybe also override the name of the finishing function that consumes the builder from the default `build` to `finish`. The problem that you'll quickly run into is that you'll need to repeat the same configuration everywhere: + +```rust +use bon::Builder; + +#[derive(Builder)] +#[builder( + on(String, into), // [!code highlight] + on(Box<_>, into), // [!code highlight] + finish_fn = finish, // [!code highlight] +)] +struct MyLovelyStruct1 { /**/ } + + +#[derive(Builder)] +#[builder( + on(String, into), // [!code highlight] + on(Box<_>, into), // [!code highlight] + finish_fn = finish, // [!code highlight] +)] +struct MyLovelyStruct2 { /**/ } +``` + +::: tip + +This code uses the [`#[builder(on(...))]`](../../reference/builder#on) attribute to configure the types of members for which `bon` should enable `Into` conversions. + +::: + +The annoying thing here is that we need to copy all these configurations on every struct where we derive the builder. + +## Solution + +### Structs + +To overcome this problem we can utilize the [`macro_rules_attribute`] crate. It allows you to declare an [`attribute_alias`](https://docs.rs/macro_rules_attribute/latest/macro_rules_attribute/macro.attribute_alias.html) that defines all the shared configuration for your builders and makes it reusable. + +So with the [`macro_rules_attribute`] your code will look like this: + +```rust +use macro_rules_attribute::{attribute_alias, apply}; + +// The alias can also be defined in a separate module. +// Under the hood it creates a macro with `pub(crate)` visibility. +attribute_alias! { + #[apply(derive_builder!)] = + #[derive(::bon::Builder)] + #[builder( + on(String, into), // [!code highlight] + on(Box<_>, into), // [!code highlight] + finish_fn = finish, // [!code highlight] + )]; +} + +#[apply(derive_builder!)] +struct MyLovelyStruct1 { /**/ } + +#[apply(derive_builder!)] +struct MyLovelyStruct2 { /**/ } +``` + +Use this approach if you have a lot of structs in your crate that need a builder. Adding [`macro_rules_attribute`] to your dependencies shouldn't have a noticeable impact on the compilation performance. This approach [was tested](https://github.com/ayrat555/frankenstein/blob/91ac379a52ed716e09632f78b984852c85f2adaa/src/macros.rs#L3-L14) on a crate with ~320 structs that derive a builder and compile time was the same as before adding the [`macro_rules_attribute`] crate. + +### Free functions + +A similar approach works with `#[bon::builder]` on free functions. +**Example:** + +```rust + +use macro_rules_attribute::{attribute_alias, apply}; + +attribute_alias! { + #[apply(builder!)] = + #[::bon::builder( + on(String, into), // [!code highlight] + on(Box<_>, into), // [!code highlight] + finish_fn = finish, // [!code highlight] + )]; +} + +#[apply(builder!)] +fn my_lovely_fn1(/**/) { /**/ } + +#[apply(builder!)] +fn my_lovely_fn2(/**/) { /**/ } +``` + +### Associated methods + +Unfortunately, this technique doesn't quite work with associated methods (functions inside impl blocks) due to the limitations of proc macro attribute expansion order. The `#[bon]` macro on top of the impl block is expanded first before the `#[apply(...)]` macro inside of the impl block, so `#[bon]` doesn't see the configuration expanded from the `#[apply(...)]`. + +There is a proposed solution to this problem in the issue [#elastio/bon#144](https://github.com/elastio/bon/issues/144). Add a 👍 to that issue if your use case needs a solution for this, and maybe leave a comment about your particular use case where you'd like to have this feature. + +[`macro_rules_attribute`]: https://docs.rs/macro_rules_attribute/latest/macro_rules_attribute/ diff --git a/website/reference/builder.md b/website/reference/builder.md index fd4444c7..bf19bf20 100644 --- a/website/reference/builder.md +++ b/website/reference/builder.md @@ -1117,7 +1117,7 @@ Don't confuse this with the top-level [`#[builder(start_fn = ...)]`](#start-fn) ::: -**Example**: +**Example:** ::: code-group @@ -1224,7 +1224,7 @@ Don't confuse this with the top-level [`#[builder(finish_fn = ...)]`](#finish-fn ::: -**Example**: +**Example:** ::: code-group diff --git a/website/v1/guide/compatibility.md b/website/v1/guide/compatibility.md index 1c844730..515cd37a 100644 --- a/website/v1/guide/compatibility.md +++ b/website/v1/guide/compatibility.md @@ -180,7 +180,7 @@ If your existing code defines functions with positional parameters in its public See [this attribute's docs](../reference/builder#expose-positional-fn) for details. -**Example**: +**Example:** ```rust ignore use bon::builder;