Compare commits

...

4 Commits

9 changed files with 117 additions and 84 deletions

View File

@@ -57,8 +57,11 @@ var r1 = new NameAndAge("foo", 123);
var r2 = r1.withName("bar");
var r3 = r2.withAge(456);
// access the builder as well:
// 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 = r3.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._
@@ -161,19 +164,11 @@ public class NameAndAgeBuilder {
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Cast this to the record type
*/
default NameAndAge internalGetThis() {
Object obj = this;
return (PersonRecord)obj;
}
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
NameAndAge r = internalGetThis();
var r = (NameAndAge)(Object)this;
return NameAndAgeBuilder.builder(r);
}
@@ -181,7 +176,7 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
NameAndAge r = internalGetThis();
var r = (NameAndAge)(Object)this;
return new NameAndAge(name, r.age());
}
@@ -189,7 +184,7 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
NameAndAge r = internalGetThis();
var r = (NameAndAge)(Object)this;
return new NameAndAge(r.name(), age);
}
}
@@ -327,7 +322,6 @@ Alternatively, you can provide values for each individual meta data (or combinat
- `javac ... -AcomponentsMethodName=foo`
- `javac ... -AwithClassName=foo`
- `javac ... -AwithClassMethodPrefix=foo`
- `javac ... -AwithClassGetThisMethodName=foo`
- `javac ... -AfileComment=foo`
- `javac ... -AfileIndent=foo`
- `javac ... -AprefixEnclosingClassNames=foo`

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>1.8.ea</version>
<version>1.9.ea</version>
<modules>
<module>record-builder-core</module>
@@ -71,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.8.ea</tag>
<tag>record-builder-1.9.ea</tag>
</scm>
<issueManagement>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.8.ea</version>
<version>1.9.ea</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -105,15 +105,6 @@ public interface RecordBuilderMetaData {
return "with";
}
/**
* The name to use for the method that returns "this" cast to the record type
*
* @return method name
*/
default String withClassGetThisMethodName() {
return "internalGetThis";
}
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.8.ea</version>
<version>1.9.ea</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -15,15 +15,25 @@
*/
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.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;
@@ -32,7 +42,8 @@ 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);
@@ -72,19 +84,23 @@ 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 addWithNestedClass() {
private void addWithNestedClass()
{
/*
Adds a nested interface that adds withers similar to:
@@ -94,45 +110,61 @@ class InternalRecordBuilderProcessor {
}
}
*/
TypeSpec.Builder classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
addWithGetThisMethod(classBuilder);
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
builder.addType(classBuilder.build());
}
private void addWithGetThisMethod(TypeSpec.Builder classBuilder) {
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
{
/*
Adds a method that returns "this" cast to the record similar to:
Adds a method that returns a pre-filled copy builder similar to:
default MyRecord internalGetThis() {
Object obj = this;
return (MyRecord)obj;
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
MyRecord r = (MyRecord)(Object)this;
MyRecordBuilder builder MyRecordBuilder.builder(r);
consumer.accept(builder);
return builder.build();
}
*/
CodeBlock codeBlock = CodeBlock.builder()
.add("Object obj = this;\n")
.add("return ($T)obj;", recordClassType.typeName())
.build();
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassGetThisMethodName())
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("Cast this to the record type")
.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(codeBlock)
.addCode(codeBlockBuilder.build())
.build();
classBuilder.addMethod(methodSpec);
}
private void addWithBuilderMethod(TypeSpec.Builder classBuilder) {
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
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());
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record builder using the current values")
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
@@ -142,17 +174,18 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index) {
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 = internalGetThis();
MyRecord r = (MyRecord)(Object)this;
return new MyRecord(name, r.age());
}
*/
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
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) {
@@ -161,15 +194,16 @@ class InternalRecordBuilderProcessor {
ClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
codeBlockBuilder.add(parameterComponent.name());
} else {
}
else {
codeBlockBuilder.add("r.$L()", parameterComponent.name());
}
});
codeBlockBuilder.add(");");
String methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
MethodSpec methodSpec = MethodSpec.methodBuilder(methodName)
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)
@@ -180,7 +214,8 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void addDefaultConstructor() {
private void addDefaultConstructor()
{
/*
Adds a default constructor similar to:
@@ -194,7 +229,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(constructor);
}
private void addAllArgsConstructor() {
private void addAllArgsConstructor()
{
/*
Adds an all-args constructor similar to:
@@ -215,7 +251,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(constructorBuilder.build());
}
private void addToStringMethod() {
private void addToStringMethod()
{
/*
add a toString() method similar to:
@@ -244,7 +281,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addHashCodeMethod() {
private void addHashCodeMethod()
{
/*
add a hashCode() method similar to:
@@ -272,7 +310,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addEqualsMethod() {
private void addEqualsMethod()
{
/*
add an equals() method similar to:
@@ -290,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);
}
});
@@ -307,7 +347,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addBuildMethod() {
private void addBuildMethod()
{
/*
Adds the build method that generates the record similar to:
@@ -334,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:
@@ -363,7 +405,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticDefaultBuilderMethod() {
private void addStaticDefaultBuilderMethod()
{
/*
Adds a the default builder method similar to:
@@ -382,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
@@ -416,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:
@@ -426,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:
@@ -444,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:

View File

@@ -75,11 +75,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
*/
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
/**
* @see #withClassGetThisMethodName()
*/
public static final String OPTION_WITH_CLASS_GET_THIS_METHOD_NAME = "withClassGetThisMethodName";
private final String suffix;
private final String interfaceSuffix;
private final String copyMethodName;
@@ -88,7 +83,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
private final String componentsMethodName;
private final String withClassName;
private final String withClassMethodPrefix;
private final String withClassGetThisMethodName;
private final String fileComment;
private final String fileIndent;
private final boolean prefixEnclosingClassNames;
@@ -102,7 +96,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
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());
withClassGetThisMethodName = options.getOrDefault(OPTION_WITH_CLASS_GET_THIS_METHOD_NAME, DEFAULT.withClassGetThisMethodName());
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()));
@@ -148,11 +141,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
return withClassMethodPrefix;
}
@Override
public String withClassGetThisMethodName() {
return withClassGetThisMethodName;
}
@Override
public String fileComment() {
return fileComment;

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.8.ea</version>
<version>1.9.ea</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -46,4 +46,17 @@ class TestWithers {
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());
}
}