-
Notifications
You must be signed in to change notification settings - Fork 526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend prost-build to generate message builders #901
base: master
Are you sure you want to change the base?
Conversation
Notes on the present implementation:
|
a38d703
to
1fb7926
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this PR is difficult to review. Could you do these things to make it easier to review?
-
Can you explain why this APi is better then a generic builder? For example: https://crates.io/crates/derive_builder
-
Add documentation about how to use this builder.
-
Reorganize the commits so that they change a single thing with a description of what and why you do that. If that commit generally improves the codebase, consider opening a separate PR for just that commit. This allows me to review each commit individually.
-
I am not sure about this one: Is it beneficial to split the build code generator into a separate source file?
Thanks for the feedback @caspermeijn! I have rebased the branch and squashed the changes into a few topical commits.
I have added an explanation in the description comment.
I mean to do it, but first I'd like to settle on a more flexible way to configure the builders. prost_buid::Config::new()
// .mypackage.Foo gets foo::Builder and the associated Foo::builder() function
.builders(".mypackage", "Builder")
// ...
I was hesitant to make a separate module for this, as hardly any functionality used in Also, I feel that the builders should be a core feature, because as discussed in #399, this together with |
I could try to split off a PR with the refactoring changes in |
Done in #1011 and rebased on top. |
For each message, optionally generate a struct to provide the forward-compatible builder API, with inherent methods for field setters and the .build() method to produce the fully initialized message struct. If generation of builders is enabled with a name `Builder` for a message named `Foo`, an impl block with the associated fn `Foo::builder()` is appended to the top-level file scope, and the definition of the builder struct `Builder` and its methods is appended to the module `foo`. Reuse the helpers that generate field definitions for a message, but use a mode to output code for the builder fields, which must be private and devoid of docs and prost annotations. The added Config::builder method populates a PathMap with values given in the second argument to configure the builder name.
Adorn a struct with #[non_exhaustive] to showcase and test how the builder API (and Default) can be used to forward-compatibly construct values of generated message types, while the struct initializer syntax is forbidden. Some of the doc tests showcase working around name conflicts between the associated function generated for the builder API and a getter method generated by the Message derive for a field named `builder`. Also verify the no-conflict case where the field does not have a getter generated for it, so the `builder` associated fn is fine. The test crates in tests-2015 and tests-no-std are uniformly named "tests" to simplify boilerplate in the doc tests.
This allows plugging a message builder into other builders' setters for fields of the message type without the need to call .build().
The setter for field named `test` in proto must be named `r#test`.
What else does this PR need to be merged? |
self.buf.push_str(" = "); | ||
if repeated { | ||
self.buf | ||
.push_str("value.into_iter().map(Into::into).collect();\n"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A repeated type is a Vec
, but this setter doesn't allow the direct setting of a Vec
. It will reallocate. I think the setter should just use Vec
as its input type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I consider the setter a convenience method. The field is public, so there is always the option of assigning to it directly rather than going through FromIterator
.
When specialization is stabilized in future Rust, the setter can be specialized for Vec
without breaking the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An impl Into<Vec<T>>
argument looks like a reasonable compromise: apart from Vec<T>
with zero copying, it accepts &[T]
, &[T; N]
, and [T; N]
, so it should cover the most common cases without extra conversions. To set from an iterator, the caller would have to call .collect::<Vec<_>>()
self.buf | ||
.push_str("value.into_iter().map(Into::into).collect();\n"); | ||
} else if optional { | ||
self.buf.push_str("Some(value.into());\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setter for optional fields should accept a Option
. For "normal" fields I think it is good to forgo the Option
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied the same rationale as for repeated fieds: the setter is complementary and allows shorthand code, while there is the option of assigning the field directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking of the most common use case here, where the application has a valid value to set. It should be rare to need to explicitly set a field to None
, that is not addressed by Default
impls on the struct and the Option
field type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand, maybe the case of an explicitly optional field (as opposed to a "normal" field which must have a default for the uninitialized case) is special enough that the developers meant it to be used this way. I will change to it to Option
.
/// **`path`** - a path matching any number of types. It works the same way as in | ||
/// [`btree_map`](#method.btree_map), just with the field name omitted. | ||
/// | ||
/// **`builder_name`** - A name for the builder type. The struct with this name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The path can match multiple messages. I think the builder_name
should be a suffix so that it doesn't generate multiple builders with the same name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The builder is generated inside the submodule named after each message, so a collision could only occur with the message's nested types and enums. This should be rare enough that it can be solved by a specific matcher for messages where it occurs.
Stylistically, I prefer my_message::Builder
to my_message::MyMessageBuilder
or self::MyMessageBuilder
.
@@ -11,6 +11,7 @@ edition = "2015" | |||
build = "../tests/src/build.rs" | |||
|
|||
[lib] | |||
name = "tests" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? It is not related to builders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed so that the doctests I have added to the tests
crate also work in these shim crates.
@@ -0,0 +1,139 @@ | |||
//!#[doc(hidden)] | |||
|
|||
/// # Doc tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use hidden doc tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are compile_fail
test cases and other tests of generator behavior and corner cases that are of no interest to a normal library user.
Since the tests crate is publish = false
, this should not matter much though.
@@ -24,6 +24,7 @@ cfg_if! { | |||
} | |||
} | |||
|
|||
pub mod builders; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see an additional test: generate a builder for TestAllTypesProto3 and write that to file. This has two purposes:
- It adds a example of all posible setters to the PR
- It forces changes to builder API to be visible in future PRs.
The repo has examples for a test writing to file and asserting nothing changes: test_generate_no_empty_outputs
@jregistr Could you provide an additional review of the PR and the generated code? |
@@ -11,6 +11,7 @@ edition = "2015" | |||
build = "../tests/src/build.rs" | |||
|
|||
[lib] | |||
name = "tests" | |||
doctest = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is doctest = false
in both crates (also in tests-2018 which is not included in the workspace for some reason), but doctests are compiled by cargo test
even without the --doc
option.
First crack at #399
Based on #1011
Advantages over generic macro-generated builders like derive_builder:
Foo
does not need to be specially configured to not useOption<Foo>
as the argument type, we assume the setter is only used when the field is meant to be set so it'simpl Into<Foo>
.Option
, but no generics with theInto
bound here as that could obfuscate call sites more than necessary (maybe it's a wrong assumption and the argument conventions should be made uniform).impl IntoIterator
argument.i32
type that the corresponding struct field is generated with.