idanarye / rust-typed-builder

Compile-time type-checked builder derive
https://crates.io/crates/typed-builder
Apache License 2.0
928 stars 52 forks source link

Context for builder #75

Open wackazong opened 1 year ago

wackazong commented 1 year ago

I would love to see a feature that allows to set a context for the builder. The context could then be used in the default functions. To keep the builder() function as is, there could be a .context() function to set the context. Then we could use something like::

#[builder(default_code = "self.context()")]

What do you think?

I find the code of this crate quite complex for me as a Rust beginner, but I would be happy to do a PR if given some pointers on where to start.

wackazong commented 1 year ago

My use case would be to allow for the control of default values for keys and timestamps in different testing environments.

idanarye commented 1 year ago

To keep the builder() function as is, there could be a .context() function to set the context.

So... something like this?

#[derive(TypedBuilder)]
#[builder(context(/*Syntax TBD*/))]
struct Foo {
    #[buidler(default = self.context())]
    bar: i32,
}

// This will work:
Foo::builder().context(Context::new()).build();

// This will work:
Foo::builder().bar(42).build();

// This will not:
Foo::builder().build();

This can be a bit hard to model, but what if we require the context to always have a default?

Also, rather than a "context" method, I'd rather do something ike this:

#[derive(TypedBuilder)]
#[builder(context(
    // Will be treated like a field that does not get into the final struct
    number: i32 = 4,
))]
struct Foo {
    #[buidler(default = number * 10 + 2)]
    bar: i32,
}
wackazong commented 1 year ago

Yes, the first proposition would be great. The context would just need to be available in the builder, not in the final struct. I would like to set the context at runtime, therefore an attribute macro as you suggested would not serve me.

Maybe like this?

struct Context;

impl Context {
    fn bar() {
        ...
    }
}

#[derive(TypedBuilder)]
struct Foo {
    #[builder(use_context_for_default)]
    bar: i32,
}

// if a field is marked with use_context_for_default then a context needs to be supplied
// and the default value is derived from the context passed in at runtime

// context will be required for each call of builder if default value is not given
Foo::builder().context(Context::new()).build();

// This will work:
Foo::builder().bar(42).build();

// This will not:
Foo::builder().build();
idanarye commented 1 year ago

I really don't want to move the defaults elsewhere. If I have to specify dependencies, I'd rather make a breaking change and do something like this:

#[derive(TypedBuilder)]
#[builder(context(
    x: i32 = 1,
    y: i32 = 2,
))]
struct MyStruct {
    // Depends only on `x`
    #[buidler(default = |x| x + 1)]
    foo: i32,

    // Depends only on `y`
    #[buidler(default = |y| y + 2)]
    bar: i32,

    // Depends on both
    #[buidler(default = |x, y| x + y + 3)]
    baz: i32,

    // Depends on nothing
    #[buidler(default = || 4)]
    baz: i32,
}

But... I really don't want to jump into the complexities of these dependencies, and I think that if it's just for the context fields I can get a way with using default values.

wackazong commented 1 year ago

For me the most important would be to pass in the context at runtime and not at compile time.