Compare commits

...

32 Commits

Author SHA1 Message Date
Jordan Zimmerman
861e2e745a [maven-release-plugin] prepare release record-builder-1.13.ea 2020-11-29 08:27:25 -05:00
Jordan Zimmerman
1fc7c9a4b3 Make sure the downcast variable name doesn't collide with a record component name 2020-11-28 22:39:00 -05:00
Jordan Zimmerman
570514e077 buildPackageName() can't assume that the immediate enclosing element is the package. It may be a nested class, etc. 2020-11-28 22:38:48 -05:00
Jordan Zimmerman
9d8b9e65bc [maven-release-plugin] prepare for next development iteration 2020-11-28 08:44:33 -05:00
Jordan Zimmerman
93d6204b76 [maven-release-plugin] prepare release record-builder-1.12.ea 2020-11-28 08:44:25 -05:00
Jordan Zimmerman
15e5bfccc6 [maven-release-plugin] rollback the release of record-builder-1.12.ea 2020-11-28 08:42:15 -05:00
Jordan Zimmerman
67c54244c5 [maven-release-plugin] prepare release record-builder-1.12.ea 2020-11-28 08:41:59 -05:00
Jordan Zimmerman
870ac4a9d9 [maven-release-plugin] rollback the release of record-builder-1.12.ea 2020-11-28 08:29:46 -05:00
Jordan Zimmerman
9ee8b5912a [maven-release-plugin] prepare for next development iteration 2020-11-28 08:28:09 -05:00
Jordan Zimmerman
6d9bcf27da [maven-release-plugin] prepare release record-builder-1.12.ea 2020-11-28 08:27:49 -05:00
Jordan Zimmerman
44db5fdf17 Change to 1.12.ea-SNAPSHOT - last release must have missed this 2020-11-28 08:25:10 -05:00
Jordan Zimmerman
90a65235a9 Add support for static constructor
Add a static constructor/builder so it can statically imported. Instead of
calling "new Record(...)" you can call just "Record(...)".
2020-11-28 07:35:34 -05:00
Ted Cassirer
7e8ddbd700 Remove unused variable in RecordBuilder 2020-11-25 09:33:08 -05:00
Ted Cassirer
3a534fbea9 Don't add the allArgsConstructor to the RecordBuilder if record has no fields 2020-11-25 08:53:06 -05:00
Jordan Zimmerman
6d7ebe2545 Update README.md 2020-11-09 14:42:30 -05:00
Jordan Zimmerman
f1e47391c8 Update README.md 2020-11-04 11:34:41 -05:00
Jordan Zimmerman
c999d0ba06 Update README.md 2020-11-04 10:47:33 -05:00
Jordan Zimmerman
e0243c8b1c Fixed some typos in previous PR 2020-11-04 10:44:43 -05:00
Jordan Zimmerman
65bbbaea05 Update README.md 2020-11-04 10:38:53 -05:00
Jordan Zimmerman
5ae03a2c66 Update README.md 2020-11-04 10:37:38 -05:00
Jordan Zimmerman
437e314799 Better exception when Wither is set as implementor on non-builder class 2020-11-04 10:36:35 -05:00
Jordan Zimmerman
a870beee21 Update README.md 2020-11-02 23:41:19 -05:00
Jordan Zimmerman
5b879743ef Added simple module-info. I've never done this before, I hope it's right 2020-11-02 23:37:57 -05:00
Jordan Zimmerman
c7bdafb0b9 Add method to do downcasting 2020-11-02 23:37:57 -05:00
Jordan Zimmerman
d67c62ed3b Update README.md 2020-10-05 15:00:28 -05:00
Jordan Zimmerman
39cf2b0353 Update README.md 2020-10-05 15:00:16 -05:00
Jordan Zimmerman
6813b88f8d Update README.md 2020-10-05 14:59:28 -05:00
Jordan Zimmerman
54662d69c7 Support Include versions of the annotation 2020-10-05 14:45:34 -05:00
Jordan Zimmerman
7811ff8823 Update README.md 2020-10-05 11:30:04 -05:00
Jordan Zimmerman
4c5076690d Update README.md 2020-10-05 11:29:32 -05:00
Jordan Zimmerman
39a800f10e Update README.md 2020-10-05 11:29:00 -05:00
Jordan Zimmerman
610081b27e [maven-release-plugin] prepare for next development iteration 2020-10-05 11:22:40 -05:00
26 changed files with 681 additions and 83 deletions

