Record builder generator for Java records

Overview

Build Status Maven Central

RecordBuilder

What is RecordBuilder

Java 16 introduces Records. While this version of records is fantastic, it's currently missing some important features normally found in data classes: a builder and "with"ers. This project is an annotation processor that creates:

  • a companion builder class for Java records
  • an interface that adds "with" copy methods
  • an annotation that generates a Java record from an Interface template

Details:

RecordBuilder Example

@RecordBuilder
public record NameAndAge(String name, int age){}

This will generate a builder class that can be used ala:

// build from components
NameAndAge n1 = NameAndAgeBuilder.builder().name(aName).age(anAge).build();

// generate a copy with a changed value
NameAndAge n2 = NameAndAgeBuilder.builder(n1).age(newAge).build(); // name is the same as the name in n1

// pass to other methods to set components
var builder = new NameAndAgeBuilder();
setName(builder);
setAge(builder);
NameAndAge n3 = builder.build();

// use the generated static constructor/builder
import static NameAndAgeBuilder.NameAndAge;
...
var n4 = NameAndAge("hey", 42);

Wither Example

@RecordBuilder
public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With {}

In addition to creating a builder, your record is enhanced by "wither" methods ala:

NameAndAge r1 = new NameAndAge("foo", 123);
NameAndAge r2 = r1.withName("bar");
NameAndAge r3 = r2.withAge(456);

// access the builder as well
NameAndAge r4 = r3.with().age(101).name("baz").build();

// alternate method of accessing the builder (note: no need to call "build()")
NameAndAge r5 = r4.with(b -> b.age(200).name("whatever"));

// perform some logic in addition to changing values
NameAndAge r5 = r4.with(b -> {
   if (b.age() > 13) {
       b.name("Teen " + b.name());
   } else {
       b.name("whatever"));
   }
});

// or, if you cannot add the "With" interface to your record...
NameAndAge r6 = NameAndAgeBuilder.from(r5).with(b -> b.age(200).name("whatever"));
NameAndAge r7 = NameAndAgeBuilder.from(r5).withName("boop");

Hat tip to Benji Weber for the Withers idea.

Builder Class Definition

(Note: you can see a builder class built using @RecordBuilderFull here: SingleItemsBuilder.java)

The full builder class is defined as:

public class NameAndAgeBuilder {
  private String name;

  private int age;

  private NameAndAgeBuilder() {
  }

  private NameAndAgeBuilder(String name, int age) {
    this.name = name;
    this.age = age;
  }

  /**
   * Static constructor/builder. Can be used instead of new NameAndAge(...)
   */
  public static NameAndAge NameAndAge(String name, int age) {
    return new NameAndAge(name, age);
  }

  /**
   * Return a new builder with all fields set to default Java values
   */
  public static NameAndAgeBuilder builder() {
    return new NameAndAgeBuilder();
  }

  /**
   * Return a new builder with all fields set to the values taken from the given record instance
   */
  public static NameAndAgeBuilder builder(NameAndAge from) {
    return new NameAndAgeBuilder(from.name(), from.age());
  }

  /**
   * Return a "with"er for an existing record instance
   */
  public static NameAndAgeBuilder.With from(NameAndAge from) {
    return new NameAndAgeBuilder.With() {
      @Override
      public String name() {
        return from.name();
      }

      @Override
      public int age() {
        return from.age();
      }
    };
  }

