Skip to content
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

Support generating builders from traits and trait impls #123

Open
Veetaha opened this issue Sep 10, 2024 · 4 comments
Open

Support generating builders from traits and trait impls #123

Veetaha opened this issue Sep 10, 2024 · 4 comments
Labels
design needed The feature requires more design effort feature request A new feature is requested

Comments

@Veetaha
Copy link
Collaborator

Veetaha commented Sep 10, 2024

Read this if you found this issue because you want to have builder syntax for methods in traits

The design for this feature is rather hard and is yet to be researched, but bon should eventually be there! If you'd like to benefit from the builder syntax in your traits (e.g. assign default values, allow the caller to skip optional parameters, etc.), in the meantime I recommend you to just define a separate "parameters" struct annotated with a #[derive(bon::Builder)].

Example:

#[derive(bon::Builder)]
struct PutStarParams {
    #[builder(into)]
    github_repo: String,
    // .. other params
}

trait GithubClient {
    fn put_star(params: &PutStarParams) -> Result;
}

impl GithubClient for MyClient {
    fn put_star(params: &PutStarParams) -> Result { /**/ }
}

let client = MyClient::new();

// Use builder syntax to construct the params struct:
let params = PutStarParams::builder()
    .github_repo("elastio/bon")
    .build();

// call the trait method:
client.put_star(params)?;

A note for the community from the maintainers

Please vote on this issue by adding a 👍 reaction to help the maintainers with prioritizing it. You may add a comment describing your real use case related to this issue for us to better understand the problem domain. Sharing your use case will help with moving this issue forward!

Main issue body

We need some syntax to allow developers to generate builders from traits and trait impl blocks. This way it should be possible to define a trait that uses builder syntax for its methods.

The main problem here is that trait declarations and trait impl blocks are syntactically separated. We can also have multiple impl blocks. It would be awesome if all the impl blocks didn't have to redeclare the trait methods' parameter lists. The macro should probably generate a struct with input parameters that is then passed into the trait methods.

This all has a bunch of nuances (e.g. support for dyn Trait objects, methods without self). This feature requires a huge design effort.

@Veetaha Veetaha added design needed The feature requires more design effort feature request A new feature is requested labels Sep 10, 2024
@dzmitry-lahoda
Copy link
Contributor

assuming it is possible to do, when it is useful?

@Veetaha
Copy link
Collaborator Author

Veetaha commented Sep 11, 2024

As an example, the current Hashicorp Vault API uses functions with lots of positional parameters and it always accepts an impl Client.

If only we had a way to generate builder syntax for that Client trait, that would make it possible for vaultrs to use the syntax like this:

let client: impl Client = /**/

client
    .create_certificate()
    .mount("...")
    .cert_name("...")
    .aws_public_cert("...")
    .send()
    .await?;

@eboody
Copy link

eboody commented Feb 22, 2025

This would be super useful if we could do something like

trait MyTrait {
   fn some_fn(&self, field_1: i32, field_2: i32) -> Whatever
}

#[bon]
impl MyTrait for MyStruct {
   #[builder(start_fn = some_fn)]
   fn some_fn(&self, field_1: i32, field_2: i32) -> Whatever {
      todo!()
   }
}

and then be able to do

let my_struct: MyStruct = ..;

let whatever: Whatever = my_struct.some_fun().field_1(..).field_2(..).build();

Is that something you think is possible @Veetaha ?

P.S. this crate is a game changer! thank you for all of this work!

@Veetaha
Copy link
Collaborator Author

Veetaha commented Feb 22, 2025

Hi! The goal here is to provide the builder syntax when the user uses the trait as you mentioned in here:

and then be able to do

let my_struct: MyStruct = ..;

let whatever: Whatever = my_struct.some_fun().field_1(..).field_2(..).build();

That is clear. I think it is possible to achieve that goal indeed.

The main problem here is the design for the macro syntax, behaviour and the generated code pattern. For example, with the code in your proposal it won't work, because the trait declaration is just this:

trait MyTrait {
   fn some_fn(&self, field_1: i32, field_2: i32) -> Whatever
}

Nothing tells the compiler that the method some_fn supports builder syntax from this context. Given that the trait declaration and its implementations can live in different places (even in different crates), the compiler can't know that this trait supports builder syntax in this case.

So the trait should be annotated with the builder macro too. But then what code does it generate? If it generates a builder struct, then how can we make sure that the name of that builder struct is available in the scope where the implementation of the trait is defined?

Also, it would be cool if we didn't even have to annotate the trait implementation with the builder macro at all, thus hiding the fact that bon is used to support the builder syntax for the trait.

Another cool to have thing is that the builder macro would generate the struct of parameters for the method so that the trait implementation doesn't have to enumerate the arguments and their types (i.e. duplicate the signature of the method in the trait declaration). This would allow adding new optional parameters to the trait's method without breaking its implementations:

#[bon]
trait Trait {
    #[builder]
    fn method(a: u32, b: u32) -> u32 {}
}


// Somewhere potentially in another crate

use path::to::Trait;

struct Struct;

impl Trait for Struct {
    // In this case if the trait declaration is extended with a new parameter `c: Option<u32>` for example,
    // this trait impl will still compile (the generated `MethodParams` struct should be `#[non_exhaustive]`).
    fn method(params: MethodParams) -> u32 {
        params.a + params.b
    }
}

The challenge here is that in this case there are two users of this feature - the code that calls the starting function to create the builder and the code that implements the finishing function of the builder (i.e. the trait method). And I want to make sure both of these sides have good backwards compatibility guarantees that are equal to (or even better) that with the approach of defining the parameters struct and using it manually as suggested in the header of the issue as the alternative approach for builders for trait methods directly.

The main work in this issue is designing this solution (implementing it should be rather easy once we have the design).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design needed The feature requires more design effort feature request A new feature is requested
Projects
None yet
Development

No branches or pull requests

3 participants