View File

@@ -19,6 +19,9 @@ _Details:_
- [Wither Details](#Wither-Example)
- [RecordBuilder Full Definition](#Builder-Class-Definition)
- [Record From Interface Details](#RecordInterface-Example)
- [Generation Via Includes](#generation-via-includes)
- [Usage](#usage)
- [Customizing](#customizing)
## RecordBuilder Example
@@ -31,16 +34,21 @@ This will generate a builder class that can be used ala:
```java
// build from components
var n1 = NameAndAgeBuilder.builder().name(aName).age(anAge).build();
NameAndAge n1 = NameAndAgeBuilder.builder().name(aName).age(anAge).build();
// generate a copy with a changed value
var n2 = NameAndAgeBuilder.builder(n1).age(newAge).build(); // name is the same as the name in n1
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);
var n3 = builder.build();
NameAndAge n3 = builder.build();
// use the generated static constructor/builder
import static NameAndAgeBuilder.NameAndAge;
...
var n4 = NameAndAge("hey", 42);
```
## Wither Example
@@ -53,15 +61,15 @@ 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);
NameAndAge r1 = new NameAndAge("foo", 123);
NameAndAge r2 = r1.withName("bar");
NameAndAge r3 = r2.withAge(456);
// access the builder as well
var r4 = r3.with().age(101).name("baz").build();
NameAndAge 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"));
NameAndAge 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._
@@ -84,6 +92,13 @@ public class NameAndAgeBuilder {
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
*/
@@ -160,6 +175,18 @@ public class NameAndAgeBuilder {
&& (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}
*/
@@ -168,7 +195,7 @@ public class NameAndAgeBuilder {
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
var r = (NameAndAge)(Object)this;
NameAndAge r = _downcast(this);
return NameAndAgeBuilder.builder(r);
}
@@ -176,7 +203,7 @@ public class NameAndAgeBuilder {
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
var r = (NameAndAge)(Object)this;
NameAndAge r = _downcast(this);
NameAndAgeBuilder builder = NameAndAgeBuilder.builder(r);
consumer.accept(builder);
return builder.build();
@@ -186,7 +213,7 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
var r = (NameAndAge)(Object)this;
NameAndAge r = _downcast(this);
return new NameAndAge(name, r.age());
}
@@ -194,7 +221,7 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
var r = (NameAndAge)(Object)this;
NameAndAge r = _downcast(this);
return new NameAndAge(r.name(), age);
}
}
@@ -215,7 +242,8 @@ This will generate a record ala:
```java
@RecordBuilder
public record NameAndAgeRecord(String name, int age) implements NameAndAge {}
public record NameAndAgeRecord(String name, int age) implements
NameAndAge, NameAndAgeRecordBuilder.With {}
```
Note that the generated record is annotated with `@RecordBuilder` so a record
@@ -230,6 +258,31 @@ Notes:
- 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)`
## 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 {
}
```
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

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>1.10.ea</version>
<version>1.13.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.10.ea</tag>
<tag>record-builder-1.13.ea</tag>
</scm>
<issueManagement>

View File

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

View File

@@ -23,4 +23,19 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@interface Include {
Class<?>[] value();
/**
* Pattern used to generate the package for the generated class. The value
* is the literal package name however two replacement values can be used. '@'
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
}

View File

@@ -78,6 +78,15 @@ public interface RecordBuilderMetaData {
return "build";
}
/**
* The name to use for the downcast method
*
* @return downcast method
*/
default String downCastMethodName() {
return "_downcast";
}
/**
* The name to use for the method that returns the record components as a stream
*

View File

@@ -24,4 +24,22 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface RecordInterface {
boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@interface Include {
Class<?>[] value();
boolean addRecordBuilder() default true;
/**
* Pattern used to generate the package for the generated class. The value
* is the literal package name however two replacement values can be used. '@'
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
}

View File

@@ -0,0 +1,19 @@
/**
* 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.
*/
module io.soabase.record.builder.core {
exports io.soabase.recordbuilder.core;
opens io.soabase.recordbuilder.core;
}

View File

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

View File

@@ -20,14 +20,63 @@ import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.TypeMirror;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public class ElementUtils {
public static Optional<? extends AnnotationMirror> findAnnotationMirror(ProcessingEnvironment processingEnv, Element element, String annotationClass) {
return processingEnv.getElementUtils().getAllAnnotationMirrors(element).stream()
.filter(e -> e.getAnnotationType().toString().equals(annotationClass))
.findFirst();
}
public static Optional<? extends AnnotationValue> getAnnotationValue(Map<? extends ExecutableElement, ? extends AnnotationValue> values, String name) {
return values.entrySet()
.stream()
.filter(e -> e.getKey().getSimpleName().toString().equals(name))
.map(Map.Entry::getValue)
.findFirst();
}
@SuppressWarnings("unchecked")
public static List<TypeMirror> getClassesAttribute(AnnotationValue attribute)
{
List<? extends AnnotationValue> values = (attribute != null) ? (List<? extends AnnotationValue>)attribute.getValue() : Collections.emptyList();
return values.stream().map(v -> (TypeMirror)v.getValue()).collect(Collectors.toList());
}
public static boolean getBooleanAttribute(AnnotationValue attribute)
{
Object value = (attribute != null) ? attribute.getValue() : null;
if ( value != null )
{
return Boolean.parseBoolean(String.valueOf(value));
}
return false;
}
public static String getStringAttribute(AnnotationValue attribute, String defaultValue)
{
Object value = (attribute != null) ? attribute.getValue() : null;
if ( value != null )
{
return String.valueOf(value);
}
return defaultValue;
}
public static String getPackageName(TypeElement typeElement) {
while (typeElement.getNestingKind().isNested()) {
Element enclosingElement = typeElement.getEnclosingElement();

View File

@@ -33,6 +33,7 @@ import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -52,15 +53,17 @@ class InternalRecordBuilderProcessor
private final List<ClassType> recordComponents;
private final TypeSpec builderType;
private final TypeSpec.Builder builder;
private final String uniqueVarName;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData)
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = ElementUtils.getPackageName(record);
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
uniqueVarName = getUniqueVarName();
builder = TypeSpec.classBuilder(builderClassType.name())
.addModifiers(Modifier.PUBLIC)
@@ -68,7 +71,10 @@ class InternalRecordBuilderProcessor
.addTypeVariables(typeVariables);
addWithNestedClass();
addDefaultConstructor();
addAllArgsConstructor();
addStaticBuilder();
if (recordComponents.size() > 0) {
addAllArgsConstructor();
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticComponentsMethod();
@@ -81,6 +87,7 @@ class InternalRecordBuilderProcessor
add1SetterMethod(component);
add1GetterMethod(component);
});
addStaticDowncastMethod();
builderType = builder.build();
}
@@ -134,8 +141,8 @@ class InternalRecordBuilderProcessor
}
*/
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("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName())
.add("$T builder = $L.$L($L);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName(), uniqueVarName)
.add("consumer.accept(builder);\n")
.add("return builder.build();\n");
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
@@ -162,8 +169,8 @@ class InternalRecordBuilderProcessor
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName())
.add("return $L.$L($L);", builderClassType.name(), metaData.copyMethodName(), uniqueVarName);
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record builder using the current values")
@@ -174,6 +181,20 @@ class InternalRecordBuilderProcessor
classBuilder.addMethod(methodSpec);
}
private String getUniqueVarName()
{
return getUniqueVarName("");
}
private String getUniqueVarName(String prefix)
{
var name = prefix + "r";
var alreadyExists = recordComponents.stream()
.map(ClassType::name)
.anyMatch(n -> n.equals(name));
return alreadyExists ? getUniqueVarName(prefix + "_") : name;
}
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
{
/*
@@ -184,9 +205,11 @@ class InternalRecordBuilderProcessor
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());
var codeBlockBuilder = CodeBlock.builder();
if (recordComponents.size() > 1) {
codeBlockBuilder.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName());
}
codeBlockBuilder.add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
if (parameterIndex > 0) {
codeBlockBuilder.add(", ");
@@ -196,7 +219,7 @@ class InternalRecordBuilderProcessor
codeBlockBuilder.add(parameterComponent.name());
}
else {
codeBlockBuilder.add("r.$L()", parameterComponent.name());
codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name());
}
});
codeBlockBuilder.add(");");
@@ -229,6 +252,27 @@ class InternalRecordBuilderProcessor
builder.addMethod(constructor);
}
private void addStaticBuilder()
{
/*
Adds an static builder similar to:
public static MyRecord(int p1, T p2, ...) {
return new MyRecord(p1, p2, ...);
}
*/
CodeBlock codeBlock = buildCodeBlock();
var builder = MethodSpec.methodBuilder(recordClassType.name())
.addJavadoc("Static constructor/builder. Can be used instead of new $L(...)\n", recordClassType.name())
.addTypeVariables(typeVariables)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addStatement(codeBlock);
recordComponents.forEach(component -> builder.addParameter(component.typeName(), component.name()));
this.builder.addMethod(builder.build());
}
private void addAllArgsConstructor()
{
/*
@@ -324,14 +368,14 @@ class InternalRecordBuilderProcessor
*/
var codeBuilder = CodeBlock.builder();
codeBuilder.add("return (this == o) || (");
codeBuilder.add("(o instanceof $L b)", builderClassType.name());
codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName);
recordComponents.forEach(recordComponent -> {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == b.$L)", name, name);
codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name);
}
else {
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name);
}
});
codeBuilder.add(")");
@@ -356,6 +400,22 @@ class InternalRecordBuilderProcessor
return new MyRecord(p1, p2, ...);
}
*/
CodeBlock codeBlock = buildCodeBlock();
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addStatement(codeBlock)
.build();
builder.addMethod(methodSpec);
}
private CodeBlock buildCodeBlock() {
/*
Builds the code block for allocating the record from its parts
*/
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
@@ -364,15 +424,7 @@ class InternalRecordBuilderProcessor
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
return codeBuilder.build();
}
private void addStaticCopyBuilderMethod()
@@ -460,6 +512,38 @@ 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 for " + 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)
{
/*

View File

@@ -15,12 +15,14 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordInterface;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -28,7 +30,13 @@ import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -46,9 +54,9 @@ class InternalRecordInterfaceProcessor {
private static final String FAKE_METHOD_NAME = "__FAKE__";
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = ElementUtils.getPackageName(iface);
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);
this.iface = iface;
@@ -65,7 +73,7 @@ class InternalRecordInterfaceProcessor {
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (recordInterface.addRecordBuilder()) {
if (addRecordBuilder) {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
builder.addAnnotation(RecordBuilder.class);
builder.addSuperinterface(builderClassType.typeName());

View File

@@ -45,6 +45,11 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
*/
public static final String OPTION_BUILD_METHOD_NAME = "buildMethodName";
/**
* @see #downCastMethodName()
*/
public static final String OPTION_DOWN_CAST_METHOD_NAME = "downCastMethodName";
/**
* @see #componentsMethodName()
*/
@@ -80,6 +85,7 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
private final String copyMethodName;
private final String builderMethodName;
private final String buildMethodName;
private final String downCastMethodName;
private final String componentsMethodName;
private final String withClassName;
private final String withClassMethodPrefix;
@@ -93,6 +99,7 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
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());
downCastMethodName = options.getOrDefault(OPTION_DOWN_CAST_METHOD_NAME, DEFAULT.downCastMethodName());
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());
@@ -126,6 +133,11 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
return buildMethodName;
}
@Override
public String downCastMethodName() {
return downCastMethodName;
}
@Override
public String componentsMethodName() {
return componentsMethodName;

View File

@@ -18,39 +18,46 @@ package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.*;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_BUILDER;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_INTERFACE;
@SupportedAnnotationTypes({RECORD_BUILDER, RECORD_INTERFACE})
public class RecordBuilderProcessor extends AbstractProcessor {
public static final String RECORD_BUILDER = "io.soabase.recordbuilder.core.RecordBuilder";
public static final String RECORD_INTERFACE = "io.soabase.recordbuilder.core.RecordInterface";
private static final String RECORD_BUILDER = RecordBuilder.class.getName();
private static final String RECORD_BUILDER_INCLUDE = RecordBuilder.Include.class.getName().replace('$', '.');
private static final String RECORD_INTERFACE = RecordInterface.class.getName();
private static final String RECORD_INTERFACE_INCLUDE = RecordInterface.Include.class.getName().replace('$', '.');
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_BUILDER).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_INTERFACE).build();
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilder.class.getName()).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation)
.forEach(element -> process(annotation, element))
);
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE);
}
@Override
public SourceVersion getSupportedSourceVersion() {
// we don't directly return RELEASE_14 as that may
@@ -63,37 +70,120 @@ public class RecordBuilderProcessor extends AbstractProcessor {
private void process(TypeElement annotation, Element element) {
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
if (annotation.getQualifiedName().toString().equals(RECORD_BUILDER)) {
processRecordBuilder((TypeElement) element, metaData);
} else if (annotation.getQualifiedName().toString().equals(RECORD_INTERFACE)) {
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class), metaData);
} else {
String annotationClass = annotation.getQualifiedName().toString();
if ( annotationClass.equals(RECORD_BUILDER) )
{
processRecordBuilder((TypeElement)element, metaData, Optional.empty());
}
else if ( annotationClass.equals(RECORD_INTERFACE) )
{
processRecordInterface((TypeElement)element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
}
else if ( annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
processIncludes(element, metaData, annotationClass);
}
else
{
throw new RuntimeException("Unknown annotation: " + annotation);
}
}
private void processRecordInterface(TypeElement element, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
if (!element.getKind().isInterface()) {
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) {
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
if ( annotationMirrorOpt.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
}
else
{
var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get());
var classes = ElementUtils.getAnnotationValue(values, "value");
if ( classes.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element);
}
else
{
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(values, "packagePattern").orElse(null), "*");
var classesMirrors = ElementUtils.getClassesAttribute(classes.get());
for ( TypeMirror mirror : classesMirrors )
{
TypeElement typeElement = (TypeElement)processingEnv.getTypeUtils().asElement(mirror);
if ( typeElement == null )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + mirror, element);
}
else
{
var packageName = buildPackageName(packagePattern, element, typeElement);
if (packageName != null)
{
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(values, "addRecordBuilder");
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName));
}
else
{
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
}
}
}
}
}
}
}
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) {
PackageElement includedClassPackage = findPackageElement(includedClass, includedClass);
if (includedClassPackage == null) {
return null;
}
String replaced = packagePattern.replace("*", includedClassPackage.getQualifiedName().toString());
if (builderElement instanceof PackageElement) {
return replaced.replace("@", ((PackageElement)builderElement).getQualifiedName().toString());
}
return replaced.replace("@", ((PackageElement)builderElement.getEnclosingElement()).getQualifiedName().toString());
}
private PackageElement findPackageElement(Element actualElement, Element includedClass) {
if (includedClass == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element has not package", actualElement);
return null;
}
if (includedClass.getEnclosingElement() instanceof PackageElement) {
return (PackageElement)includedClass.getEnclosingElement();
}
return findPackageElement(actualElement, includedClass.getEnclosingElement());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageName) {
if ( !element.getKind().isInterface() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
return;
}
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, recordInterface, metaData);
if (!internalProcessor.isValid()) {
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
if ( !internalProcessor.isValid() )
{
return;
}
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
}
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData) {
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageName) {
// we use string based name comparison for the element kind,
// as the ElementKind.RECORD enum doesn't exist on JRE releases
// older than Java 14, and we don't want to throw unexpected
// NoSuchFieldErrors
if (!"RECORD".equals(record.getKind().name())) {
if ( !"RECORD".equals(record.getKind().name()) )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", record);
return;
}
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData);
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData, packageName);
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
@@ -105,7 +195,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
{
String fullyQualifiedName = packageName.isEmpty() ? builderClassType.name() : (packageName + "." + builderClassType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try ( Writer writer = sourceFile.openWriter() )
try (Writer writer = sourceFile.openWriter())
{
javaFile.writeTo(writer);
}
@@ -128,7 +218,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
{
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try ( Writer writer = sourceFile.openWriter() )
try (Writer writer = sourceFile.openWriter())
{
writer.write(recordSourceCode);
}
@@ -140,11 +230,10 @@ public class RecordBuilderProcessor extends AbstractProcessor {
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
var javaFileBuilder = JavaFile.builder(packageName, type)
.skipJavaLangImports(true)
.indent(metaData.fileIndent());
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
var comment = metaData.fileComment();
if ((comment != null) && !comment.isEmpty()) {
if ( (comment != null) && !comment.isEmpty() )
{
javaFileBuilder.addFileComment(comment);
}
return javaFileBuilder.build();
@@ -152,7 +241,8 @@ public class RecordBuilderProcessor extends AbstractProcessor {
private void handleWriteError(TypeElement element, IOException e) {
String message = "Could not create source file";
if (e.getMessage() != null) {
if ( e.getMessage() != null )
{
message = message + ": " + e.getMessage();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);

View File

@@ -0,0 +1,23 @@
/**
* 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.
*/
module io.soabase.record.builder.processor {
requires com.squareup.javapoet;
requires io.soabase.record.builder.core;
requires java.compiler;
exports io.soabase.recordbuilder.processor;
opens io.soabase.recordbuilder.processor;
}

View File

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

View File

@@ -0,0 +1,28 @@
/**
* 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 io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordInterface;
@RecordInterface.Include({
Thingy.class
})
@RecordBuilder.Include({
Nested.NestedRecord.class
})
public class Builder {
}

View File

@@ -0,0 +1,26 @@
/**
* 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 java.time.Instant;
public interface Customer {
String name();
String address();
Instant activeDate();
}

View File

@@ -0,0 +1,22 @@
/**
* 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 io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
public record Empty() implements EmptyBuilder.With {
}

View File

@@ -0,0 +1,20 @@
/**
* 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;
public class Nested {
record NestedRecord(int x, int y){}
}

View File

@@ -0,0 +1,18 @@
/**
* 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;
public record Pair<T, U>(T t, U u) {}

View File

@@ -0,0 +1,18 @@
/**
* 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;
public record Point(int x, int y) {}

View File

@@ -0,0 +1,21 @@
/**
* 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 io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
public record RecordWithAnR(int r, String b) {}

View File

@@ -0,0 +1,20 @@
/**
* 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;
public interface Thingy<T> {
T getIt();
}

View File

@@ -0,0 +1,22 @@
/**
* 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.
*/
@RecordBuilder.Include(value = {Point.class, Pair.class}, packagePattern = "*.foo")
@RecordInterface.Include(value = Customer.class, addRecordBuilder = false, packagePattern = "*.bar")
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordInterface;

View File

@@ -20,6 +20,9 @@ import org.junit.jupiter.api.Test;
import java.time.Instant;
import static io.soabase.recordbuilder.test.SimpleGenericRecordBuilder.SimpleGenericRecord;
import static io.soabase.recordbuilder.test.SimpleRecordBuilder.SimpleRecord;
public class TestRecordInterface
{
@Test
@@ -32,4 +35,17 @@ public class TestRecordInterface
Assertions.assertEquals(Instant.MIN, r2.time());
Assertions.assertEquals(Instant.MIN, r2.tomorrow());
}
@Test
public void testStaticConstructor()
{
var simple = SimpleRecord(10,"hey");
Assertions.assertEquals(simple.i(), 10);
Assertions.assertEquals(simple.s(), "hey");
var now = Instant.now();
var generic = SimpleGenericRecord(101, now);
Assertions.assertEquals(generic.i(), 101);
Assertions.assertEquals(generic.s(), now);
}
}

View File

@@ -59,4 +59,11 @@ 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));
}
}