  /**
   * Return a stream of the record components as map entries keyed with the component name and the value as the component value
   */
  public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
    return Stream.of(Map.entry("name", record.name()),
            Map.entry("age", record.age()));
  }

  /**
   * Return a new record instance with all fields set to the current values in this builder
   */
  public NameAndAge build() {
    return new NameAndAge(name, age);
  }

  @Override
  public String toString() {
    return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, age);
  }

  @Override
  public boolean equals(Object o) {
    return (this == o) || ((o instanceof NameAndAgeBuilder r)
            && Objects.equals(name, r.name)
            && (age == r.age));
  }

  /**
   * Set a new value for the {@code name} record component in the builder
   */
  public NameAndAgeBuilder name(String name) {
    this.name = name;
    return this;
  }

  /**
   * Return the current value for the {@code name} record component in the builder
   */
  public String name() {
    return name;
  }

  /**
   * Set a new value for the {@code age} record component in the builder
   */
  public NameAndAgeBuilder age(int age) {
    this.age = age;
    return this;
  }

  /**
   * Return the current value for the {@code age} record component in the builder
   */
  public int age() {
    return age;
  }

  /**
   * Add withers to {@code NameAndAge}
   */
  public interface With {
    /**
     * Return the current value for the {@code name} record component in the builder
     */
    String name();

    /**
     * Return the current value for the {@code age} record component in the builder
     */
    int age();

    /**
     * Return a new record builder using the current values
     */
    default NameAndAgeBuilder with() {
      return new NameAndAgeBuilder(name(), age());
    }

    /**
     * Return a new record built from the builder passed to the given consumer
     */
    default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
      NameAndAgeBuilder builder = with();
      consumer.accept(builder);
      return builder.build();
    }

    /**
     * Return a new instance of {@code NameAndAge} with a new value for {@code name}
     */
    default NameAndAge withName(String name) {
      return new NameAndAge(name, age());
    }

    /**
     * Return a new instance of {@code NameAndAge} with a new value for {@code age}
     */
    default NameAndAge withAge(int age) {
      return new NameAndAge(name(), age);
    }
  }
}

RecordInterface Example

@RecordInterface
public interface NameAndAge {
    String name(); 
    int age();
}

This will generate a record ala:

@RecordBuilder
public record NameAndAgeRecord(String name, int age) implements 
    NameAndAge, NameAndAgeRecordBuilder.With {}

Note that the generated record is annotated with @RecordBuilder so a record builder is generated for the new record as well.

Notes:

  • Non static methods in the interface...
    • ...cannot have arguments
    • ...must return a value
    • ...cannot have type parameters
  • Methods with default implementations are used in the generation unless they are annotated with @IgnoreDefaultMethod
  • If you do not want a record builder generated, annotate your interface as @RecordInterface(addRecordBuilder = false)
  • If your interface is a JavaBean (e.g. getThing(), isThing()) the "get" and "is" prefixes are stripped and forwarding methods are added.

Generation Via Includes

An alternate method of generation is to use the Include variants of the annotations. These variants act on lists of specified classes. This allows the source classes to be pristine or even come from libraries where you are not able to annotate the source.

E.g.

import some.library.code.ImportedRecord
import some.library.code.ImportedInterface

@RecordBuilder.Include({
    ImportedRecord.class    // generates a record builder for ImportedRecord  
})
@RecordInterface.Include({
    ImportedInterface.class // generates a record interface for ImportedInterface 
})
public void Placeholder {
}

@RecordBuilder.Include also supports a packages attribute that includes all records in the listed packages.

The target package for generation is the same as the package that contains the "Include" annotation. Use packagePattern to change this (see Javadoc for details).

Usage

Maven

Add a dependency that contains the discoverable annotation processor:

<dependency>
    <groupId>io.soabase.record-builder</groupId>
    <artifactId>record-builder-processor</artifactId>
    <version>${record.builder.version}</version>
    <scope>provided</scope>
</dependency>

Gradle

Add the following to your build.gradle file:

dependencies {
    annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here'
    compileOnly 'io.soabase.record-builder:record-builder-core:$version-goes-here'
}

IDE

Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.

Customizing

RecordBuilder can be customized to your needs and you can even create your own custom RecordBuilder annotations. See Customizing RecordBuilder for details.

