-
Notifications
You must be signed in to change notification settings - Fork 19
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
Builder ergonomics #95
Comments
This was a consideration when initially designing the builders, so the mutating vs consuming builder question isn't a new one. I believe at some point, there was an ownership concern that required the final In your example, if the builder methods took/returned mutable references: // This can't work - builder is a reference that outlives the data it points to.
// let mut session_builder = Session::builder()
// .authtoken(…);
// if let Some(session_meta) = session_metadata {
// session_builder.session_metadata(session_meta);
// }
//
// let endpoint_builder = session_builder
// .connect()
// .await?
// .tcp_endpoint()
// .remote_addr(…)
// .forwards_to(…);
// Instead, it has to be:
let mut session_builder = Session::builder(); // initial bind
session_builder.authtoken(...); // initial options, separate statement
if let Some(session_metadata) = session_metadata {
session_builder.metadata(session_metadata);
}
let mut endpoint_builder = builder
.authtoken(…)
.connect()
.await?
.tcp_endpoint(); // initial bind
endpoint_builder
.remote_addr(…)
.forwards_to(…); // initial options, another extra statement
if let Some(metadata) = metadata {
endpoint_builder.metadata(metadata);
}
for deny_cidr in deny_cidrs {
endpoint_builder.deny_cidr(deny_cidr);
}
let tunnel = endpoint_builder.listen().await?; whereas with the owning-builder approach: let mut session_builder = Session::builder()
.authtoken(...); // options return the builder, so you can call them at bind-time
if let Some(session_metadata) = session_metadata {
// But you do need to reassign it if you want to "mutate" your builder
session_builder = session_builder.metadata(session_metadata);
}
let mut endpoint_builder = builder
.authtoken(…)
.connect()
.await?
.tcp_endpoint() // Again, no extra statement needed here - options are in-line
.remote_addr(…)
.forwards_to(…);
if let Some(metadata) = metadata {
endpoint_builder = endpoint_builder.metadata(metadata);
}
for deny_cidr in deny_cidrs {
endpoint_builder = endpoint_builder.deny_cidr(deny_cidr);
}
let tunnel = endpoint_builder.listen().await?; I tend toward preferring expression-oriented/functional APIs, which I think is why the forced separation between binding and initial options for mutating builders has always felt kludgey to me. I'm willing to be convinced, but it's a pretty big (if a bit rote) change to make. I'll give you that the hoops you have to jump through to mutate it in another struct behind struct Foo {
builder: Option<ngrok::config::TcpTunnelBuilder>,
}
impl Foo {
fn set_meta(&mut self, meta: impl Into<String>) {
self.builder = self.builder.take().map(|builder| builder.metadata(meta));
}
} Your macros are solving a different problem, or rather a couple of different problems:
// Abuse the IntoIter implementation for Option as well
builder = thing
.into_iter()
.fold(builder, |builder, value| builder.method(value));
|
You are bringing up good points. By declaring a mutable binding and using
We actually support both TCP and HTTP tunnels in the original code but simplified the example in my original post: Feel free to close the issue, or otherwise use it to track this potential change. |
Good news - changed my mind on this one! Realized that owning builders are really bad when you want to have fallible builder methods, since there's no hope of recovery without plumbing the builder back through every So now, all of the builders are mutable and some of the errors have been moved from connect-time up to configure-time 🎉 |
Thank you for the update!
That’s actually nice! 😄 |
Hi! 👋
First, let me thank you for the great crate. It was a breeze to add initial support for ngrok in our product, Devolutions Gateway.
However, I noticed all the builders are "consuming". This means, we can’t write straightforward code such as:
This is really important when the configuration must be dynamic. See how I had to resort to macro tricks to get something somewhat "clean": Devolutions/devolutions-gateway#442
Here is an excerpt:
This is especially bad for collections (basic_auth, allow_cidr, deny_cidr, …).
For reference, I had a similar situation in our IronRDP WASM module: SessionBuilder. In order to interface with JavaScript in a clean way, the builder methods must not take ownership of
self
. Using aRefCell
internally is often the best solution. (An additional requirement, that ngrok-rs can ignore, was for the builder to be cheap to clone, hence theRc
.)Rust official Style Guidelines book is also a good read: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
The text was updated successfully, but these errors were encountered: