-
Notifications
You must be signed in to change notification settings - Fork 89
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
Vision proposal #79
Vision proposal #79
Conversation
- move all integration tests into `derive_builder` crate - shrink `derive_builder_test` crate into `tests/dummy-crate`
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.
Thank you very much for starting this general discussion and your effort! 👍
I've left a few inline comments.
TL;DR
- I still have some concerns with indirect complex derives like
serde
, which we should discuss in Add derives on derived FooBuilder struct #43. - In terms of general direction, I'd prioritize static validation of required fields and integrated validation of values higher than the kind of flexibility suggested in this proposal. But hopefully we can still work out some compromises. :-)
I hope it doesn't sound too critical - I think it's good to see and discuss these kind of tensions in the design space. So thank you again. :-)
@@ -0,0 +1,279 @@ | |||
# Vision |
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.
We want to generate code that is as idiomatic as possible. Please add a corresponding statement and a reference to this unofficial design pattern.
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.
Done.
vision_doc.md
Outdated
* Function arguments | ||
* Wire/file/database inputs | ||
|
||
The builder should be focused specifically on the assembly of inputs and validating the completeness of the parameter set; additional validation should be handled by a constructor function provided by the target type. That constructor function may be public or private, depending on whether or not the crate author wants to _require_ the use of the builder. |
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 can see your point in decoupling validation from being a builder only thing.
But how that validation happens is a detail IMO and should not be decided on this prominent level.
The vision should be very high level. For me a Builder
(on a high level) is also about validation. If I have a PizzaOrder
and Pizza
object, then order.submit()
should check if I can have 1 kg of tuna on one pizza. :-)
Everything beyond that seems to be to detailed for the entry paragraph of the vision for me.
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've moved it to a sub-section, but I do think that having some level of opinion on where validation fits in is significant. (Also, not sure where that statement would go otherwise as the doc stands now).
vision_doc.md
Outdated
## Documentation | ||
Authors should focus documentation efforts on the target type. | ||
* If the builder is the only way to create the target type, then the target type should discuss the requirements for its own creation. | ||
* The builder can automatically include a link to the target type doc HTML from its own struct-level doc comment. |
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.
(small sidenote: this link currently breaks if the target type name changes due to re-export under a different name.)
vision_doc.md
Outdated
* The builder can automatically include a link to the target type doc HTML from its own struct-level doc comment. | ||
|
||
## Imports | ||
A crate/module should always export the target type of a builder to allow the _built_ type to appear in function and struct type declarations. The exporting crate should _generally_ also expose the builder type, but may choose to exclude it from a prelude or the crate root, preferring to expose it in a child module. The crate is also free to keep the builder for its own internal use. |
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.
Ideally I would like to support both workflows. I think both have valid use cases. Could you perhaps change it to something less opinionated like: Both of these should be possible: ?
use FooBuilder;
let foo = FooBuilder::default().foo(1).build()?;
use Foo;
let foo = Foo::builder().foo(1).build()?;
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.
If only FooBuilder
is exposed, then the importing crate has no way to refer to the constructed type in structs or function signatures. I can't think of a case where that's desirable.
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 possible usecase could be where you always just send the constructed value back into the library. A highly examplish example:
pub fn block_from_ip(ip: IpAddr) {
let rule = firewall::RuleBuilder::default()
.action(firewall::RuleAction::Block)
.from(ip)
.build()
.unwrap();
firewall::add_rule(rule);
}
But on the other hand the Foo::builder()
approach works there as well and I don't see any situation where Foo::builder()
would not work just as well as FooBuilder::default()
vision_doc.md
Outdated
## Imports | ||
A crate/module should always export the target type of a builder to allow the _built_ type to appear in function and struct type declarations. The exporting crate should _generally_ also expose the builder type, but may choose to exclude it from a prelude or the crate root, preferring to expose it in a child module. The crate is also free to keep the builder for its own internal use. | ||
|
||
For the case of builder population with statically-known values or through explicit construction from function arguments, the target type should (optionally) expose a static `builder()` method which returns the builder type. In addition to keeping the imports shorter, this will appear in RLS "find all reference" queries by type, will be updated automatically during type renaming within the workspace, and will show docs for the target type on type hover (requesting docs on the static method will get the user to the builder-specific documentation). |
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 don't understand this. Can you explain this?
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.
Added links to the relevant RLS methods.
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.
Ok, now I get it. You rename Foo
to Bar
and the RLS has you covered unless you explicitly named FooBuilder
. Because that will be renamed implicitly to BarBuilder
and the RLS will not know about that. :-)
vision_doc.md
Outdated
For the case of builder population with statically-known values or through explicit construction from function arguments, the target type should (optionally) expose a static `builder()` method which returns the builder type. In addition to keeping the imports shorter, this will appear in RLS "find all reference" queries by type, will be updated automatically during type renaming within the workspace, and will show docs for the target type on type hover (requesting docs on the static method will get the user to the builder-specific documentation). | ||
|
||
## Fallible vs. Infallible Builders | ||
The infallible-builder-with-required-fields case is interesting, but brittle; it can only be achieved by generating a custom constructor function which takes all required values up front, and creates a type which cannot be a drop-in replacement for a fallible builder. |
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 paragraph is more a discussion than a vision, but point taken. However I think it's an important use case to allow static reasoning about required fields (at compile time). This is also what typesafe builders #33 are about. IMO typesafe builders as described in #33 should be an essential part of the vision - and there's a lot left to be discussed IMO.
1. Environment variables | ||
1. A TOML config file | ||
|
||
The union of the 3 sources must provide all the required values, or the `ConnectionPool` cannot be created. No individual source is required or expected to be complete. |
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.
nice use case. :-)
vision_doc.md
Outdated
host: host, | ||
api_key: api_key, | ||
timeout: timeout | ||
} |
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.
missing socket: socket,
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.
Fixed.
vision_doc.md
Outdated
/// inherent `builder` function on the target type. | ||
#[derive(Debug, Clone, Builder, Serialize)] | ||
#[builder(setter(into), try_setter, preserve_attrs(serde), derive = "Deserialize", build_fn(skip), target_factory)] | ||
pub struct StatRequest { |
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.
That looks complicated. I'm ok with #[builder(derive(Debug))]
etc., but I'm still very sceptical about that level of indirection with complex derives like serde
for different reasons stated in #43. We should continue the discussion about these things there.
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.
Is your concern the preserve_attrs(serde)
part, or the whole thing?
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.
Actually a bit of both. That's a wall of attributes and it's hard to understand what this is doing. preserve_attrs(serde)
and all those serde annotations also contribute significantly to that reasoning footprint.
fn try_from(v: StatRequestBuilder) -> Result<Self, Self::Error> { | ||
v.build() | ||
} | ||
} |
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 feel like it's a bad sign, that you have to implement a constructor, a build method and TryFrom
. That's a lot of boiler plate left. All that derive_builder does here is generate a secondary struct with getters and setters.
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 TryFrom
could be automatically generated if the user told us that the build method was implemented and what its name was, but I take your point. I was also trying to avoid any magic that depended on field order being the same in the struct declaration and the constructor, although something could be done with that which would make the code shorter.
I tried to pick an example where the validation logic was non-trivial and would need to be applied regardless of how the object was assembled.
Here's the vision proposal doc related to discussion of #67. @colin-kiegel and @faern, you should now be able to add inline comments to the proposal.
EDIT: Rendered.