diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b864a42..8dea42b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,7 +151,7 @@ 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 \ diff --git a/README.v3.md b/README.v3.md index 2784bc0e..0f9cba6b 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 @@ -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 @@ -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. | โญ 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. @@ -202,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/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). @@ -221,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/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..07cdddb4 100755 --- a/benchmarks/runtime/run.sh +++ b/benchmarks/runtime/run.sh @@ -2,21 +2,22 @@ set -euxo pipefail -bench=${1:-args_10_structs} +# Run all benchmarks by default +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..64dd6d40 100644 --- a/benchmarks/runtime/src/lib.rs +++ b/benchmarks/runtime/src/lib.rs @@ -12,6 +12,12 @@ 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; 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/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/.vitepress/config.mts b/website/.vitepress/config.mts index 487e1bb4..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) @@ -130,8 +133,13 @@ export default defineConfig({ text: "Overview", link: "/guide/overview", }, + { + text: "Alternatives", + link: "/guide/alternatives", + }, { text: "Basics", + link: "/guide/basics", items: [ { text: "Optional Members", @@ -157,6 +165,10 @@ export default defineConfig({ text: "Documenting", link: "/guide/basics/documenting", }, + { + text: "Compatibility", + link: "/guide/compatibility", + }, ], }, { @@ -175,6 +187,7 @@ export default defineConfig({ }, { text: "Patterns", + link: "/guide/patterns", items: [ { text: "Conditional Building", @@ -184,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", @@ -195,39 +212,33 @@ export default defineConfig({ ], }, { - text: "Misc", + text: "Benchmarks", + link: "/guide/benchmarks", items: [ { - text: "Compatibility", - link: "/guide/misc/compatibility", - }, - { - text: "Limitations", - link: "/guide/misc/limitations", + text: "Runtime", + link: "/guide/benchmarks/runtime", }, { - text: "Benchmarks", - link: "/guide/misc/benchmarks", - }, - { - 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/contributing", + }, ], "/reference": [ { @@ -237,7 +248,7 @@ export default defineConfig({ { text: "Top-Level", link: "/reference/builder#top-level-attributes", - collapsed: true, + collapsed: false, items: [ { text: "builder_type", @@ -272,7 +283,7 @@ export default defineConfig({ { text: "Member", link: "/reference/builder#member-attributes", - collapsed: true, + collapsed: false, items: [ { text: "default", 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/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/alternatives.md b/website/src/guide/alternatives.md new file mode 100644 index 00000000..4c08ae50 --- /dev/null +++ b/website/src/guide/alternatives.md @@ -0,0 +1,243 @@ +--- +aside: false +--- + +# Alternatives + +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 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 attr [mutators] | โœ… 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](./basics/compatibility#switching-between-derive-builder-and-builder-on-the-new-method), meaning it's not a breaking change. + +### Example + +Suppose you already had a struct like the following with a builder derive: + +```rust ignore +use bon::Builder; + +#[derive(Builder)] +pub struct Line { + x1: u32, + y1: u32, + + x2: u32, + y2: u32, +} + +// Suppose this is your users' code +Line::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 compile_fail +use bon::Builder; + +#[derive(Builder)] +pub struct Line { + point1: Point, + point2: Point, +} + +// Private +struct Point { + x: u32, + y: u32, +} + +// Suppose this is your users' code (it no longer compiles) +Line::builder().x1(1).y1(2).x2(3).y2(4).build(); // [!code error] +// ^^- error[E0599]: no method named `x1` found for struct `LineBuilder` // [!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 `Line`'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 + +In contrast, `bon`'s type state **is** human-readable, maintainable, and [documented](./typestate-api) ๐Ÿ‘ + +::: + +### Behold the Function-Based Builder + +This change is as simple as pie ๐Ÿฅง with `bon` or `buildstructor`. The code speaks for itself: + +```rust +use bon::bon; + +// No more derives on a struct. Its internal representation is decoupled from the builder. +pub struct Line { + point1: Point, + point2: Point, +} + +struct Point { + x: u32, + y: u32, +} + +#[bon] +impl Line { + #[builder] + fn new(x1: u32, y1: u32, x2: u32, y2: u32) -> Self { + Self { + point1: Point { x: x1, y: y1 } , + point2: Point { x: x2, y: y2 } , + } + } +} + +// Suppose this is your users' code (it compiles after this change, yay ๐ŸŽ‰!) +Line::builder().x1(1).y1(2).x2(3).y2(4).build(); +``` + +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 ๐Ÿ˜‰. + +--- + +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 + +Other builder crates provide a way to generate methods to build collections one element at a time. For example, `buildstructor` even generates such methods by default: + +```rust +#[derive(buildstructor::Builder)] +struct User { + friends: Vec +} + +fn main() { + User::builder() + .friend("Foo") + .friend("Bar") + .friend("`String` value is also accepted".to_owned()) + .build(); +} +``` + +::: tip + +Why is there an explicit `main()` function in this code snippet ๐Ÿค”? It's a long story explained in a [blog post](/blog/the-weird-of-function-local-types-in-rust) (feel free to skip). + +::: + +This feature isn't available today in `bon`, but it's planned for the future. However, it won't be enabled by default, but rather be opt-in like it is in `derive_builder`. + +The problem with this feature is that a setter that pushes an element into a collection like that may confuse the reader in case if only one element is pushed. This may hide the fact that the member is actually a collection called `friends` in the plural. However, this feature is still useful to provide backwards compatibility when changing the type of a member from `T` or `Option` to `Collection`. + +Alternatively, `bon` provides a separate solution. `bon` exposes the following macros that provide convenient syntax to create collections. + +| `Vec` | `[T; N]` | `*Map` | `*Set` | +| -------------------- | -------------------- | -------------------- | -------------------- | +| [`bon::vec![]`][vec] | [`bon::arr![]`][arr] | [`bon::map!{}`][map] | [`bon::set![]`][set] | + +These macros share a common feature that every element of the collection is converted with `Into` to shorten the syntax if you, for example, need to initialize a `Vec` with items of type `&str`. Use these macros only if you need this behaviour, or ignore them if you want to be explicit in code and avoid implicit `Into` conversions. + +```rust +use bon::Builder; + +#[derive(Builder)] +struct User { + friends: Vec +} + +User::builder() + .friends(bon::vec![ + "Foo", + "Bar", + "`String` value is also accepted".to_owned(), + ]) + .build(); +``` + +Another difference is that fields of collection types are considered required by default in `bon`, which isn't the case in `buildstructor`. + +[`buildstructor`]: https://docs.rs/buildstructor/latest/buildstructor/ +[`typed-builder`]: https://docs.rs/typed-builder/latest/typed_builder/ +[`derive_builder`]: https://docs.rs/derive_builder/latest/derive_builder/ +[vec]: https://docs.rs/bon/latest/bon/macro.vec.html +[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 +[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 +[db-fall-finish]: https://docs.rs/derive_builder/latest/derive_builder/#pre-build-validation +[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 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/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/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..6fa277b5 100644 --- a/website/src/guide/basics/optional-members.md +++ b/website/src/guide/basics/optional-members.md @@ -29,22 +29,21 @@ 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). +Thanks to this design, changing the member from required to optional [preserves compatibility](./compatibility#making-a-required-member-optional). ### Examples @@ -72,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). ::: @@ -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/benchmarks.md b/website/src/guide/benchmarks.md new file mode 100644 index 00000000..10803182 --- /dev/null +++ b/website/src/guide/benchmarks.md @@ -0,0 +1,6 @@ +# Benchmarks + +This section shows the results of benchmarks: + +- [Runtime](./benchmarks/runtime) - measure the performance of code generated by macros +- [Compilation](./benchmarks/compilation) - measure the compilation time overhead of `bon` macros 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/benchmarks/runtime.md b/website/src/guide/benchmarks/runtime.md new file mode 100644 index 00000000..bcb7d908 --- /dev/null +++ b/website/src/guide/benchmarks/runtime.md @@ -0,0 +1,77 @@ +# Runtime Benchmarks + +This page describes the results of benchmarking the performance of code generated by `bon`'s builder macros. + +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. + +::: tip TIP + +Don't take these microbenchmarks for granted. Do your own performance measurements in your application in real conditions. Feel free to [open an issue](https://github.com/elastio/bon/issues) if you find performance problems in `bon`. + +::: + + + + +## Wallclock Statistics + +
+ +| 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 + +The code was compiled with `opt-level = 3` and `debug = 0`. + +### Hardware + +The benchmarks were run on a dedicated root server `AX51-NVMe` on [Hetzner](https://www.hetzner.com/). + +- OS: Ubuntu 22.04.4 (Linux 5.15.0-76-generic) +- CPU: AMD Ryzen 7 3700X 8-Core Processor (x86_64) +- RAM: 62.8 GiB + +## References + +The source code of the benchmarks is [available here][benchmarks-source]. + +[benchmarks-source]: https://github.com/elastio/bon/tree/master/benchmarks/runtime diff --git a/website/src/guide/internal/contributing.md b/website/src/guide/contributing.md similarity index 100% rename from website/src/guide/internal/contributing.md rename to website/src/guide/contributing.md diff --git a/website/src/guide/misc/alternatives.md b/website/src/guide/misc/alternatives.md deleted file mode 100644 index 58e60b09..00000000 --- a/website/src/guide/misc/alternatives.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -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. - - - -| 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` supported for functions | โœ… | | | -| Anonymous lifetimes supported for functions | โœ… | | | -| `Self` mentions in functions/structs are supported | โœ… | | | -| Positional function is hidden by default | โœ… | | | -| Special setter methods for collections | [(see below)][r1] | โœ… | | โœ… | -| Custom methods can be added to the builder type | | | โœ… ([mutators]) | โœ… | -| Builder may be configured to use &self/&mut self | | | | โœ… | - -## Function builder fallback paradigm - -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. - -However, `bon` still provides some simple attributes for common use cases to configure the behaviour without falling back to a more verbose syntax. - -## Special setter methods for collections - -Other builder crates provide a way to generate methods to build collections one element at a time. For example, `buildstructor` even generates such methods by default: - -```rust -#[derive(buildstructor::Builder)] -struct User { - friends: Vec -} - -fn main() { - User::builder() - .friend("Foo") - .friend("Bar") - .friend("`String` value is also accepted".to_owned()) - .build(); -} -``` - -::: tip - -Why is there an explicit `main()` function in this code snippet ๐Ÿค”? It's a long story explained in a [blog post](/blog/the-weird-of-function-local-types-in-rust) (feel free to skip). - -::: - -This feature isn't available today in `bon`, but it's planned for the future. However, it won't be enabled by default, but rather be opt-in like it is in `derive_builder`. - -The problem with this feature is that a setter that pushes an element into a collection like that may confuse the reader in case if only one element is pushed. This may hide the fact that the member is actually a collection called `friends` in the plural. However, this feature is still useful to provide backwards compatibility when changing the type of a member from `T` or `Option` to `Collection`. - -Alternatively, `bon` provides a separate solution. `bon` exposes the following macros that provide convenient syntax to create collections. - -| `Vec` | `[T; N]` | `*Map` | `*Set` | -| -------------------- | -------------------- | -------------------- | -------------------- | -| [`bon::vec![]`][vec] | [`bon::arr![]`][arr] | [`bon::map!{}`][map] | [`bon::set![]`][set] | - -These macros share a common feature that every element of the collection is converted with `Into` to shorten the syntax if you, for example, need to initialize a `Vec` with items of type `&str`. Use these macros only if you need this behaviour, or ignore them if you want to be explicit in code and avoid implicit `Into` conversions. - -**Example:** - -```rust -use bon::Builder; - -#[derive(Builder)] -struct User { - friends: Vec -} - -User::builder() - .friends(bon::vec![ - "Foo", - "Bar", - "`String` value is also accepted".to_owned(), - ]) - .build(); -``` - -Another difference is that fields of collection types are considered required by default, which isn't the case in `buildstructor`. - -[`buildstructor`]: https://docs.rs/buildstructor/latest/buildstructor/ -[`typed-builder`]: https://docs.rs/typed-builder/latest/typed_builder/ -[`derive_builder`]: https://docs.rs/derive_builder/latest/derive_builder/ -[vec]: https://docs.rs/bon/latest/bon/macro.vec.html -[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 -[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 diff --git a/website/src/guide/misc/benchmarks.md b/website/src/guide/misc/benchmarks.md deleted file mode 100644 index fa9aa9c3..00000000 --- a/website/src/guide/misc/benchmarks.md +++ /dev/null @@ -1,49 +0,0 @@ -# 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]`. - -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. - -::: tip TIP - -Don't take these microbenchmarks for granted. Do your own performance measurements in your application in real conditions. Feel free to [open an issue](https://github.com/elastio/bon/issues) if you find performance problems in `bon`. - -::: - -## 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` | - -## 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` | - -## Conditions - -The code was compiled with `opt-level = 3` and `debug = 0`. - -### Hardware - -The benchmarks were run on a dedicated root server `AX51-NVMe` on [Hetzner](https://www.hetzner.com/). - -- OS: Ubuntu 22.04.4 (Linux 5.15.0-76-generic) -- CPU: AMD Ryzen 7 3700X 8-Core Processor (x86_64) -- RAM: 62.8 GiB - -## References - -The source code of the benchmarks is [available here](https://github.com/elastio/bon/tree/master/benchmarks). 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/patterns/fallible-builders.md b/website/src/guide/patterns/fallible-builders.md index 4808970e..40d9fb35 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**(!) 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 gain a lot of power 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/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`. diff --git a/website/src/guide/patterns/optional-generic-members.md b/website/src/guide/patterns/optional-generic-members.md new file mode 100644 index 00000000..51851926 --- /dev/null +++ b/website/src/guide/patterns/optional-generic-members.md @@ -0,0 +1,116 @@ +# 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. + +## :red_circle: Bad + +```rust compile_fail +#[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. + +## :green_circle: Good + +Instead, make the member's type non-generic and move generics to the setter methods' signature. Let's see what it means with an example. + +For the case above, the good solution is [`#[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 ignore [builder(into)] +fn good() -> GoodBuilder { /**/ } + +impl GoodBuilder { + fn x1(self, value: impl Into) -> GoodBuilder> { + GoodBuilder { /* other fields */, __x1: value.into() } + } +} +``` + + + +```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 `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. + +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 + +```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 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/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/guide/typestate-api.md b/website/src/guide/typestate-api.md index 8dc2dfc1..4bd7ae9a 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](../reference/builder). + +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 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. + +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/guide/typestate-api/builders-type-signature.md b/website/src/guide/typestate-api/builders-type-signature.md index d5d7cc8d..40d69477 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 ignore +// 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. 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). ::: 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`. 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.