Compare commits

..

6 Commits

Author SHA1 Message Date
Jordan Zimmerman
b0f4bfbe88 [maven-release-plugin] prepare release record-builder-24 2021-08-04 08:38:27 -05:00
Jordan Zimmerman
09168b6104 [maven-release-plugin] prepare for next development iteration 2021-08-04 08:34:03 -05:00
Jordan Zimmerman
d4e24993c9 [maven-release-plugin] prepare release record-builder-23-java15 2021-08-04 08:33:59 -05:00
Jordan Zimmerman
8304cb1f83 Remove downcast in favor of methods
Great suggestion from @Twisol. There's no need for the downcasting
if record component methods are added to the Wither interface.

Closes #27
2021-08-04 08:16:12 -05:00
Jordan Zimmerman
7215ad3241 Validation and null checks are missing for withers. This PR adds them.
Closes #47
2021-07-01 08:37:52 +01:00
Jordan Zimmerman
f7d65c7619 [maven-release-plugin] prepare for next development iteration 2021-06-26 05:55:30 +01:00
12 changed files with 91 additions and 95 deletions

View File

@@ -184,29 +184,26 @@ public class NameAndAgeBuilder {
&& Objects.equals(name, b.name)
&& (age == b.age));
}
/**
* Downcast to {@code NameAndAge}
*/
private static NameAndAge _downcast(Object obj) {
try {
return (NameAndAge)obj;
}
catch (ClassCastException dummy) {
throw new RuntimeException("NameAndAgeBuilder.With can only be implemented for NameAndAge");
}
}
/**
* 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() {
NameAndAge r = _downcast(this);
return NameAndAgeBuilder.builder(r);
return new NameAndAgeBuilder(name(), age());
}
/**
@@ -222,16 +219,14 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
NameAndAge r = _downcast(this);
return new NameAndAge(name, r.age());
return new NameAndAge(name, age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
NameAndAge r = _downcast(this);
return new NameAndAge(r.name(), age);
return new NameAndAge(name(), age);
}
}
}

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>22</version>
<version>24</version>
<modules>
<module>record-builder-core</module>
@@ -80,7 +80,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-22</tag>
<tag>record-builder-24</tag>
</scm>
<issueManagement>

View File

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

View File

@@ -69,11 +69,6 @@ public @interface RecordBuilder {
*/
String buildMethodName() default "build";
/**
* The name to use for the downcast method
*/
String downCastMethodName() default "_downcast";
/**
* The name to use for the method that returns the record components as a stream
*/

View File

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

View File

