-
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
Nested builder support #254
Comments
The use-case makes sense to me. However, this is the first time I've heard of it being needed, so I'm not sure how widespread the need is. In approach 2, would the getters described in #7 be sufficient? You'd still have to repeat the field name in the
Could you do this the other way round, and have an attribute macro that did the necessary rewriting of #[sub_builder]
#[derive(Builder)]
struct SuperStruct {
#[sub_builder(type = "SubStructBuilder")]
sub_struct: SubStruct
} I need to brush up on proc-macro execution order, but if #[derive(Builder)]
struct SuperStruct {
#[builder(field(type = "SubStructBuilder", build = "..."))]
sub_struct: SubStruct
}
impl SuperStructBuilder {
pub fn sub_struct(...)
} |
(Sorry for the delay replying. I wrote this a couple of days ago and I could swear I hit the "comment" button.)
Thanks. Well, Arti is the first project I've done significant work on that uses builder patterns for its configuration, but I understand the builder pattern is quite common. I don't think anything we're doing in Arti is particularly unique. Rust is still quite young in some of these areas, so I don't find it surprising that I'm breaking new ground. Within Arti: I looked at my outstanding work and my tree has 24 uses of
I don't think they would be right, because the accessors need to take
That's true. But, I have seem soo many bugs where even adjacent repetition has been botched. If we end up writing code like this, the bug is too easy to write and too hard to spot:
I believe that would be possible, but I don't think it is a very good design. It seems like an antipattern to set out to make a proc macro whose purpose is to massage the input for a diffeerent proc macro. The layering of abstractions becomes quite confusing, and the "wrapper" macro now has to unparse the inner proc-macro's input. I think this is a thing one would only do if there were compelling reasons why the "inner" macro couldn't be modified to add the desired features. (It's a different matter if the two proc macros are semantically orthogonal: I often combine proc macros and am sometimes disappointed when they interact when I wish they wouldn't...) |
I'm not sure this is true; it's still composition. It still gets the benefits of Rust's safety guarantees, though the errors may not be quite as nice.
I'm not sure what you mean by "unparse" here - the
I'm going to take a closer look at the usage of I do have a fairly strong dislike for the fact that |
(oops, pasted this comment into #253 by mistake, rather than here. I will delete it there.)
Thanks. One thing you might want to look at is the art MR I just made where I am trying to sort out our config API for "list-like" config fields. Some of these are plain values, and some are themselves things with builders. Here's the macro_rules I currently have to help drive sub_builder: I think this could usefully be done by a proc macro, perhaps in derive_builder, but it has quite a large API surface.
Yes. However, of course, I found the fact that the proc_macro code in #253 is so small quite encouraging.
Well, the But I guess maybe you are also looking at the new error type I introduced. You're right that it is quite specific to the sub-builder case. Possibly it would be better to separate out the functions of "error occurred in a particular field" and "error was a sub-builder error" - but IME flat error conversion hierarchies are normally easier to work with than repeated conversions. So having a specific error for precisely "sub builder for field failed" doesn't seem too bad. The logical conclusion of this argument is is that if derive_builder grew built-in support for
That sounds like a reasonable feature to me. But there are of course two needless occurrences of the field name in the "just use
Anyway, thanks for your consideration. FYI, currently arti is using a git dependency for derive_builder. We can't release in that state, so it is possible that we may need to upload a fork of derive_builder to crates.io. My colleagues are not worried about the implications of having a pet fork of derive_builder, but I would very much like to avoid it. |
Having looked at the linked file and slept on this for a few days, I'm still not feeling that this has enough general value to merit the API expansion. While I'm not as close to the
If you go this route, please name it something like I'm going to leave this issue open in case others hit similar use-cases. |
OK, thanks. (Your suggestion to use I haven't spoken to my colleages, but my usual practice is to name forks "originalname-fork-ourproject". That seems a widely adopted convention in the ecosystem,. so in this case that would be something like "derive-builder-fork-arti". Would that be OK with you? And yes of course I'll adjust the docs. |
I'd also love to see examples in the doc on how config-rs can be combined with builder patterns / common builder libs. |
I recently started using My existing code looks like: ClientBuilder::default()
.api_url("my_api_url"),
.auth_handler(
AuthHandlerBuilder::default()
.jwt_factory(
JwtFactoryBuilder::default()
.config(
ConfigBuilder::default()
.jwt_audience("jwt audience")
.build()?,
)
.encoding_key_from_bytes("my encoding key".as_bytes())
.build()?,
)
.build()?
)
); This is what I'd like it to look like: let mut cb = ClientBuilder::default(); // ClientBuilder
cb.api_url("my_api_url") // ClientBuilder
.auth_handler_builder() // AuthHandlerBuilder
.jwt_factory_builder() // JwtFactoryBuilder
.encoding_key_from_bytes("my encoding key".as_bytes()) // JwtFactoryBuilder
.config_builder() // ConfigBuilder
.jwt_audience("jwt audience"); // ConfigBuilder
cb.build(); // Client |
@cowlicks: This is roughly what we have with The docs there for the feature have an example. If others had interest in this, I could be persuaded to perhaps make more regular releases of the fork. |
@ijackson I would consider download/star/dependent crate volume for the fork as evidence of a need for this in the main builder package, so if there appears to be substantial demand we can reopen this issue. |
In Arti, we are using
derive_builder
for our configuration. Arti's configuration is fairly complicated, and isn't simply a flat list of keys and values. Higher-level configuration structs contain lower-level ones.Right now we have two approaches to this. In both cases the contained ("sub-struct") derives
Builder
and the containing "super-struct" containsSubStruct
. The two approaches are:(i) the containing struct (super-struct) derives
Builder
, with no special builder instructions; so the super-struct containsOption<SubStruct>
, and the user must callsuper.sub_struct(SubStruct::builder().sub_param(42).build())
or whatever. This is clumsy, and also makes it very awkward to adjust differentsub_param
of the sameSubStruct
other than at the same place in the calling code (and if a user tries to do so, incautiously, they may easily overwrite earlier settings). Another problem with this is that we need our configuration to beDeserialize
. To centralise the defaulting, validation and compatibility code (in thebuild
method), we deserialize the input configuration file into the builder, not the built struct. But if the super-struct contains a built sub-struct, we need to have the sub-structure'sbuild()
called in the middle of deserialisation by using special serde escape hatches, making things really quite complex (and making it hard to do good error handling, and handling of compat for old config inputs).(ii) the containing struct contains
SubStructBuilder
, and we hand-write an accessor method. We hand-writeSuperStruct::build
, too, to call thebuild
methods on the sub-structures. This provides a pretty good API, where you can saysuper.sub_struct().sub_param(42)
, and just callbuild
once at the end. But it involves us handwriting the accessors and the build function. To give an idea of the size of the problem: in Arti, in two of our higher-level structs, that amounts to about 300 loc of boilerplate, which I would like to get rid of.So, I would like to do (ii) but have the code autogenerated. derive_builder has most of the necessary code generation, but not a convenient way to specify the desired behaviour. Prior to #245 it would be impossible to combine such "nested" fields with derive-builder-generated fields and impls, but now one can do something like:
(and hand-write the accessor). This is pretty nasty, and not something you'd do unless you really wanted to mix sub-struct fields with leaf fields.
I considered whether it would be best for this functionality to live in derive_builder, or elsewhere. The generation of accessors could live elsewhere, although the necessary attributes would have to be smuggled through derive_builder onto the builder struct through
builder(derive)
andbuilder_field_attr
. But I think the generation of appropriate builder code ought to live in derive_builder, so that it is possible to have a struct that derives Builder and which contains both nested and leaf fields, without having to write ugly derive instructions like the one I show above. (FTAOD, I don't think we currently have any structs in Arti that are mixed in this way, but it seems like it should be made possible.)So #253 is what I ended up with. To give you an idea of what it looks like in use, here is one of the relevant commits on my corresponding branch of arti.
The text was updated successfully, but these errors were encountered: