Compare commits
15 Commits
record-bui
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb91d612e | ||
|
|
7c84f26972 | ||
|
|
82cc4f4cad | ||
|
|
f16e1b1d0e | ||
|
|
c92bf78ec5 | ||
|
|
4c4baa015f | ||
|
|
81b7b93a5b | ||
|
|
c39983e342 | ||
|
|
400caa2943 | ||
|
|
a2edd7299f | ||
|
|
6661c2ae0e | ||
|
|
74c8480b43 | ||
|
|
8dbdb43391 | ||
|
|
44064d656e | ||
|
|
791eb02faf |
@@ -1,3 +1,3 @@
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk14
|
||||
- openjdk15
|
||||
|
||||
91
README.md
91
README.md
@@ -5,13 +5,20 @@
|
||||
|
||||
## What is RecordBuilder
|
||||
|
||||
Java 14 is introducing [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9, features in Java are being released in stages. While the Java 14 version of records is fantastic, it's currently missing an important feature for data classes: a builder. This project is an annotation processor that creates companion builder classes for Java records.
|
||||
Java 15 introduced [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9,
|
||||
features in Java are being released in stages. While the Java 15 version of records is fantastic, it's currently missing important features
|
||||
for 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
|
||||
|
||||
In addition to a record builder an annotation is provided that can generate a Java record from an Interface template. This will be useful for DAO-style interfaces, etc.
|
||||
where a Record (with toString(), hashCode(), equals(), etc.) and a companion RecordBuilder are needed.
|
||||
_Details:_
|
||||
|
||||
- [RecordBuilder Details](#RecordBuilder-Example)
|
||||
- [Record From Interface Details](#Record-Interface-Example)
|
||||
- [Wither Details](#Wither-Example)
|
||||
- [RecordBuilder Full Definition](#Builder-Class-Definition)
|
||||
- [Record From Interface Details](#RecordInterface-Example)
|
||||
|
||||
## RecordBuilder Example
|
||||
|
||||
@@ -36,6 +43,31 @@ setAge(builder);
|
||||
var n3 = builder.build();
|
||||
```
|
||||
|
||||
## Wither Example
|
||||
|
||||
```java
|
||||
@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:
|
||||
|
||||
```java
|
||||
var r1 = new NameAndAge("foo", 123);
|
||||
var r2 = r1.withName("bar");
|
||||
var r3 = r2.withAge(456);
|
||||
|
||||
// access the builder as well
|
||||
var r4 = r3.with().age(101).name("baz").build();
|
||||
|
||||
// alternate method of accessing the builder (note: no need to call "build()")
|
||||
var r5 = r4.with(b -> b.age(200).name("whatever"));
|
||||
```
|
||||
|
||||
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
|
||||
|
||||
## Builder Class Definition
|
||||
|
||||
The full builder class is defined as:
|
||||
|
||||
```java
|
||||
@@ -127,6 +159,45 @@ public class NameAndAgeBuilder {
|
||||
&& Objects.equals(name, b.name)
|
||||
&& (age == b.age));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add withers to {@code NameAndAge}
|
||||
*/
|
||||
public interface With {
|
||||
/**
|
||||
* Return a new record builder using the current values
|
||||
*/
|
||||
default NameAndAgeBuilder with() {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return NameAndAgeBuilder.builder(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new record built from the builder passed to the given consumer
|
||||
*/
|
||||
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
NameAndAgeBuilder builder = NameAndAgeBuilder.builder(r);
|
||||
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) {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return new NameAndAge(name, r.age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
|
||||
*/
|
||||
default NameAndAge withAge(int age) {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return new NameAndAge(r.name(), age);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,7 +215,7 @@ This will generate a record ala:
|
||||
|
||||
```java
|
||||
@RecordBuilder
|
||||
public record NameAndAgeRecord(String name, int age){}
|
||||
public record NameAndAgeRecord(String name, int age) implements NameAndAge {}
|
||||
```
|
||||
|
||||
Note that the generated record is annotated with `@RecordBuilder` so a record
|
||||
@@ -195,7 +266,7 @@ Notes:
|
||||
|
||||
|
||||
<!-- "release" and "enable-preview" are required while records are preview features -->
|
||||
<release>14</release>
|
||||
<release>15</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
@@ -237,11 +308,11 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
|
||||
|
||||
Note: records are a preview feature only. You'll need take a number of steps in order to try RecordBuilder:
|
||||
|
||||
- Install and make active Java 14 or later
|
||||
- Make sure your development tool is using Java 14 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
|
||||
- Install and make active Java 15 or later
|
||||
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
|
||||
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
|
||||
|
||||
Note: I've seen some very odd compilation bugs with the current Java 14 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
|
||||
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
|
||||
|
||||
## Customizing
|
||||
|
||||
@@ -259,6 +330,8 @@ Alternatively, you can provide values for each individual meta data (or combinat
|
||||
- `javac ... -AbuilderMethodName=foo`
|
||||
- `javac ... -AbuildMethodName=foo`
|
||||
- `javac ... -AcomponentsMethodName=foo`
|
||||
- `javac ... -AwithClassName=foo`
|
||||
- `javac ... -AwithClassMethodPrefix=foo`
|
||||
- `javac ... -AfileComment=foo`
|
||||
- `javac ... -AfileIndent=foo`
|
||||
- `javac ... -AprefixEnclosingClassNames=foo`
|
||||
|
||||
21
pom.xml
21
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.7.ea</version>
|
||||
<version>1.10.ea</version>
|
||||
|
||||
<modules>
|
||||
<module>record-builder-core</module>
|
||||
@@ -18,7 +18,7 @@
|
||||
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<jdk-version>14</jdk-version>
|
||||
<jdk-version>15</jdk-version>
|
||||
|
||||
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
|
||||
<maven-source-plugin-version>3.2.0</maven-source-plugin-version>
|
||||
@@ -30,6 +30,7 @@
|
||||
<maven-clean-plugin-version>3.1.0</maven-clean-plugin-version>
|
||||
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
|
||||
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
|
||||
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
|
||||
|
||||
<javapoet-version>1.12.1</javapoet-version>
|
||||
<junit-jupiter-version>5.5.2</junit-jupiter-version>
|
||||
@@ -70,7 +71,7 @@
|
||||
<url>https://github.com/randgalt/record-builder</url>
|
||||
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
|
||||
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
|
||||
<tag>record-builder-1.7.ea</tag>
|
||||
<tag>record-builder-1.10.ea</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
@@ -271,6 +272,15 @@
|
||||
<tagNameFormat>record-builder-@{project.version}</tagNameFormat>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin-version}</version>
|
||||
<configuration>
|
||||
<argLine>--enable-preview</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
@@ -278,11 +288,6 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>14</source>
|
||||
<target>14</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
|
||||
@@ -3,22 +3,9 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.7.ea</version>
|
||||
<version>1.10.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>14</source>
|
||||
<target>14</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -87,6 +87,24 @@ public interface RecordBuilderMetaData {
|
||||
return "stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the nested With class
|
||||
*
|
||||
* @return with class name
|
||||
*/
|
||||
default String withClassName() {
|
||||
return "With";
|
||||
}
|
||||
|
||||
/**
|
||||
* The prefix to use for the methods in the With class
|
||||
*
|
||||
* @return prefix
|
||||
*/
|
||||
default String withClassMethodPrefix() {
|
||||
return "with";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.7.ea</version>
|
||||
<version>1.10.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<proc>none</proc>
|
||||
<source>14</source>
|
||||
<target>14</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
@@ -61,6 +61,14 @@ public class ElementUtils {
|
||||
return new ClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString());
|
||||
}
|
||||
|
||||
public static String getWithMethodName(ClassType component, String prefix) {
|
||||
var name = component.name();
|
||||
if (name.length() == 1) {
|
||||
return prefix + name.toUpperCase();
|
||||
}
|
||||
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
public static String getBuilderName(TypeElement element, RecordBuilderMetaData metaData, ClassType classType, String suffix) {
|
||||
// generate the class name
|
||||
var baseName = classType.name() + suffix;
|
||||
|
||||
@@ -15,24 +15,35 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.CodeBlock;
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
|
||||
|
||||
class InternalRecordBuilderProcessor {
|
||||
class InternalRecordBuilderProcessor
|
||||
{
|
||||
private final RecordBuilderMetaData metaData;
|
||||
private final ClassType recordClassType;
|
||||
private final String packageName;
|
||||
@@ -42,7 +53,8 @@ class InternalRecordBuilderProcessor {
|
||||
private final TypeSpec builderType;
|
||||
private final TypeSpec.Builder builder;
|
||||
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData)
|
||||
{
|
||||
this.metaData = metaData;
|
||||
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
|
||||
packageName = ElementUtils.getPackageName(record);
|
||||
@@ -54,6 +66,7 @@ class InternalRecordBuilderProcessor {
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithNestedClass();
|
||||
addDefaultConstructor();
|
||||
addAllArgsConstructor();
|
||||
addStaticDefaultBuilderMethod();
|
||||
@@ -71,19 +84,138 @@ class InternalRecordBuilderProcessor {
|
||||
builderType = builder.build();
|
||||
}
|
||||
|
||||
String packageName() {
|
||||
String packageName()
|
||||
{
|
||||
return packageName;
|
||||
}
|
||||
|
||||
ClassType builderClassType() {
|
||||
ClassType builderClassType()
|
||||
{
|
||||
return builderClassType;
|
||||
}
|
||||
|
||||
TypeSpec builderType() {
|
||||
TypeSpec builderType()
|
||||
{
|
||||
return builderType;
|
||||
}
|
||||
|
||||
private void addDefaultConstructor() {
|
||||
private void addWithNestedClass()
|
||||
{
|
||||
/*
|
||||
Adds a nested interface that adds withers similar to:
|
||||
|
||||
public class MyRecordBuilder {
|
||||
public interface With {
|
||||
// with methods
|
||||
}
|
||||
}
|
||||
*/
|
||||
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithBuilderMethod(classBuilder);
|
||||
addWithSuppliedBuilderMethod(classBuilder);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
|
||||
builder.addType(classBuilder.build());
|
||||
}
|
||||
|
||||
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
MyRecordBuilder builder MyRecordBuilder.builder(r);
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("$T builder = $L.$L(r);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName())
|
||||
.add("consumer.accept(builder);\n")
|
||||
.add("return builder.build();\n");
|
||||
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
|
||||
var parameter = ParameterSpec.builder(consumerType, "consumer").build();
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new record built from the builder passed to the given consumer")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(parameter)
|
||||
.returns(recordClassType.typeName())
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecordBuilder with() {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
return MyRecordBuilder.builder(r);
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new record builder using the current values")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.returns(builderClassType.typeName())
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
|
||||
{
|
||||
/*
|
||||
Adds a with method for the component similar to:
|
||||
|
||||
default MyRecord withName(String name) {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
return new MyRecord(name, r.age());
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
|
||||
if (parameterIndex > 0) {
|
||||
codeBlockBuilder.add(", ");
|
||||
}
|
||||
ClassType parameterComponent = recordComponents.get(parameterIndex);
|
||||
if (parameterIndex == index) {
|
||||
codeBlockBuilder.add(parameterComponent.name());
|
||||
}
|
||||
else {
|
||||
codeBlockBuilder.add("r.$L()", parameterComponent.name());
|
||||
}
|
||||
});
|
||||
codeBlockBuilder.add(");");
|
||||
|
||||
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
|
||||
var methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(parameterSpec)
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.returns(recordClassType.typeName())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addDefaultConstructor()
|
||||
{
|
||||
/*
|
||||
Adds a default constructor similar to:
|
||||
|
||||
@@ -97,7 +229,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(constructor);
|
||||
}
|
||||
|
||||
private void addAllArgsConstructor() {
|
||||
private void addAllArgsConstructor()
|
||||
{
|
||||
/*
|
||||
Adds an all-args constructor similar to:
|
||||
|
||||
@@ -118,7 +251,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(constructorBuilder.build());
|
||||
}
|
||||
|
||||
private void addToStringMethod() {
|
||||
private void addToStringMethod()
|
||||
{
|
||||
/*
|
||||
add a toString() method similar to:
|
||||
|
||||
@@ -147,7 +281,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addHashCodeMethod() {
|
||||
private void addHashCodeMethod()
|
||||
{
|
||||
/*
|
||||
add a hashCode() method similar to:
|
||||
|
||||
@@ -175,7 +310,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addEqualsMethod() {
|
||||
private void addEqualsMethod()
|
||||
{
|
||||
/*
|
||||
add an equals() method similar to:
|
||||
|
||||
@@ -193,7 +329,8 @@ class InternalRecordBuilderProcessor {
|
||||
String name = recordComponent.name();
|
||||
if (recordComponent.typeName().isPrimitive()) {
|
||||
codeBuilder.add("\n&& ($L == b.$L)", name, name);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
|
||||
}
|
||||
});
|
||||
@@ -210,7 +347,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addBuildMethod() {
|
||||
private void addBuildMethod()
|
||||
{
|
||||
/*
|
||||
Adds the build method that generates the record similar to:
|
||||
|
||||
@@ -237,7 +375,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticCopyBuilderMethod() {
|
||||
private void addStaticCopyBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a copy builder method that pre-fills the builder with existing values similar to:
|
||||
|
||||
@@ -266,7 +405,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticDefaultBuilderMethod() {
|
||||
private void addStaticDefaultBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a the default builder method similar to:
|
||||
|
||||
@@ -285,7 +425,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticComponentsMethod() {
|
||||
private void addStaticComponentsMethod()
|
||||
{
|
||||
/*
|
||||
Adds a static method that converts a record instance into a stream of its component parts
|
||||
|
||||
@@ -319,7 +460,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1Field(ClassType component) {
|
||||
private void add1Field(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a field similar to:
|
||||
|
||||
@@ -329,7 +471,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addField(fieldSpec);
|
||||
}
|
||||
|
||||
private void add1GetterMethod(ClassType component) {
|
||||
private void add1GetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a getter similar to:
|
||||
|
||||
@@ -347,7 +490,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1SetterMethod(ClassType component) {
|
||||
private void add1SetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a setter similar to:
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ class InternalRecordInterfaceProcessor {
|
||||
.addTypeVariables(typeVariables);
|
||||
|
||||
if (recordInterface.addRecordBuilder()) {
|
||||
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
|
||||
builder.addAnnotation(RecordBuilder.class);
|
||||
builder.addSuperinterface(builderClassType.typeName());
|
||||
}
|
||||
|
||||
recordType = builder.build();
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
/**
|
||||
* @see #suffix()
|
||||
@@ -65,27 +65,40 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
*/
|
||||
public static final String OPTION_PREFIX_ENCLOSING_CLASS_NAMES = "prefixEnclosingClassNames";
|
||||
|
||||
/**
|
||||
* @see #withClassName()
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_NAME = "withClassName";
|
||||
|
||||
/**
|
||||
* @see #withClassMethodPrefix()
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
|
||||
|
||||
private final String suffix;
|
||||
private final String interfaceSuffix;
|
||||
private final String copyMethodName;
|
||||
private final String builderMethodName;
|
||||
private final String buildMethodName;
|
||||
private final String componentsMethodName;
|
||||
private final String withClassName;
|
||||
private final String withClassMethodPrefix;
|
||||
private final String fileComment;
|
||||
private final String fileIndent;
|
||||
private final boolean prefixEnclosingClassNames;
|
||||
|
||||
public OptionBasedRecordBuilderMetaData(Map<String, String> options) {
|
||||
suffix = options.getOrDefault(OPTION_SUFFIX, "Builder");
|
||||
interfaceSuffix = options.getOrDefault(OPTION_INTERFACE_SUFFIX, "Record");
|
||||
builderMethodName = options.getOrDefault(OPTION_BUILDER_METHOD_NAME, "builder");
|
||||
copyMethodName = options.getOrDefault(OPTION_COPY_METHOD_NAME, builderMethodName);
|
||||
buildMethodName = options.getOrDefault(OPTION_BUILD_METHOD_NAME, "build");
|
||||
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, "stream");
|
||||
fileComment = options.getOrDefault(OPTION_FILE_COMMENT,
|
||||
"Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder");
|
||||
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, " ");
|
||||
String prefixenclosingclassnamesopt = options.get(OPTION_PREFIX_ENCLOSING_CLASS_NAMES);
|
||||
suffix = options.getOrDefault(OPTION_SUFFIX, DEFAULT.suffix());
|
||||
interfaceSuffix = options.getOrDefault(OPTION_INTERFACE_SUFFIX, DEFAULT.interfaceSuffix());
|
||||
builderMethodName = options.getOrDefault(OPTION_BUILDER_METHOD_NAME, DEFAULT.builderMethodName());
|
||||
copyMethodName = options.getOrDefault(OPTION_COPY_METHOD_NAME, DEFAULT.copyMethodName());
|
||||
buildMethodName = options.getOrDefault(OPTION_BUILD_METHOD_NAME, DEFAULT.buildMethodName());
|
||||
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, DEFAULT.componentsMethodName());
|
||||
withClassName = options.getOrDefault(OPTION_WITH_CLASS_NAME, DEFAULT.withClassName());
|
||||
withClassMethodPrefix = options.getOrDefault(OPTION_WITH_CLASS_METHOD_PREFIX, DEFAULT.withClassMethodPrefix());
|
||||
fileComment = options.getOrDefault(OPTION_FILE_COMMENT, DEFAULT.fileComment());
|
||||
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, DEFAULT.fileIndent());
|
||||
String prefixenclosingclassnamesopt = options.getOrDefault(OPTION_PREFIX_ENCLOSING_CLASS_NAMES, String.valueOf(DEFAULT.prefixEnclosingClassNames()));
|
||||
if (prefixenclosingclassnamesopt == null) {
|
||||
prefixEnclosingClassNames = true;
|
||||
} else {
|
||||
@@ -118,6 +131,16 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
return componentsMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String withClassName() {
|
||||
return withClassName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String withClassMethodPrefix() {
|
||||
return withClassMethodPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileComment() {
|
||||
return fileComment;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.7.ea</version>
|
||||
<version>1.10.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
@@ -37,9 +42,6 @@
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
<source>14</source>
|
||||
<target>14</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
|
||||
@@ -18,5 +18,5 @@ package io.soabase.recordbuilder.test;
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
|
||||
@RecordBuilder
|
||||
public record SimpleGenericRecord<T>(int i, T s) {
|
||||
public record SimpleGenericRecord<T>(int i, T s) implements SimpleGenericRecordBuilder.With<T> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright 2019 Jordan Zimmerman
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.soabase.recordbuilder.test;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class TestRecordInterface
|
||||
{
|
||||
@Test
|
||||
public void testHasDefaults()
|
||||
{
|
||||
var r1 = new HasDefaultsRecord(Instant.MIN, Instant.MAX);
|
||||
var r2 = r1.with(b -> b.tomorrow(Instant.MIN));
|
||||
Assertions.assertEquals(Instant.MIN, r1.time());
|
||||
Assertions.assertEquals(Instant.MAX, r1.tomorrow());
|
||||
Assertions.assertEquals(Instant.MIN, r2.time());
|
||||
Assertions.assertEquals(Instant.MIN, r2.tomorrow());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright 2019 Jordan Zimmerman
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.soabase.recordbuilder.test;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class TestWithers {
|
||||
@Test
|
||||
void testWithers() {
|
||||
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
|
||||
var r2 = r1.withS(List.of("4", "5"));
|
||||
var r3 = r2.withI(20);
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals(List.of("1", "2", "3"), r1.s());
|
||||
Assertions.assertEquals(10, r2.i());
|
||||
Assertions.assertEquals(List.of("4", "5"), r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals(List.of("4", "5"), r2.s());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWitherBuilder() {
|
||||
var r1 = new SimpleGenericRecord<>(10, "ten");
|
||||
var r2 = r1.with().i(20).s("twenty").build();
|
||||
var r3 = r2.with().s("changed");
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals("ten", r1.s());
|
||||
Assertions.assertEquals(20, r2.i());
|
||||
Assertions.assertEquals("twenty", r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("changed", r3.s());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWitherBuilderConsumer() {
|
||||
var r1 = new SimpleGenericRecord<>(10, "ten");
|
||||
var r2 = r1.with(r -> r.i(15));
|
||||
var r3 = r1.with(r -> r.s("twenty").i(20));
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals("ten", r1.s());
|
||||
Assertions.assertEquals(15, r2.i());
|
||||
Assertions.assertEquals("ten", r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("twenty", r3.s());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user