Comments
  • Add possibility to add custom annotations to Builder

    Add possibility to add custom annotations to Builder

    In particular edu.umd.cs.findbugs.annotations.SuppressFBWarnings, otherweise SpotBugs detects violations which normally would be ignored in generated code.

    It's not possible to make SpotBugs process @Generated annotation since SpotBugs process byte-code and @Generated has source retention type.

    P.S. Nice library and nice work! I use Immutables a lot so it's really nice to have similar funtionality for java records.

    enhancement 
    opened by lazystone 18
  • null values and addConcreteSettersForOptional

    null values and addConcreteSettersForOptional

    I find that addConcreteSettersForOptional uses Optional#of instead of Optional#ofNullable quite surprising especially since Intellij IDEA by default doesn't warn against passing nulls to javax.validation.constraints.NotNull annotated parameters. What is the reasoning behind this decision?

    bug PR welcome good first issue 
    opened by lpandzic 16
  • Support single item collection builders

    Support single item collection builders

    When addSingleItemCollectionBuilders() is enabled in options, collection types (List, Set and Map) are handled specially. The setters for these types now create an internal collection and items are added to that collection. Additionally, "adder" methods prefixed with singleItemBuilderPrefix() are created to add single items to these collections.

    The generated builder looks like this: https://gist.github.com/Randgalt/8aa487a847ea2acdd76d702f7cf17d6a

    cc @tmichel

    Closes #73

    opened by Randgalt 13
  • Sealed wither interface?

    Sealed wither interface?

    Its still down the line (Java 17, most likely), but it would be nice to have the RecordName.With generated interface be sealed so that only the record can implement it

    enhancement future waiting-on-javapoet 
    opened by bowbahdoe 13
  • Feature/114 collection copying only when changed

    Feature/114 collection copying only when changed

    For #114 again.

    Instead of using explicit booleans to track if a collection was changed I track it by the type of the collection. For that, I added custom mutable private collections (subclassing ArrayList, HashSet, and HashMap) to the builder. This makes it explicitly known for us whether it's "our" mutable collection or if it comes from the outside.

    opened by freelon 10
  • RecordBuilder Enhancer

    RecordBuilder Enhancer

    FOR YOUR CONSIDERATION

    This is an idea that I've had for a while: being able to inject null checks and defensive copying into Java Record default constructors. While working on the idea I was able to generalize it into something that can be customizable as part of a normal build process.

    See the README for complete details.

    I haven't decided on whether or not to release this yet. If people find it useful I will. Use <version>34-SNAPSHOT</version> to test it.

    THANK YOU

    opened by Randgalt 9
  • Collection copying only when changed

    Collection copying only when changed

    For #114

    Currently the test TestCollections.testRecordBuilderOptionsCopied fails, since I had to change the signature of the __list() shim methods. The problem is, that the code as on master generates setters for collections with different signatures: If addSingleItemCollectionBuilders == true: someList(Collection<? extends ListItem> someList), but otherwise the someList parameter is of type List<...>. I didn't want to change that behavior together with the other ticket, though (see #117 ).

    opened by freelon 7
  • `TYPE_USE` annotations were being ignored

    `TYPE_USE` annotations were being ignored

    Java's DAG for annotations processors doesn't contain TYPE_USE annotations on the Element for some reason. However, they are on the type. So, use the type instead.

    Note due to limitations of JavaPoet this doesn't fix TYPE_USE annotations on parameterized types or array components. If we want to address those we will need changes in JavaPoet which has been dormant for a very long time.

    Fixes #113 Relates to #111

    opened by Randgalt 7
  • Add configurable method name prefixes to builders

    Add configurable method name prefixes to builders

    Sometimes I feel old and conservative and want to do things the old way.

    Just kidding; We have a giant codebase built on top of Immutables.org. It makes builders with setter methods. To make our migration to records as painless as possible, it is nice to migrate without having to rename the usage of Immutables builders for thousands of classes.

    Also, get* set* and is* have been idiomatic Java since "forever". Some people might prefer these prefixes, even if they do make the methods three characters longer

    enhancement 
    opened by mads-b 7
  • Simplify updating immutable members when referencing the previous value

    Simplify updating immutable members when referencing the previous value

    Hey there! I've been using your library a bit and think it's a nice improvement for records. When working with nested immutable types I've found some thing a bit cumbersome. Imagine that you have the following records:

    @RecordBuilder
    public record Context(int id, Counter count) implements ContextBuilder.With {}
    
    @RecordBuilder
    public record Counter(int count) implements CounterBuilder.With {}
    

    If we want to increment the count in a context we need to do this:

    var ctx = new Context(1, new Counter(0));
    
    // I know withCounter can be used as well
    var newCtx = ctx.with().counter(ctx.counter().with().counter(ctx.counter().count() + 1).build());
    

    We need to repeat the ctx.counter() bit three times unless we want to bring in a temporary variable. It would be nice if RecordBuilder provided something that simplified mutation of members that are record or other immutable types like queues. Ideally something that allows us to add arbitrary helper methods to the builder would be nice, but maybe that's difficult to do?

    Another idea would be to generate variants of the setters in the builder that takes a Function<T, T>. That allows the user to mutate the inner types. The example above would become something like:

    var ctx = new Context(1, new Counter(0));
    var newCtx = ctx.with().counter(ctr -> ctr.with().count(c -> c + 1).build());
    

    The drawback is of course that it could conflict with records that have members with a type of Function.

    enhancement question 
    opened by runfalk 7
  • Validations for the withers

    Validations for the withers

    Hi Jordan

    I'm playing around with new validation opportunities that you just have implemented. Very nice :-)

    Howevere, It seems to be a possible to sneak thru the validation when using the withers methods.

    For example when letting RequiredRecord implement RequiredRecordBuilder.With and RequiredRecord2 implement RequiredRecord2Builder.With

    I expect these two unit test to pass:

        void testNotNullsWithNewProperty() {
            var valid = RequiredRecordBuilder.builder().hey("hey").i(1).build();
            Assertions.assertThrows(NullPointerException.class, () -> valid.withHey(null));
        }
    
        @Test
        void testValidationWithNewProperty() {
            var valid = RequiredRecord2Builder.builder().hey("hey").i(1).build();
            Assertions.assertThrows(ValidationException.class, () -> valid.withHey(null));
        }
    

    For the @RecordBuilder.Options(interpretNotNulls = true) I guess it just as easy to add Objects.reuireNonNull in appropriate with-methods

    but how to handle @RecordBuilder.Options(useValidationApi = true) ?

    We dont want to perform a full validation of the entiere object via the builders static "new" method but instead only validate the actual property.

    Please let me know your thoughts around this and I'll be happy to start with an initial PR.

    Regards Dan

    bug PR welcome 
    opened by danp11 6
  • feat: add optional suffix to static builder

    feat: add optional suffix to static builder

    Not sure what you think of this, my team are coming from using Immutables heavily and are fond of their FieldName.of() syntax convention for static builders.

    I’m assuming my change would enable us to call a static builder like FieldNameBuilder.FieldNameOf().

    When statically imported, would read succinctly as FieldNameOf()

    improvement needs discussion 
    opened by RichardTree 2
  • Using getterPrefix and/or booleanPrefix generates incompatible Wither

    Using getterPrefix and/or booleanPrefix generates incompatible Wither

    E.g.

    @RecordBuilder
    @RecordBuilder.Options(
        setterPrefix = "set", getterPrefix = "get", booleanPrefix = "is", beanClassName = "Bean")
    public record CustomMethodNames<K, V>(
        Map<K, V> kvMap,
        int theValue,
        List<Integer> theList,
        boolean theBoolean) implements Bean, CustomMethodNamesBuilder.With {
    }
    

    Generates With interface with incorrect method names. They shouldn't have the prefix.

    bug 
    opened by Randgalt 0
  • aa027af causes compile errors

    aa027af causes compile errors

    Hello, The aforementioned commit is preventing me from upgrading from v33 to v34. The issue:

    I have a quite simple record which should create a builder:

    public record CombinedFields(
        @JsonValue String combinedField,
        List<CombinedFieldsLine> lines) implements CombinedFieldsBuilder.Bean {}
    

    But now, the generated builder contains a setter with an undefined method (shim?)

        /**
         * Re-create the internally allocated {@code List<CombinedFields.CombinedFieldsLine>} for {@code lines} by copying the argument
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        public CombinedFieldsBuilder setLines(
                Collection<? extends CombinedFields.CombinedFieldsLine> lines) {
            this.lines = __list(lines);
            return this;
        }
    

    It's this __list method that does not exist. Is this a bug or something I need to change in my configuration?

    bug 
    opened by mads-b 8
  • Make builder constructor public

    Make builder constructor public

    The purpose is to make record-builder fit for a slightly off-label use case: Generate the getter/setter/equals/hashcode/toString boilerplate for our otherwise standard Java Bean classes. We don't oppose records, actually, it's just that some libraries don't fully support using them yet.

    We would like to be able to make a bean class Foo like this:

    public class Bean extends FooBaseRecordBuilder {
        // We need a parameterless constructor for a Java Bean.
        // If the parent class has it but makes it private, we can't even create it here.
    
        // Any overrides for getters/setters go here.
        // (If there are no overrides, maybe we can configure record-builder to create Foo directly.
        // We did not pursue that venue since our team is okay with having an empty Foo class.)
    }
    

    where FooBaseRecordBuilder is generated from a FooBase interface:

    @RecordInterface
    // Actually this is in a @RecordBuilder.Template :-)
    @RecordBuilder.Options(booleanPrefix = "is", getterPrefix = "get", setterPrefix = "set")
    public interface FooBase {
    
        boolean isCustomizable();
    
        @JsonView(Patchable.class) // Probably need a @RecordBuilder.Options entry to have this on the actual Java Bean
        boolean isEnabled();
    }
    
    

    Aside notes:

    • We do not need the generated FooRecord class.
    • In the FooRecordBuilder class, we need only the default constructor, the fields, getters/setters, and equals/hashcode/toString.
    • The stream is a very, very appreciated addition that will help us with some other stuff. We interact with MongoDB and Jackson, and being able to iterate over whatever fields are in an entity object is a perfect match for these. (It would be nice to have a stream that returns the getters and setters, but we have to encounter that use case yet.)
    enhancement needs discussion 
    opened by toolforger 4
  • Allow nullable collections with useImmutableCollections=true

    Allow nullable collections with useImmutableCollections=true

    Hi, somewhat related to #122, I'd like to propose allowing collections to become nullable, because right now useImmutableCollections = true does two things

    • it adds the conversion to immutable collections (awesome!)
    • but also enforces that the output record can never have the collection values nullable

    I'm proposing to change the behaviour based on interpretNotNulls value.

    • useImmutableCollections = true && interpretNotNulls = false
      • mapping will be return (o != null) ? Map.copyOf(o) : null;
    • useImmutableCollections = true && interpretNotNulls = true
      • and field is determined to be nullable
        • mapping will be return (o != null) ? Map.copyOf(o) : null;
      • and field is determined to be notnull
        • mapping will be return (o != null) ? Map.copyOf(o) : Map.of();
    • useImmutableCollections = true
      • current behaviour

    This should also (consistently) affect the default value for collections mentioned in #122

    enhancement PR welcome 
    opened by fprochazka 0
Releases(record-builder-35)
Owner
Jordan Zimmerman
Jordan Zimmerman
Log4j-payload-generator - Log4j jndi injects the Payload generator

0x01 简介 log4j-payload-generator是 woodpecker框架 生产log4 jndi注入漏洞payload的插件。目前可以一键生产以下5类payload。 原始payload {[upper|lower]:x}类型随机混payload {[upper|lower]:x}

null 469 Dec 30, 2022
OpenApi Generator - REST Client Generator

Quarkus - Openapi Generator Welcome to Quarkiverse! Congratulations and thank you for creating a new Quarkus extension project in Quarkiverse! Feel fr

Quarkiverse Hub 46 Jan 3, 2023
Relational database project, PC Builder, for the Database Systems Design course.

README: Starting the Progam: This program was built and ran on the Eclipse IDE. To run, first create the database, "ty_daniel_db", using the ty_dani

Daniel Ty 1 Jan 6, 2022
YetAnotherConfigLib (yacl) is just that. A builder-based configuration library for Minecraft.

YetAnotherConfigLib Yet Another Config Lib, like, what were you expecting? Why does this mod even exist? This mod was made to fill a hole in this area

Xander 36 Dec 29, 2022
RR4J is a tool that records java execution and later allows developers to replay locally.

RR4J [Record Replay 4 Java] RR4J is a tool that records java execution and later allows developers to replay locally. The tool solves one of the chall

Kartik  kalaghatgi 18 Dec 7, 2022
Representational State Transfer + Structured Query Language(RSQL): Demo application using RSQL parser to filter records based on provided condition(s)

Representational State Transfer + Structured Query Language: RSQL Demo application using RSQL parser to filter records based on provided condition(s)

Hardik Singh Behl 9 Nov 23, 2022
An examples of creating test records in the database with Spring Boot + Spring Data + JPA usage.

Spring Boot + JPA — Clear Tests An examples of creating test records in the database with Spring Boot + Spring Data + JPA usage. Check out the article

Semyon Kirekov 8 Nov 24, 2022
Java XMLDecoder payload generator

0x01 简介 xmldecoder-payload-generator是woodpecker框架快速生成XMLDecoder荷载插件,目前支持如下payload生成: sleep dnslog socket log httplog execute command jndi bcel bcel wi

null 14 Jul 6, 2022
Java based open source static site/blog generator for developers & designers.

JBake JBake is a Java based open source static site/blog generator for developers. Documentation Full documentation is available on jbake.org. Contrib

JBake 1k Dec 30, 2022
Java SQL (JDBC) code generator with GUI. SQL and OOP finally united.

jSQL-Gen Java SQL (JDBC) code generator with GUI. SQL and OOP finally united. Usage Install the latest release. Create a database, tables and their co

Osiris-Team 11 Nov 14, 2022
Auto-Unit-Test-Case-Generator automatically generates high-level code-coverage JUnit test suites for Java, widely used within the ANT Group.

中文README传送门 What is Auto-Unit-Test-Case-Generator Auto-Unit-Test-Case-Generator generates JUnit test suites for Java class just as its name. During te

TRaaS 108 Dec 22, 2022
A fractal generator

FractalMatic This app is a simple 2d fractal generator that uses JavaFx framework. Fractalmatic has only one type of fractal at the moment, but the pl

Zeynel 25 Oct 18, 2022
Universal, flexible, high-performance distributed ID generator

CosId Universal, flexible, high-performance distributed ID generator 中文文档 Introduction CosId aims to provide a universal, flexible and high-performanc

Ahoo Wang 256 Dec 27, 2022
OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)

OpenAPI Generator Master (5.4.x): 6.0.x (6.0.x): ⭐ ⭐ ⭐ If you would like to contribute, please refer to guidelines and a list of open tasks. ⭐ ⭐ ⭐ ‼️

OpenAPI Tools 14.8k Dec 30, 2022
RAML to HTML documentation generator.

A simple RAML to HTML documentation generator, written for Node.js, with theme support. RAML version support raml2html 4 and higher only support RAML

null 1.1k Nov 27, 2022
The simple, R+D and Innovation Evidences Generator

R+D and Innovation Evidences Generator Evidences Generator The simple, R+D and Innovation Evidences Generator Project status As of January 1, 2022, Ev

Ramón Granda García 1 Jan 21, 2022
The KubeJS data dumper and dynamic typing generator.

ProbeJS A data dumper and typing generator for the KubeJS functions, constants and classes. Great thanks to @DAmNRelentless, @LatvianModder and @yeste

Li Junyu 22 Dec 8, 2022
A simple QR-code generator.

javaQR-generator A simple QR-code generator. Installation instructions. *Note: in order to build this program you must have JDK v1.8 or greater proper

Yoel N. Fabelo González 1 May 18, 2022
Distributed id generator application

Java distributed Unique ID generator inspired by Twitter snowflake You can read about Twitter snowflake here. The IDs are 64-bits in size and are gene

Mert Aksu 6 Oct 21, 2021