colin-kiegel / rust-derive-builder

derive builder implementation for rust structs
https://colin-kiegel.github.io/rust-derive-builder/
Apache License 2.0
1.32k stars 88 forks source link

Support post build functionality #259

Closed marlon-sousa closed 2 years ago

marlon-sousa commented 2 years ago

Use scenarius

some times, one or more fields of your struct need to be built according to other field’s values. May be you have a layout which needs tpo know all other field’s values to be built, or that you have some field that needs to be calculated only once for the struct and you don’t want to recalculate it every time one of the fields are set in the builder. There are several situations in which a post build event might be useful.

  1. You might have a field which needs to be calculated when the struct is built, but deppends on several other field values to calculate.
  2. You might want to trigger an event whenever a new struct of a given type is built, so that other components of your application can react.
  3. You might want to perform a complete validation which needs to know values of all fields to be performed.

The classical ways to solve this problem would be:

  1. call a method wich accepts &mut self in the target struct as soon as it is built. This method would make validations, trigger events, calculate fields. The main issue with this strategy is one can easily forget to call the post build method or function, and even if they don’t, this function is effectively finishing to build the struct, so it shouldn’t be decoupled from the building process at all. This post build function or method also shouldn’t be part of the public api.
  2. Customize the setters of all fields affecting the field to be calculated and update it accordingly. The main issue with this strategy is that This would make room for repeated code, might run an expensive calculation on all calls to affected setters and would couple the set of the calculated field with the setters of all the affected fields, all things we want to avoid.

Proposed solution

The post_build parameter of #[derive(builder(build_fn))] allows you to provide a function or a method which gets called by the build() method of the builder struct as soon as the target struct is built. this solves the proposed problem, creates great ergonomics and decouples the set of the calculated field and all other side effects you might want to add from the setters of other fields In order to use post_build functionality, you can declare #[builder(build_fn(postbuild = “path::to::fn”))] to specify a post build function which will be called as soon as the target struct is built, still inside the builder function. The path does not need to be fully-qualified, and will consider use statements made at module level. It must be accessible from the scope where the target struct is declared. The provided function must have the signature (&mut Foo) -> Result<, String>; the Ok variant is not used by the build method.

Implementation example

use derive_builder::{PostBuildError, UninitializedFieldError};

#[macro_use]
extern crate derive_builder;

#[derive(Debug, Clone, Builder, PartialEq, Eq)]
#[builder(build_fn(post_build = "LoremBuilder::post_build"))]
pub struct Lorem {
/// a number
    number: i32,

    /// big_number
    /// this will be calculated by the post build function, so it should not (though mothing prevents it from) make a setter available.
    /// It also should have a default value, either specified in the builder directive or delegated to the Default trait of the type
    #[builder(setter(skip), default = "false")]
    big_number: bool,
}

impl LoremBuilder {
    /// performs post build operation
    fn post_build(target: &mut Lorem) -> Result<(), String> {
        // post build validation
        if target.number <= 0 {
            return Err("Number must be greater than 0".to_string());
        }
        // remember that we didn't set the big_number field. We deppend on the number field to decide if this instance has or hasn't a big number.
        // This can only be safely known when the struct has just been built but nobody for sure yet used it
        if target.number > 60 {
            // initialize the big_number field
            target.big_number = true;
        }

        Ok(())
    }
}

fn main() {
    // post build does not modify the big_number default field value
    let x = LoremBuilder::default().number(20).build().unwrap();
    assert_eq!(x.big_number, false);

    // post build modifies the big_number field value
    let x = LoremBuilder::default().number(80).build().unwrap();
    assert_eq!(x.big_number, true);

    // post build validation fails
    let x = LoremBuilder::default().number(-1).build().unwrap_err();
    let correct_variant = match x {
        LoremBuilderError::UninitializedField(_) => {
            panic!("Should get a post builder error, got a uninitialized field error instead")
        }
        LoremBuilderError::PostBuildError(e) => true,
        LoremBuilderError::ValidationError(_) => {
            panic!("Should get a post builder error, got a validation error instead")
        }
    };
    assert!(correct_variant);
}

Implementation

A reference implementation can be found at #258

TedDriggs commented 2 years ago

This is simple to implement without a change to the crate:

  1. Rename and make private the provided build_fn using build_fn(private, name = "...")
  2. Add an inherent public method to the builder, with the same signature as the private build_fn.
  3. Do any post-build modification of the built struct there.

The result is indistinguishable to the caller from this attribute, and the implementation is virtually identical.

sowbug commented 1 year ago

The advice given was very concise, and I had a hard time following it. For others in this situation, here's a working sample:

#[derive(Builder)]
#[builder(build_fn(private, name = "build_from_builder"))]
struct MyStruct {
    a: i16,
    b: i16,
    #[builder(setter(skip))]
    sum: i16, // this is to be computed internally rather than set
}
impl MyStruct {
    fn calculate(&mut self) {
        self.sum = self.a + self.b;
    }
}
impl MyStructBuilder {
    pub fn build(&self) -> Result<MyStruct, MyStructBuilderError> {
        let mut s = self.build_from_builder();
        if let Ok(st) = s.as_mut() {
            st.calculate();
        }
        s
    }
}