rust-typed-builder icon indicating copy to clipboard operation
rust-typed-builder copied to clipboard

Allow invariant checks in `.build()`

Open Banyc opened this issue 3 years ago • 10 comments

I want something like this:

pub struct S {
    x: i32,
    y: i32,
}

impl S {
    fn check_rep(&self) {
        assert!(self.x < self.y);
    }
}

When .build() is called, I also want check_rep to be called.

Banyc avatar May 22 '22 07:05 Banyc

Just stumbled upon this issue while looking through the repo, but you could do something like this:

pub struct S {
    xy: Xy
}

pub struct Xy {
    x: i32,
    y: i32,
}

impl Xy {
    pub fn new(x: i32, y: i32) -> Result<Self, Whatever> {
        if x < y {
            Ok(Self { x, y })
        } else {
            Err(/* whatever */)
        }
    }

    // also add some getters for x and y
}

i.e. just encode the invariants into the type system

benluelo avatar Aug 21 '22 00:08 benluelo

@benluelo its like a builder pattern and I have another idea:

pub struct S {
    x: i32,
    y: i32,
}

pub struct SBuilder {
    pub x: i32,
    pub y: i32,
}

impl SBuilder {
    pub fn build(self) -> Result<S, Whatever> {
        if self.x < self.y {
            Ok(S { x, y })
        } else {
            Err(/* whatever */)
        }
    }
}

Banyc avatar Aug 21 '22 06:08 Banyc

I would implement it like this:

fn build(self) -> Result<S, Whatever> {
    …
    validate(S {x, y})
}

Where validate is a function pointer fn(S) -> Result<S, Whatever> that is specified as a struct builder attribute #[builder(prebuild = "validate")].

I would call it something like prebuild instead of validator because it can be used for post-processing and not only validation.

@idanarye Does this look fine to you?

mo8it avatar Jun 07 '23 14:06 mo8it

I can tackle this if @idanarye approves.

Techassi avatar Jun 16 '23 11:06 Techassi

What's the signature of validate? How does the TypedBuidler macro know what the error type is? Also, why prebuild when it happens after the struct is built? Shouldn't it be postbuild?

idanarye avatar Jun 16 '23 15:06 idanarye

Since the crate is split now and we can bring in regular structs, who about something like this:

#[derive(TypedBuilder)]
#[builder(postbuild)]
struct S {
    x: i32,
    y: i32,
}

impl PostBuild for S {
    type Output = Result<Self, Whatever>;

    fn process(self) -> Self::Output {
        validate(&self)?; // or just do the validation here
        Ok(self)
    }
}

#[builder(postbuild)] will tell the macro to use PostBuild::Output and PostBuild::process. Alternatively, the macro could always use the PostBuild trait, but without #[builder(postbuild)] it would generate a simple implementation of it (and when Rust finally stabilizes specialization we could have a blanket implementation and remove this setting)

idanarye avatar Jun 16 '23 15:06 idanarye

Nice approach! Using PostBuild only when the attribute is specified sounds better to me.

mo8it avatar Jun 16 '23 18:06 mo8it

I wonder about that. I think always using PostBuild and generating one when the setting is not passed will result in simpler code because we won't have to conditionally change the generated code of the build method.

idanarye avatar Jun 16 '23 20:06 idanarye

Yeah I agree, we should always use the PostBuild::Output. This simplifies the code generation for the .build() method. I will open a PR and will start working on this.

Techassi avatar Jun 19 '23 09:06 Techassi

I created an initial draft PR over at #95 which implements this feature. It should be noted that it currently is a WIP PR.

Techassi avatar Jun 19 '23 14:06 Techassi