@@ -80,7 +80,6 @@ class InternalRecordBuilderProcessor {
add1SetterMethod(component);
add1GetterMethod(component);
});
addStaticDowncastMethod();
builderType = builder.build();
}
@@ -129,6 +128,7 @@ class InternalRecordBuilderProcessor {
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
recordComponents.forEach(component -> addWithGetterMethod(classBuilder, component));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
@@ -167,13 +167,13 @@ class InternalRecordBuilderProcessor {
Adds a method that returns a pre-filled copy builder similar to:
default MyRecordBuilder with() {
MyRecord r = _downcast(this);
return MyRecordBuilder.builder(r);
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName())
.add("return $L.$L($L);", builderClassType.name(), metaData.copyMethodName(), uniqueVarName);
.add("return new $L(", builderClassType.name());
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record builder using the current values")
@@ -201,27 +201,22 @@ class InternalRecordBuilderProcessor {
Adds a with method for the component similar to:
default MyRecord withName(String name) {
MyRecord r = _downcast(this);
return new MyRecord(name, r.age());
}
*/
var codeBlockBuilder = CodeBlock.builder();
if (recordComponents.size() > 1) {
codeBlockBuilder.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName());
addNullCheckCodeBlock(codeBlockBuilder, index);
codeBlockBuilder.add("$[return ");
if (metaData.useValidationApi()) {
codeBlockBuilder.add("$T.validate(", validatorTypeName);
}
codeBlockBuilder.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("$L.$L()", uniqueVarName, parameterComponent.name());
}
});
codeBlockBuilder.add(");");
codeBlockBuilder.add("new $T(", recordClassType.typeName());
addComponentCallsAsArguments(index, codeBlockBuilder);
codeBlockBuilder.add(")");
if (metaData.useValidationApi()) {
codeBlockBuilder.add(")");
}
codeBlockBuilder.add(";$]");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
@@ -237,6 +232,20 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void addComponentCallsAsArguments(int index, CodeBlock.Builder codeBlockBuilder) {
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("$L()", parameterComponent.name());
}
});
}
private void addDefaultConstructor() {
/*
Adds a default constructor similar to:
@@ -277,10 +286,18 @@ class InternalRecordBuilderProcessor {
private void addNullCheckCodeBlock(CodeBlock.Builder builder) {
if (metaData.interpretNotNulls()) {
recordComponents.stream()
.filter(component -> !component.typeName().isPrimitive())
.filter(this::isNullAnnotated)
.forEach(component -> builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required"));
for (int i = 0; i < recordComponents.size(); ++i) {
addNullCheckCodeBlock(builder, i);
}
}
}
private void addNullCheckCodeBlock(CodeBlock.Builder builder, int index) {
if (metaData.interpretNotNulls()) {
var component = recordComponents.get(index);
if (!component.typeName().isPrimitive() && isNullAnnotated(component)) {
builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required");
}
}
}
@@ -529,37 +546,6 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticDowncastMethod() {
/*
Adds a method that downcasts to the record type
private static MyRecord _downcast(Object this) {
return (MyRecord)this;
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("try {\n")
.indent()
.add("return ($T)obj;\n", recordClassType.typeName())
.unindent()
.add("}\n")
.add("catch (ClassCastException dummy) {\n")
.indent()
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented by " + recordClassType.name())
.unindent()
.add("}");
var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Downcast to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addParameter(Object.class, "obj")
.addTypeVariables(typeVariables)
.returns(recordClassType.typeName())
.addCode(codeBlockBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void add1Field(ClassType component) {
/*
For a single record component, add a field similar to:
@@ -590,7 +576,22 @@ class InternalRecordBuilderProcessor {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName) && ((ParameterizedTypeName) component.typeName()).rawType.equals(optionalType);
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
}
private void addWithGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
/*
For a single record component, add a getter similar to:
T p();
*/
var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName());
component.getAccessorAnnotations().forEach(annotationMirror -> methodSpecBuilder.addAnnotation(AnnotationSpec.get(annotationMirror)));
classBuilder.addMethod(methodSpecBuilder.build());
}
private void add1GetterMethod(RecordClassType component) {

View File

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

View File

@@ -21,5 +21,5 @@ import javax.validation.constraints.NotNull;
@RecordBuilder.Options(interpretNotNulls = true)
@RecordBuilder
public record RequiredRecord(@NotNull String hey, @NotNull int i) {
public record RequiredRecord(@NotNull String hey, @NotNull int i) implements RequiredRecordBuilder.With {
}

View File

@@ -21,5 +21,5 @@ import javax.validation.constraints.NotNull;
@RecordBuilder.Options(useValidationApi = true)
@RecordBuilder
public record RequiredRecord2(@NotNull String hey, @NotNull int i) {
public record RequiredRecord2(@NotNull String hey, @NotNull int i) implements RequiredRecord2Builder.With {
}

View File

@@ -30,4 +30,16 @@ class TestValidation {
void testValidation() {
Assertions.assertThrows(ValidationException.class, () -> RequiredRecord2Builder.builder().build());
}
@Test
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));
}
}

View File

@@ -59,11 +59,4 @@ class TestWithers {
Assertions.assertEquals(20, r3.i());
Assertions.assertEquals("twenty", r3.s());
}
private static class BadSubclass implements PersonRecordBuilder.With {}
@Test
void testBadWithSubclass() {
Assertions.assertThrows(RuntimeException.class, () -> new BadSubclass().withAge(10));
}
}

View File

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