From 27264db42f1482a076a86a4d2610270f96c5e58b Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 4 Nov 2024 21:04:46 +0000 Subject: [PATCH 01/12] Update fallible builders pattern page and the typestate API introduction --- .../src/guide/patterns/fallible-builders.md | 22 +++++++++++---- website/src/guide/typestate-api.md | 28 +++++++++++++++++-- website/src/reference/builder/member/with.md | 4 +++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/website/src/guide/patterns/fallible-builders.md b/website/src/guide/patterns/fallible-builders.md index 4808970e..c5ac0f43 100644 --- a/website/src/guide/patterns/fallible-builders.md +++ b/website/src/guide/patterns/fallible-builders.md @@ -1,10 +1,6 @@ # Fallible Builders -With `bon`, you can write a builder that validates its inputs and returns a `Result`. It's possible to do this only via the function or associated method syntax. - -If you need to build a struct and do some validation, you won't be able to use the `#[derive(Builder)]` annotation on the struct for that. You'll _have to_ define your logic in the associated constructor method (e.g. `new`). - -**Example:** +With `bon`, you can write a builder that validates its inputs and returns a `Result`. It's possible to do this via the function or associated method syntax. Simply write a constructor function with the `Result` return type and add a `#[builder]` to it. ```rust use anyhow::Error; @@ -38,4 +34,18 @@ if let Err(error) = result { } ``` -If you have a use case for some convenience attributes to do automatic validations using the `#[derive(Builder)]` macro with the struct syntax, then add a ๐Ÿ‘ reaction to [this Github issue](https://github.com/elastio/bon/issues/34). +With this approach, the finishing function of the generated builder returns a `Result`. Thus, validations are deferred until you invoke the finishing `build()` or `call()`. + +## Fallible Setter + +You can do validations earlier instead, right when the setter is called. Use `#[builder(with)]` with a fallible closure to achieve that. The following example is an excerpt from that attribute's [API reference](../../reference/builder/member/with), see more details there in the [Fallible Closure](../../reference/builder/member/with#fallible-closure) section. + + + +## None Of This Works. Help! + +This is very, _very_, **very**(!) unlikely but if you have an elaborate use case where none of the options above are flexible enough, then your last resort is writing a [custom method](../typestate-api/custom-methods) on the builder. You'll need to study the builder's [Typestate API](../typestate-api) to be able to do that. Don't worry, it's rather simple, and you'll be much more powerful at the end of the day ๐Ÿฑ. + +## Future Possibilities + +If you have some design ideas for an attributes API to do validations with the builder macros, then feel free to post them in [this Github issue](https://github.com/elastio/bon/issues/34). diff --git a/website/src/guide/typestate-api.md b/website/src/guide/typestate-api.md index 8dc2dfc1..51840f41 100644 --- a/website/src/guide/typestate-api.md +++ b/website/src/guide/typestate-api.md @@ -2,6 +2,30 @@ This section teaches you the builder's typestate API. It describes the underlying component traits and types of the builder type and how to use them. -Reading this is optional. The typestate API is private by default. The users of your builder can't denote its type unless you enable [`#[builder(state_mod(vis = "pub"))]`](../reference/builder/top-level/state_mod). +Reading this is optional. By default, both the typestate API and the builder's type signature (except for the initial state) are not visible/nameable outside of the module where the builder was generated. You can use `bon` perfectly fine without ever touching the typestate API so closely. -It is more of an advanced concept that you'll rarely need. However, "advanced" doesn't necessarily mean "complicated" in this case. So feel free to study this section if you feel like it. +## Why Would I Need It Anyway? + +The typestate API should only come into play when you have a use case that isn't expressible with existing configuration attributes. + +Here are some example use cases when you'd need to interact with the typestate API: + +- Denoting the builder's type when... + - returning it from a function + - declaring it as a parameter of a function + - storing it in a struct +- Defining a setter that... + - is `unsafe` or `async` + - declares its own named generic parameters + - sets multiple members at once + - ~~Cooks you a dinner ๐Ÿ~~ + +Just kidding ๐Ÿ˜ธ, the Typestate API can't cook your dinner ๐Ÿ˜ณ, but **you** can definitely cook something cool using it ๐Ÿ’ช! + +Spoiler: the typestate API is rather simple, and it's designed for humans. You **will not** feel like writing some cryptic code that should've been generated by a macro otherwise. + +Note, however, that if you have to resort to the typestate API a lot, then it _may_ indicate that `bon` may be missing some attributes' syntax sugar for your use case. Feel free to create a feature request via an issue or discussion on [Github](https://github.com/elastio/bon), or message us on [Discord](https://bon-rs.com/discord) about it. + +--- + +Now, click the "Next page" link at the bottom to start learning ๐Ÿš€ diff --git a/website/src/reference/builder/member/with.md b/website/src/reference/builder/member/with.md index b631fd1e..9a76fd0b 100644 --- a/website/src/reference/builder/member/with.md +++ b/website/src/reference/builder/member/with.md @@ -107,6 +107,8 @@ assert_eq!(value.y, 3); You can add a return type annotation to the closure to signify that it's fallible. The closure is expected to return a `Result` with the `Ok` variant of the member's underlying type. This will make the setter fallible. + + ::: code-group ```rust [Struct] @@ -184,6 +186,8 @@ fn main() -> Result<(), ParseIntError> { ::: + + The return type annotation must be of the form `*Result<_[, E]>`. Here `*Result` means the type must have a `Result` suffix. `[, E]` means the error type annotation is optional. From 965a4a26cdfd58465f7e49ba3847b78664c566f1 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 4 Nov 2024 22:55:17 +0000 Subject: [PATCH 02/12] Try fix miri CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b864a42..76787036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,7 +157,7 @@ jobs: cargo miri test --locked --all-features --all-targets \ --workspace --exclude runtime-benchmarks env: - RUSTFLAGS: >- + MIRIFLAGS: >- --deny warnings --allow edition-2024-expr-fragment-specifier --allow if_let_rescope From fb73b65218f1f42a503efb6598161c2dd2be78b7 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Tue, 5 Nov 2024 01:27:38 +0000 Subject: [PATCH 03/12] Add more details to typestate API and builder's type signature docs --- .github/workflows/ci.yml | 4 +- website/src/guide/misc/alternatives.md | 9 ++-- website/src/guide/typestate-api.md | 4 +- .../typestate-api/builders-type-signature.md | 54 +++++++++++++++---- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76787036..8dea42b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,13 +151,13 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2024-10-30 - components: miri + components: miri, rust-src - run: | cargo miri test --locked --all-features --all-targets \ --workspace --exclude runtime-benchmarks env: - MIRIFLAGS: >- + RUSTFLAGS: >- --deny warnings --allow edition-2024-expr-fragment-specifier --allow if_let_rescope diff --git a/website/src/guide/misc/alternatives.md b/website/src/guide/misc/alternatives.md index 58e60b09..a908dc8f 100644 --- a/website/src/guide/misc/alternatives.md +++ b/website/src/guide/misc/alternatives.md @@ -18,12 +18,10 @@ There are several other existing alternative crates that generate builders. `bon | Making required member optional is compatible by default | โœ… | โœ… | opt-in `#[builder(setter(strip_option))]` | opt-in `#[builder(setter(strip_option))]` | | Generates `T::builder()` method | โœ… | โœ… | โœ… | only `Builder::default()` | | `Into` conversion in setters | opt-in ([members subset][bon-on], [single member][bon-into]) | [implicit (automatic)][bs-into] | opt-in (all members + out-out, single member) | [opt-in (all members, single member)][db-into] | -| `impl Trait` supported for functions | โœ… | | | -| Anonymous lifetimes supported for functions | โœ… | | | -| `Self` mentions in functions/structs are supported | โœ… | | | +| `impl Trait`, anonymous lifetimes support | โœ… | | | | Positional function is hidden by default | โœ… | | | | Special setter methods for collections | [(see below)][r1] | โœ… | | โœ… | -| Custom methods can be added to the builder type | | | โœ… ([mutators]) | โœ… | +| Custom methods can be added to the builder type | โœ… [Typestate API] | | โœ… ([Mutators]) | โœ… | | Builder may be configured to use &self/&mut self | | | | โœ… | ## Function builder fallback paradigm @@ -97,9 +95,10 @@ Another difference is that fields of collection types are considered required by [arr]: https://docs.rs/bon/latest/bon/macro.arr.html [map]: https://docs.rs/bon/latest/bon/macro.map.html [set]: https://docs.rs/bon/latest/bon/macro.set.html -[mutators]: https://docs.rs/typed-builder/latest/typed_builder/derive.TypedBuilder.html#mutators +[Mutators]: https://docs.rs/typed-builder/latest/typed_builder/derive.TypedBuilder.html#mutators [bon-on]: ../../reference/builder/top-level/on [bon-into]: ../../reference/builder/member/into [bs-into]: https://docs.rs/buildstructor/latest/buildstructor/#into-field [db-into]: https://docs.rs/derive_builder/latest/derive_builder/#generic-setters [r1]: #special-setter-methods-for-collections +[Typestate API]: ../typestate-api diff --git a/website/src/guide/typestate-api.md b/website/src/guide/typestate-api.md index 51840f41..5e1d2c61 100644 --- a/website/src/guide/typestate-api.md +++ b/website/src/guide/typestate-api.md @@ -6,7 +6,7 @@ Reading this is optional. By default, both the typestate API and the builder's t ## Why Would I Need It Anyway? -The typestate API should only come into play when you have a use case that isn't expressible with existing configuration attributes. +The typestate API should only come into play when you have a use case that isn't expressible with [existing configuration attributes](../reference/builder). Here are some example use cases when you'd need to interact with the typestate API: @@ -20,7 +20,7 @@ Here are some example use cases when you'd need to interact with the typestate A - sets multiple members at once - ~~Cooks you a dinner ๐Ÿ~~ -Just kidding ๐Ÿ˜ธ, the Typestate API can't cook your dinner ๐Ÿ˜ณ, but **you** can definitely cook something cool using it ๐Ÿ’ช! +Just kidding ๐Ÿ˜ธ, the Typestate API can't cook you a dinner ๐Ÿ˜ณ, but **you** can definitely cook something cool using it ๐Ÿ’ช! Spoiler: the typestate API is rather simple, and it's designed for humans. You **will not** feel like writing some cryptic code that should've been generated by a macro otherwise. diff --git a/website/src/guide/typestate-api/builders-type-signature.md b/website/src/guide/typestate-api/builders-type-signature.md index d5d7cc8d..2c9de35c 100644 --- a/website/src/guide/typestate-api/builders-type-signature.md +++ b/website/src/guide/typestate-api/builders-type-signature.md @@ -16,7 +16,7 @@ Builders generated by `bon` macros use the typestate pattern. The builder's type Every time you call a setter the builder's type changes. The builder always contains a generic parameter `S` (stands for "state") at the end. This parameter holds the type state that describes what members were set in the builder. -It's probably easier to understand with an example. Don't worry, the next paragraph will explain everything. +It's probably easier to understand with an example. Don't worry, the next paragraph will explain everything ๐Ÿˆ. ```rust use bon::Builder; @@ -42,13 +42,43 @@ There is a special `Empty` type state, which is used as the default value for th - The builder type itself: `ExampleBuilder` - The type state transitions: `Set{Member}` -The type states come from the builder's state module. The name of that module is the `snake_case` version of the builder's name. +This is why we didn't have to mention the generic parameter for the first `ExampleBuilder`, and for `SetX1`. + +The type states come from the builder's state module. The name of that module is the `snake_case` version of the builder's type name. ## Visibility -The type state module is private by default and only accessible within the module where the builder macro was used. Thus, the builder type becomes unnameable outside of the module where it was generated. +The type state module is private by default and only accessible within the module where the builder macro was used. The type states and other symbols that we haven't covered yet in that module all inherit their visibility from the builder's type visibility. + +Visibility of the builder's type is by default derived from the visibility of the underlying struct or function from which the builder was generated. + +Here is a simplified view of the generated builder type and state module with their visibility. + +```rust +// Let's suppose we had this derive on a struct with `pub` visibility +// #[derive(bon::Builder)] +pub struct Example { + x1: u32 +} + +// Builder type inherits the `pub` visibility from the underlying `Example` struct +// from which it was generated. +pub struct ExampleBuilder { /**/ } + +// Typestate module is private by default. It means it is accessible only within +// the surrounding module. +mod example_builder { + // The type states inherit the builder type's visibility, which is `pub` + pub struct SetX1 { /**/ } + pub struct Empty { /**/ } + // ... +} + +``` + +Notice how we have `pub` symbols defined inside of a private module. Such a pattern ensures that the builder type is _accessible_, but _unnameable_ outside of the module where it was generated. This is similar to a sealed trait, but it's a "sealed" type in this case. -If you want to expose your builder's type signature, you need to add [`#[builder(state_mod(vis = "..."))]`](../../reference/builder/top-level/state_mod), where `...` can be `pub` or `pub(crate)` or any other visibility you want to expose the state module under. +If you want to expose your builder's type signature, you need to add [`#[builder(state_mod(vis = "..."))]`](../../reference/builder/top-level/state_mod), where `...` can be `pub` or `pub(crate)` or any other visibility you want to assign to the state module instead of the default private visibility. ## Example Rustdoc @@ -58,7 +88,7 @@ You can see the `rustdoc` API reference generated for this example [here](https: The builder inherits all generic parameters from the struct or function from which it was generated. -Functions may even use anonymous lifetimes and `impl Trait` syntax. Every such anonymous lifetime or `impl Trait` will get a separate generic parameter generated in the builder's type automatically. +Functions may even use anonymous/elided lifetimes and `impl Trait` syntax. Every such anonymous/elided lifetime or `impl Trait` will get a separate generic parameter generated in the builder's type automatically. ::: code-group @@ -70,8 +100,10 @@ struct Example<'a, T> { x1: &'a T, } +// 'aโ” let builder: ExampleBuilder<'_, bool, _> = Example::builder().x1(&true); - // ^- type state (always last) +// ^^^^ ^- type state (always last) +// Tโ”˜ ``` ```rust [Function] @@ -82,8 +114,10 @@ fn example( x1: &impl Clone ) {} +// lifetime param from `&...`โ” let builder: ExampleBuilder<'_, bool, _> = example().x1(&true); - // ^- type state (always last) +// ^^^^ ^- type state (always last) +// type param from `impl Clone`โ”˜ ``` ```rust [Method] @@ -97,8 +131,10 @@ impl Example { fn method(x1: &impl Clone) {} } +// lifetime param from `&...`โ” let builder: ExampleMethodBuilder<'_, bool, _> = Example::method().x1(&true); - // ^- type state (always last) +// ^^^^ ^- type state (always last) +// type param from `impl Clone`โ”˜ ``` ::: @@ -107,6 +143,6 @@ If there is a mix of named and anonymous lifetimes or named generic types and `i ## What's Next? -Now you know the mechanics of how a builder's type is built, so you can denote it when it's returned from a function or stored in a struct. +Now you know the mechanics of how a builder's type is built, so you can denote it when declaring function parameters or return type, or storing the builder in a struct. However, to be able to write useful custom methods on the builder, you'll need to know the traits behind the type states. Go to the next page to learn more. From 56e753c320ed660e0de9e6a4e8dd937d4dcc4c4e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Wed, 6 Nov 2024 00:47:49 +0000 Subject: [PATCH 04/12] Update Alternatives page --- website/package-lock.json | 24 +++- website/package.json | 2 +- website/src/guide/misc/alternatives.md | 117 +++++++++++++++--- .../patterns/into-conversions-in-depth.md | 8 +- 4 files changed, 122 insertions(+), 29 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index cacbda17..e4d8dc5f 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -20,7 +20,7 @@ "medium-zoom": "^1.1.0", "ts-node": "^10.9.2", "ts-pattern": "^5.5.0", - "vitepress": "^1.4.5" + "vitepress": "^1.5.0" } }, "node_modules/@algolia/autocomplete-core": { @@ -803,6 +803,21 @@ "node": ">=12" } }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.10.tgz", + "integrity": "sha512-9OK1dsSjXlH36lhu5n+BlSoXuqFjHUErGLtNdzHpq0vHq4YFBuGYWtZ+vZTHLreRQ8ijPRv/6EsgkV+nf6AReQ==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -3006,13 +3021,14 @@ } }, "node_modules/vitepress": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.4.5.tgz", - "integrity": "sha512-9K0k8kvdEbeowVCpKF/x0AySSq0Pr9pM8xufLgQcKMjsifwxtDjXJjcFhZv4LYw2dcpdYiBq2j7PnWi0tCaMCg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.5.0.tgz", + "integrity": "sha512-q4Q/G2zjvynvizdB3/bupdYkCJe2umSAMv9Ju4d92E6/NXJ59z70xB0q5p/4lpRyAwflDsbwy1mLV9Q5+nlB+g==", "dev": true, "dependencies": { "@docsearch/css": "^3.6.2", "@docsearch/js": "^3.6.2", + "@iconify-json/simple-icons": "^1.2.10", "@shikijs/core": "^1.22.2", "@shikijs/transformers": "^1.22.2", "@shikijs/types": "^1.22.2", diff --git a/website/package.json b/website/package.json index 51a11a5f..449130fe 100644 --- a/website/package.json +++ b/website/package.json @@ -19,7 +19,7 @@ "medium-zoom": "^1.1.0", "ts-node": "^10.9.2", "ts-pattern": "^5.5.0", - "vitepress": "^1.4.5" + "vitepress": "^1.5.0" }, "dependencies": { "vue": "^3.5.12" diff --git a/website/src/guide/misc/alternatives.md b/website/src/guide/misc/alternatives.md index a908dc8f..4d20d184 100644 --- a/website/src/guide/misc/alternatives.md +++ b/website/src/guide/misc/alternatives.md @@ -4,31 +4,100 @@ aside: false # Alternatives -There are several other existing alternative crates that generate builders. `bon` was designed with many lessons learned from them. Here is a table that compares the builder crates with some additional explanations below. +There are several other existing alternative crates for generating builders. `bon` was designed based on many lessons learned from them. A table that compares the builder crates with some additional explanations is below. + + + +
-| Feature | `bon` | [`buildstructor`] | [`typed-builder`] | [`derive_builder`] | -| -------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | -| Builder for structs | โœ… | โœ… | โœ… | โœ… | -| Builder for free functions | โœ… | | | -| Builder for associated methods | โœ… | โœ… | | -| Panic safe | โœ… | โœ… | โœ… | `build()` returns a `Result` | -| Member of `Option` type is optional by default | โœ… | โœ… | opt-in `#[builder(default)]` | opt-in `#[builder(default)]` | -| Making required member optional is compatible by default | โœ… | โœ… | opt-in `#[builder(setter(strip_option))]` | opt-in `#[builder(setter(strip_option))]` | -| Generates `T::builder()` method | โœ… | โœ… | โœ… | only `Builder::default()` | -| `Into` conversion in setters | opt-in ([members subset][bon-on], [single member][bon-into]) | [implicit (automatic)][bs-into] | opt-in (all members + out-out, single member) | [opt-in (all members, single member)][db-into] | -| `impl Trait`, anonymous lifetimes support | โœ… | | | -| Positional function is hidden by default | โœ… | | | -| Special setter methods for collections | [(see below)][r1] | โœ… | | โœ… | -| Custom methods can be added to the builder type | โœ… [Typestate API] | | โœ… ([Mutators]) | โœ… | -| Builder may be configured to use &self/&mut self | | | | โœ… | +| Feature | `bon` | [`buildstructor`] | [`typed-builder`] | [`derive_builder`] | +| ---------------------------------------- | ---------------------------------- | ------------------------- | ------------------------------------ | --------------------------------------------- | +| Builder for structs | โœ… | โœ… | โœ… | โœ… | +| Builder for free functions | โœ… | | | +| Builder for associated methods | โœ… | โœ… | | +| Panic safe | โœ… | โœ… | โœ… | `build()` returns `Result` | +| `Option` makes members optional | โœ… | โœ… | | | +| `T` -> `Option` is non-breaking | โœ… [docs][bon-req-to-opt] | โœ… | via attr `strip_option` | via attr [`strip_option`][db-so] | +| Generates `T::builder()` method | โœ… | โœ… | โœ… | only `Builder::default()` | +| `Into` conversion in setters | [opt-in][bon-into] | [implicit][bs-into] | opt-in | [opt-in][db-into] | +| Validation in the finishing function | โœ… [docs][bon-fallible-builder] | โœ… [docs][bs-fall-finish] | | โœ… [docs][db-fall-finish] | +| Validation in setters (fallible setters) | โœ… attr [`with = closure`][b] | | | โœ… `TryInto` via attr [`try_setter`][db-fs] | +| Custom methods on builder | โœ… via [direct impl block][bon-ts] | | โœ…  via [mutators] (attrs) | โœ… via [direct impl block][db-custom-methods] | +| `impl Trait`, elided lifetimes support | โœ… | | | +| Builder for `fn` hides original `fn` | โœ… | | | +| Special setters for collections | [(see below)][r1] | โœ… | | โœ… | +| Builder by `&self`/`&mut self` | | | | โœ… | + +
+ +## Function Builder Paradigm Shift + +If you ever hit a wall with `typed-builder` or `derive_builder`, you have no other choice but to hack something around their derive attributes syntax on a struct. + +With `bon` and `buildstructor` you can simply change the syntax from `#[derive(Builder)]` on a struct to a `#[builder]` on a function, which gives you much more flexibility. It is [guaranteed to preserve compatibility](../misc/compatibility#switching-between-derivebuilder-and-builder-on-the-new-method) (non a breaking change). It allows you to create fallible, `async` or even `unsafe` builders naturally. + +For example, suppose one day you found a need to count the number of fields that were default-initialized in the builder. + +```rust +use bon::Builder; + +#[derive(Builder)] +struct Example { + #[builder(default)] + x1: u32, + + #[builder(default)] + x2: u32, + + #[builder(skip = /* What? How can I calculate this here? ๐Ÿค” */)] + defaults_counter: u32 +} +``` + +::: tip + +The attribute [`#[builder(skip)]`](../../reference/builder/member/skip) skips generating setters for a member. The field is initialized with the given expression instead. + +::: + +The attribute [`#[builder(skip)]`](../../reference/builder/member/skip) is the first obvious candidate for this use case. It also has analogues in `typed-builder` and `derive_builder`. However, it's actually too limited for this use case, because it doesn't have the required context. + +At this point, you'd probably give up on `typed-builder` and `derive_builder`, because there is no way to express the required behavior with their attributes' syntax. However, it's as simple as pie with `bon` or `buildstructor`: -## Function builder fallback paradigm +```rust +use bon::bon; + +struct Example { + x1: u32, + x2: u32, + defaults_counter: u32 +} + +#[bon] +impl Example { + #[builder] + fn new(x1: Option, x2: Option) -> Self { + let mut defaults_counter = 0; + let x1 = x1.unwrap_or_else(|| { defaults_counter += 1; 0 }); + let x2 = x2.unwrap_or_else(|| { defaults_counter += 1; 0 }); + Self { x1, x2, defaults_counter } + } +} + +assert_eq!(Example::builder().build().defaults_counter, 2); +assert_eq!(Example::builder().x1(1).build().defaults_counter, 1); +assert_eq!(Example::builder().x1(1).x2(2).build().defaults_counter, 0); +``` -The builder crates `typed-builder` and `derive_builder` have a bunch of attributes that allow users to insert custom behaviour into the building process of the struct. However, `bon` and `buildstructor` avoid the complexity of additional config attributes for advanced use cases by proposing the user fallback to defining a custom function with the `#[builder]` attached to it where it's possible to do anything you want. +Ah... Simple just like regular Rust, isn't it? ๐Ÿ˜Œ -However, `bon` still provides some simple attributes for common use cases to configure the behaviour without falling back to a more verbose syntax. +The chances of hitting a wall with function builders are close to zero. Even if you ever hit the wall with function builders you still have access to the [Typestate API](../typestate-api/) in `bon` for even more flexibility. ## Special setter methods for collections @@ -98,7 +167,15 @@ Another difference is that fields of collection types are considered required by [Mutators]: https://docs.rs/typed-builder/latest/typed_builder/derive.TypedBuilder.html#mutators [bon-on]: ../../reference/builder/top-level/on [bon-into]: ../../reference/builder/member/into +[bon-req-to-opt]: ../misc/compatibilitymaking-a-required-member-optional [bs-into]: https://docs.rs/buildstructor/latest/buildstructor/#into-field [db-into]: https://docs.rs/derive_builder/latest/derive_builder/#generic-setters +[db-so]: https://docs.rs/derive_builder/latest/derive_builder/#setters-for-option +[bon-fallible-builder]: ../patterns/fallible-builders +[bs-fall-finish]: https://docs.rs/buildstructor/latest/buildstructor/#fallible +[db-fall-finish]: https://docs.rs/derive_builder/latest/derive_builder/#pre-build-validation +[b]: ../../reference/builder/member/with#fallible-closure +[db-custom-methods]: https://docs.rs/derive_builder/latest/derive_builder/#custom-setters-skip-autogenerated-setters +[db-fs]: https://docs.rs/derive_builder/latest/derive_builder/#fallible-setters [r1]: #special-setter-methods-for-collections -[Typestate API]: ../typestate-api +[bon-ts]: ../typestate-api diff --git a/website/src/guide/patterns/into-conversions-in-depth.md b/website/src/guide/patterns/into-conversions-in-depth.md index 9c10abed..c01668b1 100644 --- a/website/src/guide/patterns/into-conversions-in-depth.md +++ b/website/src/guide/patterns/into-conversions-in-depth.md @@ -251,7 +251,7 @@ connect() .call(); ``` -Notice how we didn't add a type annotation for the variable `ip_addr`. The compiler can deduce (infer) the type of `ip_addr` because it sees that the variable is passed to the `ip_addr()` setter method that expects a parameter of type `IpAddr`. It's a really simple exercise for the compiler in this case because all the context to solve it is there. +Notice how we didn't add a type annotation for the variable `ip_addr`. The compiler can deduce (infer) the type of `ip_addr` because it sees that the variable is passed to the `ip_addr()` setter method that expects a parameter of type `IpAddr`. It's a really simple exercise for the compiler in this case because all the context to do it is there. However, if you use an `Into` conversion, not even Sherlock Holmes can answer the question "What type did you intend to parse?": @@ -288,7 +288,7 @@ help: consider giving `ip_addr` an explicit type // [!co This is because now the `ip_addr` setter looks like this: ```rust ignore -fn ip_addr(self, value: impl Into) -> NextBuilderState { /* */ } +fn ip_addr(self, value: impl Into) -> ConnectBuilder> { /* */ } ``` This signature implies that the `value` parameter can be of any type that implements `Into`. There are several types that implement such a trait. Among them: [`Ipv4Addr`](https://doc.rust-lang.org/stable/std/net/struct.Ipv4Addr.html#impl-From%3CIpv4Addr%3E-for-IpAddr) and [`Ipv6Addr`](https://doc.rust-lang.org/stable/std/net/struct.Ipv6Addr.html#impl-From%3CIpv6Addr%3E-for-IpAddr), and, obviously, `IpAddr` itself (thanks to [this blanket impl](https://github.com/rust-lang/rust/blob/1a94d839be8b248b972b9e022cb940d56de72fa1/library/core/src/convert/mod.rs#L763-L771)). @@ -343,8 +343,8 @@ When we compile this code we get the following error: The problem here is that the compiler doesn't know the complete type of the `None` literal. It definitely knows that it's a value of type `Option<_>`, but it doesn't know what type to use in place of the `_`. There could be many potential candidates for the `_` inside of the `Option<_>`. This is because the signature of the `maybe_member()` setter changed: ```rust ignore -fn maybe_member(self, value: Option) -> NextBuilderState // [!code --] -fn maybe_member(self, value: Option>) -> NextBuilderState // [!code ++] +fn maybe_member(self, value: Option) -> ExampleBuilder> // [!code --] +fn maybe_member(self, value: Option>) -> ExampleBuilder> // [!code ++] ``` Before we enabled `Into` conversions the signature provided a hint for the compiler because the setter expected a single concrete type `Option`, so it was obvious that the `None` literal was of type `Option`. From cb71a44fe8f17687366fc4c6bed78d84e2a4aece Mon Sep 17 00:00:00 2001 From: Veetaha Date: Wed, 6 Nov 2024 00:54:13 +0000 Subject: [PATCH 05/12] Fix doctests --- website/src/guide/misc/alternatives.md | 6 +++--- website/src/guide/typestate-api/builders-type-signature.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/guide/misc/alternatives.md b/website/src/guide/misc/alternatives.md index 4d20d184..278acc3d 100644 --- a/website/src/guide/misc/alternatives.md +++ b/website/src/guide/misc/alternatives.md @@ -44,7 +44,7 @@ With `bon` and `buildstructor` you can simply change the syntax from `#[derive(B For example, suppose one day you found a need to count the number of fields that were default-initialized in the builder. -```rust +```rust ignore use bon::Builder; #[derive(Builder)] @@ -56,7 +56,7 @@ struct Example { x2: u32, #[builder(skip = /* What? How can I calculate this here? ๐Ÿค” */)] - defaults_counter: u32 + defaults_counter: u32, } ``` @@ -76,7 +76,7 @@ use bon::bon; struct Example { x1: u32, x2: u32, - defaults_counter: u32 + defaults_counter: u32, } #[bon] diff --git a/website/src/guide/typestate-api/builders-type-signature.md b/website/src/guide/typestate-api/builders-type-signature.md index 2c9de35c..f759e422 100644 --- a/website/src/guide/typestate-api/builders-type-signature.md +++ b/website/src/guide/typestate-api/builders-type-signature.md @@ -54,7 +54,7 @@ Visibility of the builder's type is by default derived from the visibility of th Here is a simplified view of the generated builder type and state module with their visibility. -```rust +```rust ignore // Let's suppose we had this derive on a struct with `pub` visibility // #[derive(bon::Builder)] pub struct Example { From 67189b2ded93118fae5e36180c860037cc77a54e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Wed, 6 Nov 2024 03:42:01 +0000 Subject: [PATCH 06/12] Add antipatterns --- README.v3.md | 8 +- website/.vitepress/config.mts | 6 + website/src/guide/antipatterns.md | 120 ++++++++++++++++++ website/src/guide/basics.md | 7 + website/src/guide/basics/inspecting.md | 2 - website/src/guide/basics/optional-members.md | 13 +- .../src/guide/basics/positional-members.md | 4 +- website/src/guide/patterns.md | 5 + website/src/guide/typestate-api.md | 2 +- 9 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 website/src/guide/antipatterns.md create mode 100644 website/src/guide/basics.md create mode 100644 website/src/guide/patterns.md diff --git a/README.v3.md b/README.v3.md index 2784bc0e..fb4a4a6b 100644 --- a/README.v3.md +++ b/README.v3.md @@ -75,7 +75,7 @@ assert_eq!(greeting, "Hello Bon! Your level is 24"); Any syntax for functions is supported including `async`, fallible, generic functions, `impl Trait`, etc. -Many things are customizable with additional attributes described in the [API reference](https://bon-rs.com/reference/builder), but let's see what else `bon` offers. +Many things are customizable with additional attributes described in the [API reference](https://bon-rs.com/reference/builder), but let's see what else `bon` has to offer. ## Struct Builder @@ -176,16 +176,14 @@ Methods with or without `self` are both supported. ## No Panics Possible -Builders generated by `bon`'s macros use the typestate pattern to ensure all required parameters are filled, and the same setters aren't called repeatedly to prevent unintentional overwrites. - -If something is wrong, a compile error will be created. No matter how you use the generated builder a panic is never possible. +Builders generated by `bon`'s macros use the typestate pattern to ensure all required parameters are filled, and the same setters aren't called repeatedly to prevent unintentional overwrites. If something is wrong, a compile error will be created. No runtime panics! | โญ Don't forget to give our repo a [star on Github โญ](https://github.com/elastio/bon)! | | --------------------------------------------------------------------------------------- | ## What's Next? -What you've seen above is the first page of the ๐Ÿ“– [Guide Book](https://bon-rs.com/guide/overview). Consider reading the `Basics` section. Begin [here](https://bon-rs.com/guide/basics/optional-members) with the optional/default values topic. Remember, knowledge is power ๐Ÿฑ! +What you've seen above is the first page of the ๐Ÿ“– Guide Book. If you want to learn more, jump to the [Basics](https://bon-rs.com/guide/basics) section. And remember: knowledge is power ๐Ÿฑ! Feel free to jump to code and use the `#[builder]` and `#[derive(Builder)]` once you've seen enough docs to get started. diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index 487e1bb4..a5110bbf 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -132,6 +132,7 @@ export default defineConfig({ }, { text: "Basics", + link: "/guide/basics", items: [ { text: "Optional Members", @@ -175,6 +176,7 @@ export default defineConfig({ }, { text: "Patterns", + link: "/guide/patterns", items: [ { text: "Conditional Building", @@ -194,6 +196,10 @@ export default defineConfig({ }, ], }, + { + text: "Antipatterns", + link: "/guide/antipatterns", + }, { text: "Misc", items: [ diff --git a/website/src/guide/antipatterns.md b/website/src/guide/antipatterns.md new file mode 100644 index 00000000..b69f8c17 --- /dev/null +++ b/website/src/guide/antipatterns.md @@ -0,0 +1,120 @@ +# Antipatterns + +While [Patterns](./patterns) teach you how to use `bon`, here we'll discuss how **not to use** `bon`. + +## Generic Types in Optional Members + +Generic type parameters, `impl Trait` or const generics used exclusively in optional members break type inference. This is mostly relevant to `fn`-based builders. + +This problem becomes visible when you skip setting an optional member. + +### ๐Ÿ”ด Bad + +```rust +#[bon::builder] +fn bad>(x1: Option) { + let x1 = x1.map(Into::into); + // ... +} + +// This compiles +bad().x1("&str").call(); + +// This doesn't +bad().call(); // error[E0283]: type annotations needed // [!code error] +``` + +The compilation error here is: + +```rust ignore +bad().call(); +^^^ cannot infer type of the type parameter `T` declared on the function `bad` +``` + +A similar error would be generated if we used `Option>`, although the error would reference a generic parameter [auto-generated](./typestate-api/builders-type-signature#other-generic-parameters) for the function by the builder macro. For simplicity, we'll use a named generic parameter throughout the examples. + +The caller of your builder would need to work around this problem by specifying the type `T` explicitly via turbofish: + +```rust ignore +// Both String or `&str` would work as a type hint +bad::().call(); +``` + +This is inconvenient, don't do this. + +### โœ… Good + +Instead, make the member's type non-generic. Move generics to the setter methods' signature. It's easier to understand what it means with an example. + +For the case above, the good solution will be [`#[builder(into)]`](../reference/builder/member/into). + +```rust +#[bon::builder] +fn good(#[builder(into)] x1: Option) { + // ... +} + +good().x1("&str").call(); +good().call(); +``` + +How `#[builder(into)]` is different from `Option` (`T: Into`)? +Let's compare the generated code between them (simplified). Switch between the tabs below: + +::: code-group + +```rust [#[builder(into)]] +fn good() -> GoodBuilder { /**/ } + +impl GoodBuilder { + fn x1(self, value: impl Into) -> GoodBuilder> { + GoodBuilder { /* other fields */, __x1: value.into() } + } +} +``` + + + +```rust [Option<T>] +fn bad() -> BadBuilder { /**/ } + +impl, S: State> BadBuilder { + fn x1(self, value: T) -> BadBuilder> { + BadBuilder { /* other fields */, __x1: value } + } +} +``` + +::: + +Notice how in the good example of `#[builder(into)]` the starting function `good()` doesn't declare any generic parameters, while in the `Option` example `bad()` does have a generic parameter `T`. + +Also, in the case of `#[builder(into)]` the call to `.into()` happens inside of the setter method itself (early). In the case of `Option`, the call to `.into()` is deferred to the finishing function. + +This is also visible when you compare the original functions again. Notice how in the `Bad` example, we have to manually call `x1.map(Into::into)`, while in `Good` we already have a concrete `Option` type: + +::: code-group + +```rust [Good] +#[bon::builder] +fn good(#[builder(into)] x1: Option) { + // ... +} +``` + +```rust [Bad] +#[bon::builder] +fn bad>(x1: Option) { + let x1 = x1.map(Into::into); + // ... +} +``` + +::: + +So, the builder has to carry the generic parameter `T` in its type for you to be able to use that type in your function body merely to invoke an `Into::into` on a parameter. Instead, strive to do such conversions in setters. + +If you are doing a conversion with something other than `Into`, then use [`#[builder(with)]`](../reference/builder/member/with) to apply the same technique for getting rid of generic parameters _early_. diff --git a/website/src/guide/basics.md b/website/src/guide/basics.md new file mode 100644 index 00000000..362eacb9 --- /dev/null +++ b/website/src/guide/basics.md @@ -0,0 +1,7 @@ +![bon-logo](/bon-home.png) + +Welcome to the Bon Guide Book ๐Ÿฌ! It's meant to walk you through `bon` so you don't get lost while exploring it ๐Ÿˆ. + +Code examples may use either `fn` or `struct` builder syntax interchangeably. Both follow the same principles unless stated otherwise. + +Now, let's begin with the first topic of [Optional Members](./basics/optional-members). diff --git a/website/src/guide/basics/inspecting.md b/website/src/guide/basics/inspecting.md index 938a93ec..4eeb076e 100644 --- a/website/src/guide/basics/inspecting.md +++ b/website/src/guide/basics/inspecting.md @@ -2,8 +2,6 @@ If you want to inspect the values set in the builder for debugging purposes you can leverage the [`#[builder(derive(...))]`](../../reference/builder/top-level/derive) attribute to derive the [`Debug`](https://doc.rust-lang.org/stable/std/fmt/trait.Debug.html) trait for your builder. -**Example:** - ```rust use bon::builder; diff --git a/website/src/guide/basics/optional-members.md b/website/src/guide/basics/optional-members.md index d62d100b..a01fb376 100644 --- a/website/src/guide/basics/optional-members.md +++ b/website/src/guide/basics/optional-members.md @@ -29,21 +29,20 @@ The builder provides a **pair** of setters for each optional member: [setters]: ../../reference/builder/member/setters -::: details See how the setters look in the generated code +This is how setters look in the generated code for the example above (simplified): ```rust ignore -// [GENERATED CODE (simplified)] -impl ExampleBuilder { +impl ExampleBuilder { fn level(self, value: u32) -> ExampleBuilder> { self.maybe_level(Some(value)) // Yes, it's this simple! } - fn maybe_level(self, value: Option) -> ExampleBuilder> { /* */ } + fn maybe_level(self, value: Option) -> ExampleBuilder> { + /* */ + } } ``` -::: - Thanks to this design, changing the member from required to optional [preserves compatibility](../misc/compatibility#making-a-required-member-optional). ### Examples @@ -112,4 +111,4 @@ You can also reference other members in the default expression. See [`#[builder( ## Conditional building -Now that you know how optional members work you can check out the [Conditional building](../patterns/conditional-building) design patterns or continue studying other features of `bon` by following the "Next page" link at the bottom. +Now that you know how optional members work you can check out the [Conditional Building](../patterns/conditional-building) design patterns or continue studying other features of `bon` by following the "Next page" link at the bottom. diff --git a/website/src/guide/basics/positional-members.md b/website/src/guide/basics/positional-members.md index a44741a6..3b499e7f 100644 --- a/website/src/guide/basics/positional-members.md +++ b/website/src/guide/basics/positional-members.md @@ -10,7 +10,7 @@ Use `#[builder(start_fn)]` to move some members to the parameters of the startin use bon::Builder; #[derive(Builder)] -// Top-level attribute to give a better name for the starting function // [!code highlight] +// Top-level attribute to give a custom name for the starting function // [!code highlight] #[builder(start_fn = with_coordinates)] // [!code highlight] struct Treasure { // Member-level attributes to move members // [!code highlight] @@ -48,7 +48,7 @@ Use `#[builder(finish_fn)]` to move some members to the parameters of the finish use bon::Builder; #[derive(Builder)] -// Top-level attribute to give a better name for the finishing function // [!code highlight] +// Top-level attribute to give a custom name for the finishing function // [!code highlight] #[builder(finish_fn = located_at)] // [!code highlight] struct Treasure { // Member-level attributes to move members // [!code highlight] diff --git a/website/src/guide/patterns.md b/website/src/guide/patterns.md new file mode 100644 index 00000000..3b9b80f1 --- /dev/null +++ b/website/src/guide/patterns.md @@ -0,0 +1,5 @@ +# Patterns + +This section teaches the usage patterns of _idiomatic_ `bon`. It provides examples of problems that you can solve with the primitives that you've learned in the `Basics` section. + +Now let's start thinking with builders ๐Ÿ’ก. diff --git a/website/src/guide/typestate-api.md b/website/src/guide/typestate-api.md index 5e1d2c61..4bd7ae9a 100644 --- a/website/src/guide/typestate-api.md +++ b/website/src/guide/typestate-api.md @@ -18,7 +18,7 @@ Here are some example use cases when you'd need to interact with the typestate A - is `unsafe` or `async` - declares its own named generic parameters - sets multiple members at once - - ~~Cooks you a dinner ๐Ÿ~~ + - ~~cooks you a dinner ๐Ÿ~~ Just kidding ๐Ÿ˜ธ, the Typestate API can't cook you a dinner ๐Ÿ˜ณ, but **you** can definitely cook something cool using it ๐Ÿ’ช! From 1baa796952dd14695ac369f20c610895bab12ce1 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Wed, 6 Nov 2024 18:46:57 +0000 Subject: [PATCH 07/12] Small fixes --- README.v3.md | 2 +- e2e-tests/build.rs | 2 +- website/src/guide/antipatterns.md | 22 ++++++++++---------- website/src/reference/builder/member/into.md | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.v3.md b/README.v3.md index fb4a4a6b..f88bba0a 100644 --- a/README.v3.md +++ b/README.v3.md @@ -176,7 +176,7 @@ Methods with or without `self` are both supported. ## No Panics Possible -Builders generated by `bon`'s macros use the typestate pattern to ensure all required parameters are filled, and the same setters aren't called repeatedly to prevent unintentional overwrites. If something is wrong, a compile error will be created. No runtime panics! +Builders generated by `bon`'s macros use the typestate pattern to ensure all required parameters are filled, and the same setters aren't called repeatedly to prevent unintentional overwrites. If something is wrong, a compile error will be created. | โญ Don't forget to give our repo a [star on Github โญ](https://github.com/elastio/bon)! | | --------------------------------------------------------------------------------------- | diff --git a/e2e-tests/build.rs b/e2e-tests/build.rs index 8f3ef86b..1e7a3144 100644 --- a/e2e-tests/build.rs +++ b/e2e-tests/build.rs @@ -56,7 +56,7 @@ fn main() { .join("\n"); format!( - "#[doc = r###\"{doc}\"###] \ + "#[doc = r#####\"{doc}\"#####] \ mod {test_name} {{}}" ) }) diff --git a/website/src/guide/antipatterns.md b/website/src/guide/antipatterns.md index b69f8c17..99fbcf94 100644 --- a/website/src/guide/antipatterns.md +++ b/website/src/guide/antipatterns.md @@ -10,7 +10,7 @@ This problem becomes visible when you skip setting an optional member. ### ๐Ÿ”ด Bad -```rust +```rust compile_fail #[bon::builder] fn bad>(x1: Option) { let x1 = x1.map(Into::into); @@ -44,9 +44,9 @@ This is inconvenient, don't do this. ### โœ… Good -Instead, make the member's type non-generic. Move generics to the setter methods' signature. It's easier to understand what it means with an example. +Instead, make the member's type non-generic and move generics to the setter methods' signature. Let's what it means with an example. -For the case above, the good solution will be [`#[builder(into)]`](../reference/builder/member/into). +For the case above, the good solution is [`#[builder(into)]`](../reference/builder/member/into). ```rust #[bon::builder] @@ -63,13 +63,13 @@ Let's compare the generated code between them (simplified). Switch between the t ::: code-group -```rust [#[builder(into)]] +```rust ignore [builder(into)] fn good() -> GoodBuilder { /**/ } impl GoodBuilder { fn x1(self, value: impl Into) -> GoodBuilder> { GoodBuilder { /* other fields */, __x1: value.into() } - } + } } ``` @@ -78,23 +78,23 @@ Prettier tries to replace < with a <, but it is intentionally there, because otherwise, Vue thinks as if we are trying to write an HTML tag e.g. --> -```rust [Option<T>] +```rust ignore [Option<T>] fn bad() -> BadBuilder { /**/ } impl, S: State> BadBuilder { fn x1(self, value: T) -> BadBuilder> { BadBuilder { /* other fields */, __x1: value } - } + } } ``` ::: -Notice how in the good example of `#[builder(into)]` the starting function `good()` doesn't declare any generic parameters, while in the `Option` example `bad()` does have a generic parameter `T`. +Notice how in the `builder(into)` example the starting function `good()` doesn't declare any generic parameters, while in the `Option` example `bad()` does have a generic parameter `T`. -Also, in the case of `#[builder(into)]` the call to `.into()` happens inside of the setter method itself (early). In the case of `Option`, the call to `.into()` is deferred to the finishing function. +Also, in the case of `builder(into)` the call to `.into()` happens inside of the setter method itself (early). In the case of `Option`, the call to `.into()` is deferred to the finishing function. -This is also visible when you compare the original functions again. Notice how in the `Bad` example, we have to manually call `x1.map(Into::into)`, while in `Good` we already have a concrete `Option` type: +This is also visible when you compare the original functions again. Notice how in the `Good` example we already have a concrete `Option` type while in the `Bad` example, we have to manually call `x1.map(Into::into)`: ::: code-group @@ -115,6 +115,6 @@ fn bad>(x1: Option) { ::: -So, the builder has to carry the generic parameter `T` in its type for you to be able to use that type in your function body merely to invoke an `Into::into` on a parameter. Instead, strive to do such conversions in setters. +So, the builder has to carry the generic parameter `T` in its type for you to be able to use that type in your function body to merely invoke `Into::into` on a parameter. Instead, strive to do such conversions in setters. If you are doing a conversion with something other than `Into`, then use [`#[builder(with)]`](../reference/builder/member/with) to apply the same technique for getting rid of generic parameters _early_. diff --git a/website/src/reference/builder/member/into.md b/website/src/reference/builder/member/into.md index 56ac8beb..131356fb 100644 --- a/website/src/reference/builder/member/into.md +++ b/website/src/reference/builder/member/into.md @@ -8,7 +8,7 @@ This attribute is also configurable via the top-level [`#[builder(on(...))]`](.. ::: -Changes the signature of the setters to accept [`impl Into`](https://doc.rust-lang.org/stable/std/convert/trait.Into.html), where `T` is the type of the member. +Changes the signature of the setters to accept [`impl Into`](https://doc.rust-lang.org/stable/std/convert/trait.Into.html), where `T` is the underlying type of the member. For [optional members](../../../guide/basics/optional-members), the `maybe_{member}()` setter method will accept an `Option>` type instead of just `Option`. From 7d31cf10ebdc14f92c159658de8b17a8f8e95a1c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Thu, 7 Nov 2024 13:55:54 +0000 Subject: [PATCH 08/12] Some more improvements --- README.v3.md | 12 +-- website/src/guide/misc/alternatives.md | 102 +++++++++++++++++++------ 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/README.v3.md b/README.v3.md index f88bba0a..b1056d94 100644 --- a/README.v3.md +++ b/README.v3.md @@ -200,6 +200,12 @@ bon = "2.3" You can opt out of `std` and `alloc` cargo features with `default-features = false` for `no_std` environments. +## Acknowledgments + +This project was heavily inspired by such awesome crates as [`buildstructor`](https://docs.rs/buildstructor), [`typed-builder`](https://docs.rs/typed-builder) and [`derive_builder`](https://docs.rs/derive_builder). This crate was designed with many lessons learned from them. + +See [alternatives](https://bon-rs.com/guide/misc/alternatives) for comparison. + ## Getting Help If you can't figure something out, consult the docs and maybe use the `๐Ÿ” Search` bar on our [docs website](https://bon-rs.com). You may also create an issue or a discussion on the [Github repository](https://github.com/elastio/bon) for help or write us a message on [Discord](https://bon-rs.com/discord). @@ -219,12 +225,6 @@ If you can't figure something out, consult the docs and maybe use the `๐Ÿ” Sear -## Acknowledgments - -This project was heavily inspired by such awesome crates as [`buildstructor`](https://docs.rs/buildstructor), [`typed-builder`](https://docs.rs/typed-builder) and [`derive_builder`](https://docs.rs/derive_builder). This crate was designed with many lessons learned from them. - -See [alternatives](https://bon-rs.com/guide/misc/alternatives) for comparison. - ## License diff --git a/website/src/guide/misc/alternatives.md b/website/src/guide/misc/alternatives.md index 278acc3d..408adf54 100644 --- a/website/src/guide/misc/alternatives.md +++ b/website/src/guide/misc/alternatives.md @@ -38,66 +38,118 @@ There are several other existing alternative crates for generating builders. `bo ## Function Builder Paradigm Shift -If you ever hit a wall with `typed-builder` or `derive_builder`, you have no other choice but to hack something around their derive attributes syntax on a struct. +If you ever hit a wall ๐Ÿงฑ with `typed-builder` or `derive_builder`, you'll have to hack something around their derive attributes syntax on a struct. With `bon` or `buildstructor` you can simply change the syntax from `#[derive(Builder)]` on a struct to a `#[builder]` on a function to gain more flexibility at any time ๐Ÿคธ. It is [guaranteed to preserve compatibility](../misc/compatibility#switching-between-derivebuilder-and-builder-on-the-new-method) (not a breaking change). -With `bon` and `buildstructor` you can simply change the syntax from `#[derive(Builder)]` on a struct to a `#[builder]` on a function, which gives you much more flexibility. It is [guaranteed to preserve compatibility](../misc/compatibility#switching-between-derivebuilder-and-builder-on-the-new-method) (non a breaking change). It allows you to create fallible, `async` or even `unsafe` builders naturally. +### Example -For example, suppose one day you found a need to count the number of fields that were default-initialized in the builder. +Suppose you already had a struct like the following with a builder derive: ```rust ignore use bon::Builder; #[derive(Builder)] -struct Example { - #[builder(default)] +pub struct Segment { x1: u32, + y1: u32, - #[builder(default)] x2: u32, + y2: u32, +} + +// Suppose this is your users' code +Segment::builder().x1(1).y1(2).x2(3).y2(4).build(); +``` + +Then you decided to refactor ๐Ÿงน your struct's internal representation by extracting a private utility `Point` type: + +```rust +use bon::Builder; - #[builder(skip = /* What? How can I calculate this here? ๐Ÿค” */)] - defaults_counter: u32, +#[derive(Builder)] +pub struct Segment { + point1: Point, + point2: Point, +} + +// Private +struct Point { + x: u32, + y: u32, } + +// Suppose this is your users' code (it no longer compiles) +Segment::builder().x1(1).y1(2).x2(3).y2(4).build(); // [!code error] +// ^^- error[E0599]: no method named `x1` found for struct `SegmentBuilder` // [!code error] +// available methods: `point1(Point)`, `point2(Point)` // [!code error] ``` +There are two problems with `#[derive(Builder)]` syntax in this case: + +1. This refactoring becomes a breaking change to `Segment`'s builder API ๐Ÿ˜ข. +2. The private utility `Point` type leaks through the builder API via `point1`, `point2` setters ๐Ÿ˜ญ. + +The fundamental problem is that the builder's API is _coupled_ โ›“๏ธ with your struct's internal representation. It's literally `derive`d from the fields of your struct. + +### Suffering + +If you were using `typed-builder` or `derive_builder`, you'd be stuck for a while trying to find the magical ๐Ÿช„ combination of attributes that would let you do this change without breaking compatibility or leakage of the private `Point` type. + +With no solution in sight ๐Ÿ˜ฎโ€๐Ÿ’จ, you'd then fall back to writing the same builder manually. You'd probably expand the builder derive macro and edit the generated code directly, which, ugh... hurts ๐Ÿค•. + +However, that would be especially painful with `typed-builder`, which generates a complex typestate that is not human-readable and maintainable enough by hand. It also references some internal `#[doc(hidden)]` symbols from the `typed-builder` crate. Achoo... ๐Ÿคง. + ::: tip -The attribute [`#[builder(skip)]`](../../reference/builder/member/skip) skips generating setters for a member. The field is initialized with the given expression instead. +In contrast, `bon`'s type state **is** human-readable, maintainable, and [documented](../typestate-api) ๐Ÿ‘ ::: -The attribute [`#[builder(skip)]`](../../reference/builder/member/skip) is the first obvious candidate for this use case. It also has analogues in `typed-builder` and `derive_builder`. However, it's actually too limited for this use case, because it doesn't have the required context. +### Behold the Function-Based Builder -At this point, you'd probably give up on `typed-builder` and `derive_builder`, because there is no way to express the required behavior with their attributes' syntax. However, it's as simple as pie with `bon` or `buildstructor`: +This change is as simple as pie ๐Ÿฅง with `bon` or `buildstructor`. The code speaks for itself: ```rust use bon::bon; -struct Example { - x1: u32, - x2: u32, - defaults_counter: u32, +// No more derives on a struct. Its internal representation is decoupled from the builder. +pub struct Example { + point1: Point, + point2: Point, +} + +struct Point { + x: u32, + y: u32, } #[bon] impl Example { #[builder] - fn new(x1: Option, x2: Option) -> Self { - let mut defaults_counter = 0; - let x1 = x1.unwrap_or_else(|| { defaults_counter += 1; 0 }); - let x2 = x2.unwrap_or_else(|| { defaults_counter += 1; 0 }); - Self { x1, x2, defaults_counter } + fn new(x1: u32, y1: u32, x2: u32, y2: u32) -> Self { + Self { + point1: Point { x: x1, y: y1 } , + point2: Point { x: x2, y: y2 } , + } } } -assert_eq!(Example::builder().build().defaults_counter, 2); -assert_eq!(Example::builder().x1(1).build().defaults_counter, 1); -assert_eq!(Example::builder().x1(1).x2(2).build().defaults_counter, 0); +// Suppose this is your users' code (it compiles after this change, yay ๐ŸŽ‰!) +Segment::builder().x1(1).y1(2).x2(3).y2(4).build(); ``` -Ah... Simple just like regular Rust, isn't it? ๐Ÿ˜Œ +Ah... Isn't this just so simple and beautiful? ๐Ÿ˜Œ The fun part is that the constructor method `new` that we originally abandoned comes back to heroically save us โ›‘๏ธ at no cost, other than a star โญ on `bon`'s [Github repo](https://github.com/elastio/bon) maybe ๐Ÿˆ? + +And you know what, our old friend `new` doesn't feel offended for being abandoned. It doesn't even feel emotions, actually ๐Ÿ—ฟ. But it's happy to help you ๐Ÿซ‚. + +Moreover, it offers you a completely new dimension of flexibility: + +- Need some validation? Just make the `new()` method return a `Result`. The generated `build()` method will then become fallible. +- Need to do an `async` operation in the constructor? Just make your constructor `async` and your `build()` will return a `Future`. +- Need some adrenaline ๐Ÿ’‰? Just add `unsafe`, and... you get the idea ๐Ÿ˜‰. + +### Summary -The chances of hitting a wall with function builders are close to zero. Even if you ever hit the wall with function builders you still have access to the [Typestate API](../typestate-api/) in `bon` for even more flexibility. +The chances of hitting a wall with function builders are close to zero, and even if you ever do, you still have access to the [Typestate API](../typestate-api/) in `bon` for even more flexibility ๐Ÿ’ช. ## Special setter methods for collections From 77c3e825398764669020d93597685fb1dac582c1 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Thu, 7 Nov 2024 22:44:53 +0000 Subject: [PATCH 09/12] Update runtime benchmarks --- benchmarks/runtime/README.md | 4 +- benchmarks/runtime/run.sh | 25 ++++----- benchmarks/runtime/src/args_10.rs | 2 +- benchmarks/runtime/src/args_10_alloc.rs | 2 +- benchmarks/runtime/src/args_10_structs.rs | 2 +- benchmarks/runtime/src/args_20.rs | 2 +- benchmarks/runtime/src/args_3.rs | 2 +- benchmarks/runtime/src/args_5.rs | 2 +- benchmarks/runtime/src/lib.rs | 10 +++- bon/src/__/mod.rs | 12 ----- website/src/guide/misc/benchmarks.md | 66 ++++++++++++++++------- 11 files changed, 76 insertions(+), 53 deletions(-) diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index e1637cc0..9185d64e 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -18,10 +18,12 @@ sudo apt install valgrind ## Running the benchmarks -Once you have all the [dependencies](#dependencies) installed you can run the selected benchmark from this directory like this: +Once you have all the [dependencies](#dependencies) installed you can run the selected benchmarks from this directory like this: ```bash ./run.sh {benchmark_name} ``` The `{benchmark_name}` corresponds to the modules in this crate. The number in the benchmark name represents the number of arguments that are passed to the function. + +If you don't pass any parameter to `run.sh`, it will run all benchmarks. diff --git a/benchmarks/runtime/run.sh b/benchmarks/runtime/run.sh index 0d33461d..88838f43 100755 --- a/benchmarks/runtime/run.sh +++ b/benchmarks/runtime/run.sh @@ -2,21 +2,22 @@ set -euxo pipefail -bench=${1:-args_10_structs} +# List src/ dir, remove `.rs` from file extensions and remove lib.rs +benches=${1:-$(find src -name '*.rs' | sed 's/\.rs$//' | sed 's/src\///' | grep -v lib)} export CARGO_INCREMENTAL=0 -cargo clean +for bench in $benches; do + cargo build --features "$bench" --release -p runtime-benchmarks -cargo build --features "$bench" --release -p runtime-benchmarks + # If vscode is present, show diff: + if command -v code; then + cargo asm --features "$bench" --no-color "runtime_benchmarks::$bench::builder_bench" > builder.dbg.s || true + cargo asm --features "$bench" --no-color "runtime_benchmarks::$bench::regular_bench" > regular.dbg.s || true -cargo asm --features "$bench" --no-color "runtime_benchmarks::$bench::builder_bench" > builder.dbg.s || true -cargo asm --features "$bench" --no-color "runtime_benchmarks::$bench::regular_bench" > regular.dbg.s || true + code --diff regular.dbg.s builder.dbg.s + fi -# If vscode is present, show diff: -if command -v code; then - code --diff regular.dbg.s builder.dbg.s -fi - -cargo bench --features "$bench" -p runtime-benchmarks --profile release --bench iai -cargo bench --features "$bench" -p runtime-benchmarks --profile release --bench criterion + cargo bench --features "$bench" -p runtime-benchmarks --profile release --bench iai + cargo bench --features "$bench" -p runtime-benchmarks --profile release --bench criterion +done diff --git a/benchmarks/runtime/src/args_10.rs b/benchmarks/runtime/src/args_10.rs index d7188771..04269752 100644 --- a/benchmarks/runtime/src/args_10.rs +++ b/benchmarks/runtime/src/args_10.rs @@ -46,7 +46,7 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(start_fn = builder)] +#[builder(crate = crate::bon, start_fn = builder)] fn regular( arg1: &str, arg2: u32, diff --git a/benchmarks/runtime/src/args_10_alloc.rs b/benchmarks/runtime/src/args_10_alloc.rs index c43d91c6..752b32ed 100644 --- a/benchmarks/runtime/src/args_10_alloc.rs +++ b/benchmarks/runtime/src/args_10_alloc.rs @@ -46,7 +46,7 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(start_fn = builder)] +#[builder(crate = crate::bon, start_fn = builder)] fn regular( arg1: String, arg2: u32, diff --git a/benchmarks/runtime/src/args_10_structs.rs b/benchmarks/runtime/src/args_10_structs.rs index a98b3f64..c04e5234 100644 --- a/benchmarks/runtime/src/args_10_structs.rs +++ b/benchmarks/runtime/src/args_10_structs.rs @@ -87,7 +87,7 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(start_fn = builder)] +#[builder(crate = crate::bon, start_fn = builder)] fn regular( arg1: Point3D, arg2: u32, diff --git a/benchmarks/runtime/src/args_20.rs b/benchmarks/runtime/src/args_20.rs index d87c6d29..dfd0a06b 100644 --- a/benchmarks/runtime/src/args_20.rs +++ b/benchmarks/runtime/src/args_20.rs @@ -121,7 +121,7 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(start_fn = builder)] +#[builder(crate = crate::bon, start_fn = builder)] fn regular( arg1: &str, arg2: u32, diff --git a/benchmarks/runtime/src/args_3.rs b/benchmarks/runtime/src/args_3.rs index da24e90e..1029d73c 100644 --- a/benchmarks/runtime/src/args_3.rs +++ b/benchmarks/runtime/src/args_3.rs @@ -13,7 +13,7 @@ pub fn builder_bench() -> u32 { builder().arg1(arg1).arg2(arg2).maybe_arg3(arg3).call() } -#[builder(start_fn = builder)] +#[builder(crate = crate::bon, start_fn = builder)] fn regular(arg1: &str, arg2: u32, arg3: Option<&str>) -> u32 { let x = arg1.parse::().unwrap() + arg2; let x = x + arg3.map(|x| x.parse::().unwrap()).unwrap_or(0); diff --git a/benchmarks/runtime/src/args_5.rs b/benchmarks/runtime/src/args_5.rs index d7af533d..7abb166c 100644 --- a/benchmarks/runtime/src/args_5.rs +++ b/benchmarks/runtime/src/args_5.rs @@ -19,7 +19,7 @@ pub fn builder_bench() -> u32 { .call() } -#[builder(start_fn = builder)] +#[builder(crate = crate::bon, start_fn = builder)] fn regular(arg1: &str, arg2: u32, arg3: bool, arg4: Option<&str>, arg5: Option) -> u32 { let x = arg1.parse::().unwrap() + arg2; let x = x + u32::from(arg3); diff --git a/benchmarks/runtime/src/lib.rs b/benchmarks/runtime/src/lib.rs index 0c474fba..c740fe72 100644 --- a/benchmarks/runtime/src/lib.rs +++ b/benchmarks/runtime/src/lib.rs @@ -12,10 +12,16 @@ rustdoc::missing_crate_level_docs )] +// This reexport makes it easier to prepare the ASM comparison since the +// generated code refers to a local modules that can be manually replaced +// on godbolt to avoid referencing a 3rd party crate. +#[allow(clippy::single_component_path_imports)] +use bon; + cfg_if::cfg_if! { if #[cfg(feature = "args_3")] { - pub mod args_3; - pub use args_3 as bench; + pub mod args_20; + pub use args_20 as bench; } else if #[cfg(feature = "args_5")] { pub mod args_5; pub use args_5 as bench; diff --git a/bon/src/__/mod.rs b/bon/src/__/mod.rs index 061b11bc..14e28cfd 100644 --- a/bon/src/__/mod.rs +++ b/bon/src/__/mod.rs @@ -43,15 +43,3 @@ pub struct Unset(Name); #[derive(Debug)] pub struct Set(Name); - -#[rustversion::attr( - since(1.78.0), - diagnostic::on_unimplemented( - message = "expected type state for the member `{Name}`, but got `{Self}`", - label = "expected type state for the member `{Name}`, but got `{Self}`", - ) -)] -pub trait MemberState: Sealed {} - -impl MemberState for Unset {} -impl MemberState for Set {} diff --git a/website/src/guide/misc/benchmarks.md b/website/src/guide/misc/benchmarks.md index fa9aa9c3..ff381fc6 100644 --- a/website/src/guide/misc/benchmarks.md +++ b/website/src/guide/misc/benchmarks.md @@ -1,6 +1,6 @@ # Benchmarks -`#[builder]` generates code that is easily optimizable by the compiler. This has been tested by the benchmarks below. The benchmarks compare regular positional function call syntax and builder syntax for functions annotated with `#[builder]`. +Builder macros generate code that is easily optimizable by the compiler. This has been tested by the benchmarks below. The benchmarks compare regular positional function call syntax and builder syntax for functions annotated with `#[builder]`. In many cases `rustc` generates the same assembly code for the builder syntax as it would for a regular function call. Even when the generated assembly differs, the performance differences are negligible. @@ -10,27 +10,51 @@ Don't take these microbenchmarks for granted. Do your own performance measuremen ::: -## Wallclock statistics + + -| Benchmark | Description | Assembly output | Run time | -| ----------------- | --------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------- | -| `args_3` | 3 args of primitive types | [Equal](https://godbolt.org/z/YbTc4xGGY) | regular: `6.6536ns`
builder: `6.6494ns` | -| `args_5` | 5 args of primitive types | [Equal](https://godbolt.org/z/TM3E7M6b3) | regular: `7.9592ns`
builder: `7.9731ns` | -| `args_10` | 10 args of primitive types | [Ordering diff](https://godbolt.org/z/1d1fa38co) | regular: `18.082ns`
builder: `18.217ns` | -| `args_10_structs` | 10 args of primitive types and structs | [Equal](https://godbolt.org/z/d6nn16E8q) | regular: `9.2492ns`
builder: `9.2325ns` | -| `args_10_alloc` | 10 args of primitive and heap-allocated types | [Instructions diff](https://godbolt.org/z/fEMvnWvbc) | regular: `86.090ns`
builder: `86.790ns` | -| `args_20` | 20 args of primitive types | [Ordering diff](https://godbolt.org/z/3czM3h68s) | regular: `36.121ns`
builder: `36.298ns` | +## Wallclock Statistics -## High-precision statistics +
-| Benchmark | Instructions count | L1 accesses | L2 accesses | RAM accesses | -| ----------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------- | ----------------------------------------- | -| `args_3` | regular: `108`
builder: `108` | regular: `138`
builder: `138` | regular: `2`
builder: `2` | regular: `4`
builder: `4` | -| `args_5` | regular: `126`
builder: `126` | regular: `161`
builder: `161` | regular: `2`
builder: `2` | regular: `10`
builder: `10` | -| `args_10` | regular: `281`
builder: `281` | regular: `381`
builder: `380` | regular: `2`
builder: `2` | regular: `19`
builder: `20` | -| `args_10_structs` | regular: `75`
builder: `75` | regular: `106`
builder: `106` | regular: `4`
builder: `4` | regular: `12`
builder: `12` | -| `args_10_alloc` | regular: `2028`
builder: `2027` | regular: `2824`
builder: `2824` | regular: `3`
builder: `2` | regular: `36`
builder: `36` | -| `args_20` | regular: `556`
builder: `556` | regular: `767`
builder: `767` | regular: `4`
builder: `4` | regular: `36`
builder: `36` | +| Benchmark | Description | Assembly output | Run time | +| ----------------- | --------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------ | +| `args_3` | 3 args of primitive types | [Equal](https://godbolt.org/z/xKvqr35TM) | regular: `6.2751ns`
builder: `6.3021ns` | +| `args_5` | 5 args of primitive types | [Equal](https://godbolt.org/z/oPc35ees5) | regular: `7.8298ns`
builder: `7.8321ns` | +| `args_10` | 10 args of primitive types | [Ordering diff](https://godbolt.org/z/Ys9EszPTv) | regular: `17.322ns`
builder: `17.178ns` | +| `args_10_structs` | 10 args of primitive types and structs | [Instructions diff](https://godbolt.org/z/YxjdGMncs) | regular: `2.7477ns`
builder: `2.7311ns` | +| `args_10_alloc` | 10 args of primitive and heap-allocated types | [Instructions diff](https://godbolt.org/z/chdnTYdqh) | regular: `91.666ns`
builder: `84.818ns` (\*) | +| `args_20` | 20 args of primitive types | [Equal](https://godbolt.org/z/13ncxPT5s) | regular: `36.467ns`
builder: `36.786ns` | + +
+ +::: tip (\*) + +Interestingly, in this case builder version performed even better. If you don't believe this, you can run these benchmarks for [yourself][benchmarks-source]. Maybe some ASM expert could explain this ๐Ÿ˜ณ? + +::: + +## High-Precision Statistics + +
+ +| Benchmark | Instructions count | L1 accesses | L2 accesses | RAM accesses | +| ----------------- | ----------------------------------- | ----------------------------------- | ----------------------------- | ------------------------------- | +| `args_3` | regular: `107`
builder: `107` | regular: `134`
builder: `134` | regular: `1`
builder: `1` | regular: `8`
builder: `8` | +| `args_5` | regular: `125`
builder: `125` | regular: `164`
builder: `164` | regular: `1`
builder: `1` | regular: `7`
builder: `7` | +| `args_10` | regular: `283`
builder: `283` | regular: `382`
builder: `383` | regular: `4`
builder: `2` | regular: `18`
builder: `19` | +| `args_10_structs` | regular: `22`
builder: `22` | regular: `30`
builder: `31` | regular: `2`
builder: `1` | regular: `5`
builder: `5` | +| `args_10_alloc` | regular: `2038`
builder: `2037` | regular: `2839`
builder: `2837` | regular: `1`
builder: `1` | regular: `33`
builder: `34` | +| `args_20` | regular: `557`
builder: `557` | regular: `775`
builder: `775` | regular: `1`
builder: `1` | regular: `32`
builder: `32` | + +
## Conditions @@ -46,4 +70,6 @@ The benchmarks were run on a dedicated root server `AX51-NVMe` on [Hetzner](http ## References -The source code of the benchmarks is [available here](https://github.com/elastio/bon/tree/master/benchmarks). +The source code of the benchmarks is [available here][benchmarks-source]. + +[benchmarks-source]: https://github.com/elastio/bon/tree/master/benchmarks/runtime From 167bbbf3138e708b84fc38099fc26cd1f4f9fa28 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 8 Nov 2024 00:00:39 +0000 Subject: [PATCH 10/12] Reorganize a bit --- README.v3.md | 4 +- website/.vitepress/config.mts | 58 +++++++++------- website/src/blog/bon-builder-v2-3-release.md | 2 +- ...-to-do-named-function-arguments-in-rust.md | 2 +- website/src/guide/{misc => }/alternatives.md | 68 +++++++++++-------- .../guide/{misc => basics}/compatibility.md | 0 website/src/guide/basics/optional-members.md | 4 +- website/src/guide/benchmarks/compilation.md | 0 .../benchmarks.md => benchmarks/runtime.md} | 2 +- .../optional-generic-members.md} | 10 +-- .../src/guide/{misc => }/troubleshooting.md | 2 +- .../{misc => troubleshooting}/limitations.md | 0 .../src/reference/builder/member/default.md | 2 +- 13 files changed, 84 insertions(+), 70 deletions(-) rename website/src/guide/{misc => }/alternatives.md (83%) rename website/src/guide/{misc => basics}/compatibility.md (100%) create mode 100644 website/src/guide/benchmarks/compilation.md rename website/src/guide/{misc/benchmarks.md => benchmarks/runtime.md} (99%) rename website/src/guide/{antipatterns.md => patterns/optional-generic-members.md} (95%) rename website/src/guide/{misc => }/troubleshooting.md (91%) rename website/src/guide/{misc => troubleshooting}/limitations.md (100%) diff --git a/README.v3.md b/README.v3.md index b1056d94..0f9cba6b 100644 --- a/README.v3.md +++ b/README.v3.md @@ -137,7 +137,7 @@ assert_eq!(user.id, 1); assert_eq!(user.name, "Bon"); ``` -`#[derive(Builder)]` on a struct generates builder API that is fully compatible with placing `#[builder]` on the `new()` method with a signature similar to the struct's fields (more details on the [Compatibility](https://bon-rs.com/guide/misc/compatibility#switching-between-derive-builder-and-builder-on-the-new-method) page). +`#[derive(Builder)]` on a struct generates builder API that is fully compatible with placing `#[builder]` on the `new()` method with a signature similar to the struct's fields (more details on the [Compatibility](https://bon-rs.com/guide/basics/compatibility#switching-between-derive-builder-and-builder-on-the-new-method) page). ### Other Methods @@ -204,7 +204,7 @@ You can opt out of `std` and `alloc` cargo features with `default-features = fal This project was heavily inspired by such awesome crates as [`buildstructor`](https://docs.rs/buildstructor), [`typed-builder`](https://docs.rs/typed-builder) and [`derive_builder`](https://docs.rs/derive_builder). This crate was designed with many lessons learned from them. -See [alternatives](https://bon-rs.com/guide/misc/alternatives) for comparison. +See [alternatives](https://bon-rs.com/guide/alternatives) for comparison. ## Getting Help diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index a5110bbf..a948342a 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -130,9 +130,14 @@ export default defineConfig({ text: "Overview", link: "/guide/overview", }, + { + text: "Alternatives", + link: "/guide/alternatives", + }, { text: "Basics", link: "/guide/basics", + collapsed: false, items: [ { text: "Optional Members", @@ -158,11 +163,16 @@ export default defineConfig({ text: "Documenting", link: "/guide/basics/documenting", }, + { + text: "Compatibility", + link: "/guide/compatibility", + }, ], }, { text: "Typestate API", link: "/guide/typestate-api", + collapsed: false, items: [ { text: "Builder's Type Signature", @@ -177,6 +187,7 @@ export default defineConfig({ { text: "Patterns", link: "/guide/patterns", + collapsed: false, items: [ { text: "Conditional Building", @@ -186,6 +197,10 @@ export default defineConfig({ text: "Fallible Builders", link: "/guide/patterns/fallible-builders", }, + { + text: "Optional Generic Members", + link: "/guide/patterns/optional-generic-members", + }, { text: "Into Conversions In-Depth", link: "/guide/patterns/into-conversions-in-depth", @@ -197,43 +212,34 @@ export default defineConfig({ ], }, { - text: "Antipatterns", - link: "/guide/antipatterns", - }, - { - text: "Misc", + text: "Benchmarks", + link: "/guide/benchmarks", + collapsed: false, items: [ { - text: "Compatibility", - link: "/guide/misc/compatibility", - }, - { - text: "Limitations", - link: "/guide/misc/limitations", - }, - { - text: "Benchmarks", - link: "/guide/misc/benchmarks", + text: "Runtime", + link: "/guide/benchmarks/runtime", }, { - text: "Alternatives", - link: "/guide/misc/alternatives", - }, - { - text: "Troubleshooting", - link: "/guide/misc/troubleshooting", + text: "Compilation", + link: "/guide/benchmarks/compilation", }, ], }, { - text: "Internal", + text: "Troubleshooting", + link: "/guide/troubleshooting", items: [ { - text: "Contributing", - link: "/guide/internal/contributing", + text: "Limitations", + link: "/guide/troubleshooting/limitations", }, ], }, + { + text: "Contributing", + link: "/guide/internal/contributing", + }, ], "/reference": [ { @@ -243,7 +249,7 @@ export default defineConfig({ { text: "Top-Level", link: "/reference/builder#top-level-attributes", - collapsed: true, + collapsed: false, items: [ { text: "builder_type", @@ -278,7 +284,7 @@ export default defineConfig({ { text: "Member", link: "/reference/builder#member-attributes", - collapsed: true, + collapsed: false, items: [ { text: "default", diff --git a/website/src/blog/bon-builder-v2-3-release.md b/website/src/blog/bon-builder-v2-3-release.md index 993ccb8c..809bfcbe 100644 --- a/website/src/blog/bon-builder-v2-3-release.md +++ b/website/src/blog/bon-builder-v2-3-release.md @@ -21,7 +21,7 @@ If you don't know about [`bon`], then see the [motivational blog post](./how-to- ### Positional arguments in starting and finishing functions -While having the ability to use separate setters for the members gives you a ton of flexibility and extensibility described on the ["Compatibility"](../guide/misc/compatibility) page, sometimes you don't need all of that. +While having the ability to use separate setters for the members gives you a ton of flexibility and extensibility described on the ["Compatibility"](../guide/basics/compatibility) page, sometimes you don't need all of that. Maybe you'd like to pick out some specific members and let the user pass their values as positional parameters to the starting function that creates the builder or to the finishing function that consumes it. This reduces the syntax a bit at the cost of some extensibility loss โš–๏ธ, but it may be worth it! diff --git a/website/src/blog/how-to-do-named-function-arguments-in-rust.md b/website/src/blog/how-to-do-named-function-arguments-in-rust.md index e18877d7..24ae07d8 100644 --- a/website/src/blog/how-to-do-named-function-arguments-in-rust.md +++ b/website/src/blog/how-to-do-named-function-arguments-in-rust.md @@ -147,7 +147,7 @@ User::builder() .build(); ``` -So, you can use just one builder crate solution consistently for everything. Builders for functions and structs both share the same API design, which allows you, for example, to switch between a `#[derive(Builder)]` on a struct and a `#[builder]` attribute on a method that creates a struct. This won't be an API-breaking change for your consumers ([details](../guide/misc/compatibility#switching-between-derive-builder-and-builder-on-the-new-method)). +So, you can use just one builder crate solution consistently for everything. Builders for functions and structs both share the same API design, which allows you, for example, to switch between a `#[derive(Builder)]` on a struct and a `#[builder]` attribute on a method that creates a struct. This won't be an API-breaking change for your consumers ([details](../guide/basics/compatibility#switching-between-derive-builder-and-builder-on-the-new-method)). ## Summary diff --git a/website/src/guide/misc/alternatives.md b/website/src/guide/alternatives.md similarity index 83% rename from website/src/guide/misc/alternatives.md rename to website/src/guide/alternatives.md index 408adf54..b6603a34 100644 --- a/website/src/guide/misc/alternatives.md +++ b/website/src/guide/alternatives.md @@ -16,29 +16,29 @@ There are several other existing alternative crates for generating builders. `bo -| Feature | `bon` | [`buildstructor`] | [`typed-builder`] | [`derive_builder`] | -| ---------------------------------------- | ---------------------------------- | ------------------------- | ------------------------------------ | --------------------------------------------- | -| Builder for structs | โœ… | โœ… | โœ… | โœ… | -| Builder for free functions | โœ… | | | -| Builder for associated methods | โœ… | โœ… | | -| Panic safe | โœ… | โœ… | โœ… | `build()` returns `Result` | -| `Option` makes members optional | โœ… | โœ… | | | -| `T` -> `Option` is non-breaking | โœ… [docs][bon-req-to-opt] | โœ… | via attr `strip_option` | via attr [`strip_option`][db-so] | -| Generates `T::builder()` method | โœ… | โœ… | โœ… | only `Builder::default()` | -| `Into` conversion in setters | [opt-in][bon-into] | [implicit][bs-into] | opt-in | [opt-in][db-into] | -| Validation in the finishing function | โœ… [docs][bon-fallible-builder] | โœ… [docs][bs-fall-finish] | | โœ… [docs][db-fall-finish] | -| Validation in setters (fallible setters) | โœ… attr [`with = closure`][b] | | | โœ… `TryInto` via attr [`try_setter`][db-fs] | -| Custom methods on builder | โœ… via [direct impl block][bon-ts] | | โœ…  via [mutators] (attrs) | โœ… via [direct impl block][db-custom-methods] | -| `impl Trait`, elided lifetimes support | โœ… | | | -| Builder for `fn` hides original `fn` | โœ… | | | -| Special setters for collections | [(see below)][r1] | โœ… | | โœ… | -| Builder by `&self`/`&mut self` | | | | โœ… | +| Feature | `bon` | [`buildstructor`] | [`typed-builder`] | [`derive_builder`] | +| ---------------------------------------- | ---------------------------------- | ------------------------- | ------------------------- | --------------------------------------------- | +| Builder for structs | โœ… | โœ… | โœ… | โœ… | +| Builder for free functions | โœ… | | | +| Builder for associated methods | โœ… | โœ… | | +| Panic safe | โœ… | โœ… | โœ… | `build()` returns `Result` | +| `Option` makes members optional | โœ… | โœ… | | | +| `T` -> `Option` is non-breaking | โœ… [docs][bon-req-to-opt] | โœ… | via attr `strip_option` | via attr [`strip_option`][db-so] | +| Generates `T::builder()` method | โœ… | โœ… | โœ… | only `Builder::default()` | +| `Into` conversion in setters | [opt-in][bon-into] | [implicit][bs-into] | opt-in | [opt-in][db-into] | +| Validation in the finishing function | โœ… [docs][bon-fallible-builder] | โœ… [docs][bs-fall-finish] | | โœ… [docs][db-fall-finish] | +| Validation in setters (fallible setters) | โœ… attr [`with = closure`][b] | | | โœ… `TryInto` via attr [`try_setter`][db-fs] | +| Custom methods on builder | โœ… via [direct impl block][bon-ts] | | โœ… via [mutators] (attrs) | โœ… via [direct impl block][db-custom-methods] | +| `impl Trait`, elided lifetimes support | โœ… | | | +| Builder for `fn` hides original `fn` | โœ… | | | +| Special setters for collections | [(see below)][collections] | โœ… | | โœ… | +| Builder by `&self`/`&mut self` | | | | โœ… | ## Function Builder Paradigm Shift -If you ever hit a wall ๐Ÿงฑ with `typed-builder` or `derive_builder`, you'll have to hack something around their derive attributes syntax on a struct. With `bon` or `buildstructor` you can simply change the syntax from `#[derive(Builder)]` on a struct to a `#[builder]` on a function to gain more flexibility at any time ๐Ÿคธ. It is [guaranteed to preserve compatibility](../misc/compatibility#switching-between-derivebuilder-and-builder-on-the-new-method) (not a breaking change). +If you ever hit a wall ๐Ÿงฑ with `typed-builder` or `derive_builder`, you'll have to hack something around their derive attributes syntax on a struct. With `bon` or `buildstructor` you can simply change the syntax from `#[derive(Builder)]` on a struct to a `#[builder]` on a function to gain more flexibility at any time ๐Ÿคธ. It is [guaranteed to preserve compatibility](./basics/compatibility#switching-between-derivebuilder-and-builder-on-the-new-method) (not a breaking change). ### Example @@ -149,7 +149,7 @@ Moreover, it offers you a completely new dimension of flexibility: ### Summary -The chances of hitting a wall with function builders are close to zero, and even if you ever do, you still have access to the [Typestate API](../typestate-api/) in `bon` for even more flexibility ๐Ÿ’ช. +The chances of hitting a wall with function builders are close to zero, and even if you ever do, you still have access to the [Typestate API](./typestate-api/) in `bon` for even more flexibility ๐Ÿ’ช. ## Special setter methods for collections @@ -216,18 +216,30 @@ Another difference is that fields of collection types are considered required by [arr]: https://docs.rs/bon/latest/bon/macro.arr.html [map]: https://docs.rs/bon/latest/bon/macro.map.html [set]: https://docs.rs/bon/latest/bon/macro.set.html -[Mutators]: https://docs.rs/typed-builder/latest/typed_builder/derive.TypedBuilder.html#mutators -[bon-on]: ../../reference/builder/top-level/on -[bon-into]: ../../reference/builder/member/into -[bon-req-to-opt]: ../misc/compatibilitymaking-a-required-member-optional +[collections]: #special-setter-methods-for-collections + + + +[bon-on]: ../reference/builder/top-level/on +[bon-into]: ../reference/builder/member/into +[bon-req-to-opt]: ./basics/compatibility#making-a-required-member-optional +[bon-fallible-builder]: ./patterns/fallible-builders +[bon-ts]: ./typestate-api +[b]: ../reference/builder/member/with#fallible-closure + + + [bs-into]: https://docs.rs/buildstructor/latest/buildstructor/#into-field +[bs-fall-finish]: https://docs.rs/buildstructor/latest/buildstructor/#fallible + + + +[mutators]: https://docs.rs/typed-builder/latest/typed_builder/derive.TypedBuilder.html#mutators + + + [db-into]: https://docs.rs/derive_builder/latest/derive_builder/#generic-setters [db-so]: https://docs.rs/derive_builder/latest/derive_builder/#setters-for-option -[bon-fallible-builder]: ../patterns/fallible-builders -[bs-fall-finish]: https://docs.rs/buildstructor/latest/buildstructor/#fallible [db-fall-finish]: https://docs.rs/derive_builder/latest/derive_builder/#pre-build-validation -[b]: ../../reference/builder/member/with#fallible-closure [db-custom-methods]: https://docs.rs/derive_builder/latest/derive_builder/#custom-setters-skip-autogenerated-setters [db-fs]: https://docs.rs/derive_builder/latest/derive_builder/#fallible-setters -[r1]: #special-setter-methods-for-collections -[bon-ts]: ../typestate-api diff --git a/website/src/guide/misc/compatibility.md b/website/src/guide/basics/compatibility.md similarity index 100% rename from website/src/guide/misc/compatibility.md rename to website/src/guide/basics/compatibility.md diff --git a/website/src/guide/basics/optional-members.md b/website/src/guide/basics/optional-members.md index a01fb376..6fa277b5 100644 --- a/website/src/guide/basics/optional-members.md +++ b/website/src/guide/basics/optional-members.md @@ -43,7 +43,7 @@ impl ExampleBuilder { } ``` -Thanks to this design, changing the member from required to optional [preserves compatibility](../misc/compatibility#making-a-required-member-optional). +Thanks to this design, changing the member from required to optional [preserves compatibility](./compatibility#making-a-required-member-optional). ### Examples @@ -71,7 +71,7 @@ To make a member of non-`Option` type optional you may use [`#[builder(default)] ::: tip -Switching between `#[builder(default)]` and `Option` is [compatible](../misc/compatibility#switching-between-option-t-and-builder-default). +Switching between `#[builder(default)]` and `Option` is [compatible](./compatibility#switching-between-option-t-and-builder-default). ::: diff --git a/website/src/guide/benchmarks/compilation.md b/website/src/guide/benchmarks/compilation.md new file mode 100644 index 00000000..e69de29b diff --git a/website/src/guide/misc/benchmarks.md b/website/src/guide/benchmarks/runtime.md similarity index 99% rename from website/src/guide/misc/benchmarks.md rename to website/src/guide/benchmarks/runtime.md index ff381fc6..5100ac9d 100644 --- a/website/src/guide/misc/benchmarks.md +++ b/website/src/guide/benchmarks/runtime.md @@ -1,4 +1,4 @@ -# Benchmarks +# Runtime Benchmarks Builder macros generate code that is easily optimizable by the compiler. This has been tested by the benchmarks below. The benchmarks compare regular positional function call syntax and builder syntax for functions annotated with `#[builder]`. diff --git a/website/src/guide/antipatterns.md b/website/src/guide/patterns/optional-generic-members.md similarity index 95% rename from website/src/guide/antipatterns.md rename to website/src/guide/patterns/optional-generic-members.md index 99fbcf94..7c515ee3 100644 --- a/website/src/guide/antipatterns.md +++ b/website/src/guide/patterns/optional-generic-members.md @@ -1,14 +1,10 @@ -# Antipatterns - -While [Patterns](./patterns) teach you how to use `bon`, here we'll discuss how **not to use** `bon`. - -## Generic Types in Optional Members +# Optional Generic Members Generic type parameters, `impl Trait` or const generics used exclusively in optional members break type inference. This is mostly relevant to `fn`-based builders. This problem becomes visible when you skip setting an optional member. -### ๐Ÿ”ด Bad +## ๐Ÿ”ด Bad ```rust compile_fail #[bon::builder] @@ -42,7 +38,7 @@ bad::().call(); This is inconvenient, don't do this. -### โœ… Good +## โœ… Good Instead, make the member's type non-generic and move generics to the setter methods' signature. Let's what it means with an example. diff --git a/website/src/guide/misc/troubleshooting.md b/website/src/guide/troubleshooting.md similarity index 91% rename from website/src/guide/misc/troubleshooting.md rename to website/src/guide/troubleshooting.md index 9f591922..caecefd3 100644 --- a/website/src/guide/misc/troubleshooting.md +++ b/website/src/guide/troubleshooting.md @@ -4,7 +4,7 @@ If you encounter any issues, then try the following. ## Check for known limitations -Check if your problem is already described on the [known limitations page](./limitations), where you'll also find some suggested workarounds. +Check if your problem is already described on the [known limitations page](./troubleshooting/limitations), where you'll also find some suggested workarounds. ## Expand the macro diff --git a/website/src/guide/misc/limitations.md b/website/src/guide/troubleshooting/limitations.md similarity index 100% rename from website/src/guide/misc/limitations.md rename to website/src/guide/troubleshooting/limitations.md diff --git a/website/src/reference/builder/member/default.md b/website/src/reference/builder/member/default.md index dcceea8c..e94ea142 100644 --- a/website/src/reference/builder/member/default.md +++ b/website/src/reference/builder/member/default.md @@ -26,7 +26,7 @@ If `None` is passed to the `maybe_{member}` setter, then the default value is us ::: tip -Switching between `#[builder(default)]` and `Option` is [compatible](../../../guide/misc/compatibility#switching-between-option-t-and-builder-default). +Switching between `#[builder(default)]` and `Option` is [compatible](../../../guide/basics/compatibility#switching-between-option-t-and-builder-default). ::: From 38e8803566b52a84aa86993709b27a596617293b Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 8 Nov 2024 01:04:55 +0000 Subject: [PATCH 11/12] Self-review --- benchmarks/runtime/run.sh | 2 +- benchmarks/runtime/src/lib.rs | 4 +- website/.vitepress/config.mts | 9 ++- website/src/guide/alternatives.md | 66 +++++++++---------- website/src/guide/benchmarks.md | 6 ++ website/src/guide/benchmarks/runtime.md | 4 +- .../src/guide/{internal => }/contributing.md | 0 .../src/guide/patterns/fallible-builders.md | 2 +- .../patterns/optional-generic-members.md | 2 +- .../typestate-api/builders-type-signature.md | 2 +- 10 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 website/src/guide/benchmarks.md rename website/src/guide/{internal => }/contributing.md (100%) diff --git a/benchmarks/runtime/run.sh b/benchmarks/runtime/run.sh index 88838f43..07cdddb4 100755 --- a/benchmarks/runtime/run.sh +++ b/benchmarks/runtime/run.sh @@ -2,7 +2,7 @@ set -euxo pipefail -# List src/ dir, remove `.rs` from file extensions and remove lib.rs +# Run all benchmarks by default benches=${1:-$(find src -name '*.rs' | sed 's/\.rs$//' | sed 's/src\///' | grep -v lib)} export CARGO_INCREMENTAL=0 diff --git a/benchmarks/runtime/src/lib.rs b/benchmarks/runtime/src/lib.rs index c740fe72..64dd6d40 100644 --- a/benchmarks/runtime/src/lib.rs +++ b/benchmarks/runtime/src/lib.rs @@ -20,8 +20,8 @@ use bon; cfg_if::cfg_if! { if #[cfg(feature = "args_3")] { - pub mod args_20; - pub use args_20 as bench; + pub mod args_3; + pub use args_3 as bench; } else if #[cfg(feature = "args_5")] { pub mod args_5; pub use args_5 as bench; diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index a948342a..11a6c523 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -48,6 +48,9 @@ export default defineConfig({ "Function that creates the builder (e.g. `builder()`)", ["finishing function"]: "Method on the builder struct that finishes building (e.g. `build()` or `call()`)", + ["underlying type"]: + "For required members, it's the type of the member itself. " + + "For optional members, it's the type `T` inside of the `Option`", }; const abbrsStr = Object.entries(abbrs) @@ -137,7 +140,6 @@ export default defineConfig({ { text: "Basics", link: "/guide/basics", - collapsed: false, items: [ { text: "Optional Members", @@ -172,7 +174,6 @@ export default defineConfig({ { text: "Typestate API", link: "/guide/typestate-api", - collapsed: false, items: [ { text: "Builder's Type Signature", @@ -187,7 +188,6 @@ export default defineConfig({ { text: "Patterns", link: "/guide/patterns", - collapsed: false, items: [ { text: "Conditional Building", @@ -214,7 +214,6 @@ export default defineConfig({ { text: "Benchmarks", link: "/guide/benchmarks", - collapsed: false, items: [ { text: "Runtime", @@ -238,7 +237,7 @@ export default defineConfig({ }, { text: "Contributing", - link: "/guide/internal/contributing", + link: "/guide/contributing", }, ], "/reference": [ diff --git a/website/src/guide/alternatives.md b/website/src/guide/alternatives.md index b6603a34..2cc84f6b 100644 --- a/website/src/guide/alternatives.md +++ b/website/src/guide/alternatives.md @@ -6,7 +6,7 @@ aside: false There are several other existing alternative crates for generating builders. `bon` was designed based on many lessons learned from them. A table that compares the builder crates with some additional explanations is below. - +