Flexible builder extension and type signature API
Closes #21
For anyone who wants to try out the changes here, you can do so using the following line in Cargo.toml:
[dependencies]
bon = { git = "https://github.com/elastio/bon", branch = "feat/generic-builder-type" }
Example of how this feature works (it already compiles from this branch and existing tests in bon pass):
#[derive(bon::Builder)]
struct Mare {
name: &'static str,
age: u32,
breed: &'static str,
rating: u32,
}
use mare_builder::{SetAge, SetName, SetBreed};
// Add custom methods to the builder.
impl<State: mare_builder::State> MareBuilder<State> {
fn name_and_age(self, name: &'static str, age: u32) -> MareBuilder<SetName<SetAge<State>>>
where
// Require that members are unset
State::Name: mare_builder::IsUnset,
State::Age: mare_builder::IsUnset,
// Require that members are set
// State::Breed: mare_builder::IsSet,
{
self.name(name).age(age)
}
}
fn main() {
// Builder type is finally nameable with a stable type signature!
let builder: MareBuilder<SetBreed<SetAge<SetName>>> = Mare::builder()
.breed("Equestrian")
.name_and_age("Bon Bon", 24);
let _mare: Mare = builder.rating(99).build();
}
TODO
According to my research any slightest usage of associated type projections in the generated code considerably increases the compile time. In the most minimal approach where assoc types are used as trait bounds on setter methods and they aren't used inside of the struct to change its shape the compilation performance of frankenstein (~320 structs) increases from 19 to 20 seconds (from scratch). In the worst case, when assoc types are used to define the type of optional members in the struct the compile time increases to 26 seconds 💥.
I think the ideal balance between safety and compilation performance is such that we:
- Use
Option<T>to store all named members, including required. We don't use theState::{Member}assoc types in the struct declaration itself to avoid compilation time overhead. - Generate random identifiers for all the private members of the builder struct and its private methods. This way we ensure the user can't use our private fields and methods that may lead to them breaking some unsafe invariants.
- Introduce a new private method
transition<NewState>(self) -> TBuilder<NewState>that changes thePhantomDatatype that holds the type state. It's technically an unsafe method (see the following), but it doesn't actually use any unsafe operations. This method will be used by all the generated setters and should reduce the amount of tokens generated considerably (especially for builders with tens of members). - Use
unwrap_uncheckedfor required members in thefinish_fnmethod because we know they are always initialized according to the type state in that method. This is needed because the safe "unwrap" isn't optimized-out by the compiler according to the benchmarks and generated assembly.
- [x] Make setter methods inherit builder type visibility by default. Add
#[builder(setters = {name})] override - [x] Tests for
#[builder(setters)] - [x] Consider alternative name for
mutable:overwritable,no_overwrite_protection... - [x] Insert random number into the internal field names to make them genuinely private
- [x] Autoresolve name conflicts with the builder state generic parameter
- [x] Add tests
- [ ] Add examples to the docs of
IsSettrait - [ ] Add docs for various
docs { ... }overrides attributes added in https://github.com/elastio/bon/pull/139 - [ ] Add docs for various
vis(...)configs and how the influence each other (e.g.builder_typevisibility influences thebuilder_modvisibility)builder_type(vis)- changes the default visibility of builder struct and all of its methods (setters,finish_fn)start_fn/finish_fn(vis)- changes only the visibility of thestart_fn/finish_fnbuilder_mod(vis)- changes only the visibility of thebuilder_mod. Doesn't influence the visibility of items defined inside it (since they are used in the builder's type signature)
- [ ] Update docs about alternatives. Now it is indeed possible to extend the builder, maybe even accept by a mutable reference
- [ ] Add miri for testing unsafe code
- [x] Don't generate the state transition fn if it's not needed (no states)
- [x] Revisit naming and identifiers of members, use public_ident
- [ ] Reconsider automatic bounds on derive(...) and stuff
- [ ] Compile time benchmarks
- [ ] Docs for
IsSetandIsUnsettratis (cleanup lib.rs) - [x] Add docs to clarify which members
requiredoroptionalto the default docs - [x] Fix visibility into_equivalent. Paths may start with
crateorselfinpub(in ...) - [x] Fix selector for overwritable. In fact,
onshould behave like a match and short circuit on the first match (?) - [ ] Update ide completions
- [x] Consider references and generic types in builder(with) (add tests). Allow only named lifetimes and generics to prevent users from depending on autogenerated names for generics. Maybe traverse the all syntax and reject any mentions of autogenerated lifetimes and generics anywhere
- [ ] Document an antipattern of impl Trait for optional members
- [x] Try vectorizing transition type aliases (e.g. once for every 5 members).
- Performance win is not worth it
- [x] Make
Optionalrender in a box in docs?
TODO (separate PR)
#[builder(on(..., transparent))]support#[builder(crate = ...)]override support- Remove
expose_positional_fn
Not included in this release
- Add support for
optional()and!operators to type patterns expressions. Take a look atrustc_on_unimplementedmatching syntax. (Need to decide on the behavior here: should there be short circuit or not?). - impl From<Builder> for T? There is a new
IsCompletetrait for the state which may serve as a replacement. #[builder(field)]to add custom fields to the builder (restriction: custom field names must not collide with other names of members).#[builder(getter)]to derive getters for every member (start_fn and named).- Add support for
#[doc(hidden)],#[doc = include_str!("...")]todocs {}overrides and docs copied from the fields/args
Changelog
- ~~Changed the visibility syntax from
vis = "..."tovis(...). Usevis(pub(self))for private visibility.~~ Decided not to do this. I think= ...syntax reads better. - Allow overriding the visibility of the
builder_typeandfinish_fn - Consider adding overwritable transitions as an extension to the API rather than replacement
- Add
#[builder(with = closure)]syntax to customize setters - Add
#[builder(transparent)]forOptionfields to opt out from their special handling which makes such fields optional - Add
#[builder(state_mod)]to configure the builder type state API module - Add
#[builder(overwritable)]and#[builder(on(..., overwritable)]to make it possible to call setters multiple times for the same member - Add
#[builder(setters)]to fine-tune the setters names, visibility and docs - Improve rustdoc output
- Add info that the member is required or optional.
- For members with defaults values show the default value in the docs.
- For optional members provide a link to a companion setter. The docs for
{member}(T)setter mention themaybe_{member}(Option<T>)setter and vice versa. - Remove
__prefixes for generic types and lifetimes from internal symbols. Instead the prefixes added only of the macro detects a name collision.
- Reject empty attributes in a bunch of places
e.g. #[builder()]or#[builder]with no parameters on a member
Points for discussion
- Should the module with builder type state API be private by default? This effectively makes the type signature of the builder (except for the initial state) private. Code outside of the module where builder is defined can't name the builder type. It's possible to make that type state API public by adding
#[builder(builder_mod(vis = "pub"))] - Reject square brackets and curly braces delimiters for
builder_type,finish_fn,start_fnandonattributes syntax. Only parentheses are accepted e.g.#[builder(finish_fn(...))]or#[builder(on(...))]. This no longer works:#[builder(finish_fn[...])]or#[builder(on{...})] - The naming
#[builder(transparent)]to disable the special handling for members of typeOption<T>. Alternatives:#[builder(opaque)],#[builder(blindfold)]. - Specific format of the docs (should
Optionalbe also rendered in the box, or should it intentionally stand out less)?
Quick feedback on a simple example :
impl<State: endpoint_builder::State> EndpointBuilder<State> {
pub fn smart_crop(self) -> EndpointBuilder<SetSmart<State>>
where
State::Smart: bon::IsUnset,
{
self.smart(true)
}
}
The API is very straightforward and convenient to work with, nice work 🎉
Thank you for the feedback ❤️, this should be landed in the next couple of weeks
Walkthrough
The changes in this pull request involve significant updates to the CI workflow and project structure for the Rust crate bon. Key modifications include the renaming of jobs in the CI configuration, the addition of new benchmarking packages, and the restructuring of the workspace members in Cargo.toml. New scripts for benchmarking and installation have been introduced, alongside documentation updates to clarify usage and enhance the overall user experience. Additionally, various error messages related to the builder pattern have been refined to improve clarity and correctness.
Changes
| File | Change Summary |
|---|---|
.github/workflows/ci.yml |
Renamed job benchmarks to runtime-benchmarks, added compilation-benchmarks and cargo-miri. |
Cargo.toml |
Updated workspace members structure, added resolver = "2", and refined comments in linting config. |
benchmarks/compilation/Cargo.toml |
New package for compilation-benchmarks with dependencies and features defined. |
benchmarks/compilation/README.md |
New documentation for compilation benchmarks, including dependency installation instructions. |
benchmarks/compilation/codegen/Cargo.toml |
New package compilation-benchmarks-codegen with metadata and dependencies. |
benchmarks/compilation/results.md |
New table added for performance metrics of various commands. |
benchmarks/compilation/run.sh |
New script for benchmarking compilation processes. |
benchmarks/run.sh |
Deleted old benchmarking script. |
benchmarks/runtime/Cargo.toml |
Renamed package from benchmarks to runtime-benchmarks, updated dependency path. |
benchmarks/runtime/README.md |
Minor textual modifications for clarity. |
benchmarks/runtime/run.sh |
New script for benchmarking in runtime-benchmarks. |
bon-macros/Cargo.toml |
Updated version, specified rust-version, and modified dependencies. |
bon/Cargo.toml |
Updated description and added new feature implied-bounds. |
Various tests/integration/ui/compile_fail/*.stderr |
Introduced error messages highlighting issues with attribute usage in builder macros. |
rust-toolchain |
Updated Rust toolchain version from 1.81.0 to 1.82.0. |
scripts/install/hyperfine.sh |
New script for automating the installation of Hyperfine benchmarking tool. |
scripts/test-msrv.sh |
Removed --all-features flag from cargo clippy command. |
website/.vitepress/config.mts |
Updated sidebar navigation structure for clarity. |
website/blog/*.md |
Documentation updates for releases of bon crate, including new features and breaking changes. |
website/guide/*.md |
Various updates to improve clarity and correctness of builder usage documentation. |
website/reference/builder.md |
Restructured documentation to clarify usage of builder macros. |
Assessment against linked issues
| Objective | Addressed | Explanation |
|---|---|---|
| Generic way of passing incomplete builders around (#21) | ❓ | The changes do not explicitly address this. |
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?
🪧 Tips
Chat
There are 3 ways to chat with CodeRabbit:
- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
I pushed a fix in commit <commit_id>, please review it.Generate unit testing code for this file.Open a follow-up GitHub issue for this discussion.
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. Examples:@coderabbitai generate unit testing code for this file.@coderabbitai modularize this function.
- PR comments: Tag
@coderabbitaiin a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.@coderabbitai read src/utils.ts and generate unit testing code.@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.@coderabbitai help me debug CodeRabbit configuration file.
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.
CodeRabbit Commands (Invoked using PR comments)
@coderabbitai pauseto pause the reviews on a PR.@coderabbitai resumeto resume the paused reviews.@coderabbitai reviewto trigger an incremental review. This is useful when automatic reviews are disabled for the repository.@coderabbitai full reviewto do a full review from scratch and review all the files again.@coderabbitai summaryto regenerate the summary of the PR.@coderabbitai resolveresolve all the CodeRabbit review comments.@coderabbitai configurationto show the current CodeRabbit configuration for the repository.@coderabbitai helpto get help.
Other keywords and placeholders
- Add
@coderabbitai ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
CodeRabbit Configuration File (.coderabbit.yaml)
- You can programmatically configure CodeRabbit by adding a
.coderabbit.yamlfile to the root of your repository. - Please see the configuration documentation for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation:
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
Documentation and Community
- Visit our Documentation for detailed information on how to use CodeRabbit.
- Join our Discord Community to get help, request features, and share feedback.
- Follow us on X/Twitter for updates and announcements.
Finally, this mega-PR is ready to merge. There are two low-effort features that I'd like to include in the upcomming release and the remaining work is left only for the documentation and a blog post. More details on the remaining work are in the "TODO (separate PR)" section of the PR description. I'll try to tackle that quickly.
I'll do that in separate followup PRs. Then I'll cut off a 3.0.0-rc (release-candidate) version of bon to gather some initial feedback on all the changes and maybe potential early bugs. Hopefully, there won't be any breaking changes to the new APIs as a result of that, but I'd still like to reserve some time to let these features boil in a "preview" 3.0.0-rc version for some short period of time.