groovybuilder

Groovy merge 2 builder classes


I have a big class Customer for which I am using groovy's @Builder annotation to generate builder classes. This works fine but while writing unit tests what I want to do is to define a customer with all the default values and then from my test I can pass some overrides using the builder class. I do not want to pass the field names for overwriting the fields because that is error prone.

Below is my builder class

@Builder(builderStrategy = ExternalStrategy, forClass = Customer.class, includeSuperProperties = true)
class CustomerBuilder {

}

This is my test builder class it accepts a parameter of class CustomerBuilder and calling method from unit test, is there any way I can merge these two builder to generate a customer object which has default values and the overrides

    class TestDataBuilder {

    public TestDataBuilder withDefaultCustomerOverrides(CustomerBuilder customerBuilder) {
        CustomerBuilder customerBuilderDefault = new CustomerBuilder()
                .modifiedDateTime(LocalDateTime.now())
                .custName("Test Client")
        
        //somehow merge the two builders
        
        return customerBuilderDefault.build() 
    }

}




TestDataBuilder testDataBuilder = new TestDataBuilder()
        testDataBuilder.withDefaultCustomerOverrides(new CustomerBuilder().custName("Custom Test Client"))

Solution

  • The builders are just containers for the data soon to be used to create the real object. With the default builders there is no knowledge in the builder object, whether a value was set or not (I'm sure, you can create this via a strategy you have to create first most likely yourself).

    So we can not rely on the data inside the builder to tell us, what values have been set: was id(null) called to set the id to null, or is it the initial default? So how to merge those two containers?

    So instead of passing a builder instance to merge with the default builder instance, pass in a function, that transforms the builder. The functions for common, non-default setups you also can keep around and then compose/chain them.

    E.g. (see the XXX comments)

    import java.time.Instant
    import groovy.transform.builder.*
    import groovy.transform.ToString
    import java.util.function.Function
    
    @ToString
    class Customer {
        Long id
        String name
        String comment
        Instant createdTS
        Instant modifiedTS
    }
    
    @Builder(builderStrategy = ExternalStrategy, forClass = Customer.class, includeSuperProperties = true)
    class CustomerBuilder {
    }
    
    // XXX: pass in a function for the transformation and apply it on the default builder
    Customer withDefaults(Function<CustomerBuilder,CustomerBuilder> transformBuilder) {
        transformBuilder.apply(
            new CustomerBuilder()
                .name("Default")
                .createdTS(Instant.now())
        ).build()
    }
    
    println withDefaults {
        it.id(42)
            .modifiedTS(Instant.parse('2020-01-01T00:00:00.0Z'))
    } // → Customer(42, Default, null, 2024-04-27T08:13:30.083152202Z, 2020-01-01T00:00:00Z)
    
    // XXX: extract common configs, that are not default
    Function withComment = {
        it.comment("Comment")
    }
    
    println withDefaults(withComment) // → Customer(null, Default, Comment, 2024-04-27T08:13:30.056778031Z, null)
    
    // XXX: combine common builders with other overrides
    println withDefaults(withComment.andThen {
        it.id(666)
    }) // → Customer(666, Default, Comment, 2024-04-27T08:13:30.056778031Z, null)