Compare commits

..

9 Commits

Author SHA1 Message Date
Jordan Zimmerman
d112a1b352 [maven-release-plugin] prepare release record-builder-33 2022-04-08 08:35:07 +01:00
Jordan Zimmerman
86093b6bad Make the functional static builder optional (#105)
Closes #100
2022-04-08 08:33:46 +01:00
Jordan Zimmerman
7b6ad4d7ba Generated equals() and the default with() method were missing generic angle brackets causing compiler warnings (#102)
Closes #101
2022-04-08 08:29:55 +01:00
Jordan Zimmerman
cd059f1207 Options/changes so that Jacoco checks don't fail (#104)
- Added new optional Annotation `@RecordBuilderGenerated` - Jacoco ignores
classes with any annotation names "*Generated*" but it needs to be class retained.
For backward compatibility this annotation is not added by default (though it's been
added to `@RecordBuilderFull`). There is a new option to enable it.
- The from with method now uses an internal static class instead of an anonymous
inner class so that the annotation can be on this as well. This new class's name
is configurable in the options.

Thanks to user @madisparn for initial PR and issue report.

Fixes #87
2022-04-07 12:21:50 +01:00
Jordan Zimmerman
642dd01421 Don't include @Valid on base interfaces (#99)
The validation API doesn't accept `@Valid` on base interfaces. Filter
them out.

Fixes #97
2022-03-21 10:06:02 +00:00
Mads Baardsgaard
efd1a6b0d4 Add flag to add concrete setters for optionals (#94)
* Add flag to add concrete setters for optionals

* Add another test with concrete optionals disabled
2022-03-21 09:08:00 +00:00
Mads Baardsgaard
b525eddc76 Make withers and getters optional features (#95)
* Make withers and getters optional features

* Add example record with wither and getter disabled
2022-03-21 08:51:42 +00:00
Jordan Zimmerman
d3828eda74 Fix NPE for uninitialized non-null collection (#98)
Closes #91

Co-authored-by: Clément MATHIEU <clement@unportant.info>
2022-03-20 10:46:39 +00:00
Jordan Zimmerman
fef69af183 [maven-release-plugin] prepare for next development iteration 2022-02-04 11:45:10 +00:00
25 changed files with 555 additions and 96 deletions

24
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>32</version>
<version>33</version>
<modules>
<module>record-builder-core</module>
@@ -32,6 +32,9 @@
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<jacoco-maven-plugin-version>0.8.7</jacoco-maven-plugin-version>
<license-file-path>src/etc/header.txt</license-file-path>
@@ -77,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-32</tag>
<tag>record-builder-33</tag>
</scm>
<issueManagement>
@@ -153,6 +156,12 @@
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@@ -319,6 +328,12 @@
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin-version}</version>
</plugin>
</plugins>
</pluginManagement>
@@ -352,6 +367,11 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>

View File

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

View File

@@ -99,6 +99,11 @@ public @interface RecordBuilder {
*/
String componentsMethodName() default "stream";
/**
* If true, a "With" interface is generated and an associated static factory
*/
boolean enableWither() default true;
/**
* The name to use for the nested With class
*/
@@ -139,6 +144,11 @@ public @interface RecordBuilder {
*/
boolean emptyDefaultForOptional() default true;
/**
* Add non-optional setter methods for optional record components.
*/
boolean addConcreteSettersForOptional() default false;
/**
* Add not-null checks for record components annotated with any annotation named either "NotNull",
* "NoNull", or "NonNull" (see {@link #interpretNotNullsPattern()} for the actual regex matching pattern).
@@ -195,6 +205,11 @@ public @interface RecordBuilder {
*/
String setterPrefix() default "";
/**
* If true, getters will be generated for the Builder class.
*/
boolean enableGetters() default true;
/**
* If set, all builder getter methods will be prefixed with this string. Camel-casing will
* still be enforced, so if this option is set to "get", a field named "myField" will get
@@ -219,6 +234,24 @@ public @interface RecordBuilder {
* this option does nothing.
*/
String beanClassName() default "";
/**
* If true, generated classes are annotated with {@code RecordBuilderGenerated} which has a retention
* policy of {@code CLASS}. This ensures that analyzers such as Jacoco will ignore the generated class.
*/
boolean addClassRetainedGenerated() default false;
/**
* The {@link #fromMethodName} method instantiates an internal private class. This is the
* name of that class.
*/
String fromWithClassName() default "_FromWith";
/**
* If true, a functional-style builder is added so that record instances can be instantiated
* without {@code new}.
*/
boolean addStaticBuilder() default true;
}
@Retention(RetentionPolicy.CLASS)

View File

@@ -21,7 +21,8 @@ import java.lang.annotation.*;
interpretNotNulls = true,
useImmutableCollections = true,
addSingleItemCollectionBuilders = true,
addFunctionalMethodsToWith = true
addFunctionalMethodsToWith = true,
addClassRetainedGenerated = true
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)

View File

@@ -0,0 +1,30 @@
/**
* 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.core;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* Jacoco ignores classes and methods annotated with `*Generated`
*/
@Target({PACKAGE, TYPE, METHOD, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, PARAMETER})
@Retention(RetentionPolicy.CLASS)
public @interface RecordBuilderGenerated {
}

View File

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

View File

@@ -109,6 +109,10 @@ class CollectionBuilderUtils {
return Optional.empty();
}
boolean isImmutableCollection(RecordClassType component) {
return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) || component.rawTypeName().equals(collectionType));
}
boolean isList(RecordClassType component) {
return component.rawTypeName().equals(listType);
}

View File

@@ -23,6 +23,7 @@ import javax.lang.model.element.*;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -32,6 +33,7 @@ import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleIt
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class InternalRecordBuilderProcessor {
private final RecordBuilder.Options metaData;
@@ -47,10 +49,7 @@ class InternalRecordBuilderProcessor {
private final CollectionBuilderUtils collectionBuilderUtils;
private static final TypeName overrideType = TypeName.get(Override.class);
private static final TypeName optionalType = TypeName.get(Optional.class);
private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static final TypeName validType = ClassName.get("javax.validation", "Valid");
private static final TypeName validatorTypeName = ClassName.get("io.soabase.recordbuilder.validator", "RecordBuilderValidator");
private static final TypeVariableName rType = TypeVariableName.get("R");
private final ProcessingEnvironment processingEnv;
@@ -71,19 +70,28 @@ class InternalRecordBuilderProcessor {
builder = TypeSpec.classBuilder(builderClassType.name())
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
addVisibility(recordActualPackage.equals(packageName), record.getModifiers());
addWithNestedClass();
if (metaData.enableWither()) {
addWithNestedClass();
}
if (!metaData.beanClassName().isEmpty()) {
addBeanNestedClass();
}
addDefaultConstructor();
addStaticBuilder();
if (metaData.addStaticBuilder()) {
addStaticBuilder();
}
if (recordComponents.size() > 0) {
addAllArgsConstructor();
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticFromWithMethod();
if (metaData.enableWither()) {
addStaticFromWithMethod();
}
addStaticComponentsMethod();
addBuildMethod();
addToStringMethod();
@@ -92,7 +100,12 @@ class InternalRecordBuilderProcessor {
recordComponents.forEach(component -> {
add1Field(component);
add1SetterMethod(component);
add1GetterMethod(component);
if (metaData.enableGetters()) {
add1GetterMethod(component);
}
if (metaData.addConcreteSettersForOptional()) {
add1ConcreteOptionalSetterMethod(component);
}
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, EXCLUDE_WILDCARD_TYPES);
collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component));
});
@@ -151,6 +164,9 @@ class InternalRecordBuilderProcessor {
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
classBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}
recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component, prefixedName(component, true)));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
@@ -225,7 +241,7 @@ class InternalRecordBuilderProcessor {
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("return new $L(", builderClassType.name());
.add("return new $L$L(", builderClassType.name(), typeVariables.isEmpty() ? "" : "<>");
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
@@ -371,8 +387,10 @@ class InternalRecordBuilderProcessor {
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");
if (!collectionBuilderUtils.isImmutableCollection(component)) {
if (!component.typeName().isPrimitive() && isNullAnnotated(component)) {
builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required");
}
}
}
}
@@ -474,7 +492,12 @@ class InternalRecordBuilderProcessor {
*/
var codeBuilder = CodeBlock.builder();
codeBuilder.add("return (this == o) || (");
codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName);
if (typeVariables.isEmpty()) {
codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName);
} else {
String wildcardList = typeVariables.stream().map(__ -> "?").collect(Collectors.joining(","));
codeBuilder.add("(o instanceof $L<$L> $L)", builderClassType.name(), wildcardList, uniqueVarName);
}
recordComponents.forEach(recordComponent -> {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
@@ -521,6 +544,16 @@ class InternalRecordBuilderProcessor {
*/
var codeBuilder = CodeBlock.builder();
IntStream.range(0, recordComponents.size()).forEach(index -> {
var recordComponent = recordComponents.get(index);
if (collectionBuilderUtils.isImmutableCollection(recordComponent)) {
codeBuilder.add("$[$L = ", recordComponent.name());
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
codeBuilder.add(";\n$]");
}
});
addNullCheckCodeBlock(codeBuilder);
codeBuilder.add("$[return ");
if (metaData.useValidationApi()) {
@@ -531,7 +564,7 @@ class InternalRecordBuilderProcessor {
if (index > 0) {
codeBuilder.add(", ");
}
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
if (metaData.useValidationApi()) {
@@ -541,63 +574,83 @@ class InternalRecordBuilderProcessor {
return codeBuilder.build();
}
private TypeName buildWithTypeName()
{
ClassName rawTypeName = ClassName.get(packageName, builderClassType.name() + "." + metaData.withClassName());
if (typeVariables.isEmpty()) {
return rawTypeName;
}
return ParameterizedTypeName.get(rawTypeName, typeVariables.toArray(new TypeName[]{}));
}
private void addFromWithClass() {
/*
Adds static private class that implements/proxies the Wither
private static final class _FromWith implements MyRecordBuilder.With {
private final MyRecord from;
@Override
public String p1() {
return from.p1();
}
@Override
public String p2() {
return from.p2();
}
}
*/
var fromWithClassBuilder = TypeSpec.classBuilder(metaData.fromWithClassName())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addSuperinterface(buildWithTypeName());
if (metaData.addClassRetainedGenerated()) {
fromWithClassBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}
fromWithClassBuilder.addField(recordClassType.typeName(), "from", Modifier.PRIVATE, Modifier.FINAL);
MethodSpec constructorSpec = MethodSpec.constructorBuilder()
.addParameter(recordClassType.typeName(), "from")
.addStatement("this.from = from")
.addModifiers(Modifier.PRIVATE)
.build();
fromWithClassBuilder.addMethod(constructorSpec);
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
MethodSpec methodSpec = MethodSpec.methodBuilder(prefixedName(component, true))
.returns(component.typeName())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return from.$L()", component.name())
.build();
fromWithClassBuilder.addMethod(methodSpec);
});
this.builder.addType(fromWithClassBuilder.build());
}
private void addStaticFromWithMethod() {
/*
Adds static method that returns a "with"er view of an existing record.
public static With from(MyRecord from) {
return new MyRecordBuilder.With() {
@Override
public String p1() {
return from.p1();
}
@Override
public String p2() {
return from.p2();
}
};
return new _FromWith(from);
}
*/
var witherClassNameBuilder = CodeBlock.builder()
.add("$L.$L", builderClassType.name(), metaData.withClassName());
if (!typeVariables.isEmpty()) {
witherClassNameBuilder.add("<");
IntStream.range(0, typeVariables.size()).forEach(index -> {
if (index > 0) {
witherClassNameBuilder.add(", ");
}
witherClassNameBuilder.add(typeVariables.get(index).name);
});
witherClassNameBuilder.add(">");
}
var witherClassName = witherClassNameBuilder.build().toString();
var codeBuilder = CodeBlock.builder()
.add("return new $L", witherClassName)
.add("() {\n").indent();
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
if (index > 0) {
codeBuilder.add("\n");
}
codeBuilder.add("@Override\n")
.add("public $T $L() {\n", component.typeName(), prefixedName(component, true))
.indent()
.addStatement("return from.$L()", component.name())
.unindent()
.add("}\n");
});
codeBuilder.unindent().addStatement("}");
var withType = ClassName.get("", witherClassName);
var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName())
addFromWithClass();
var methodSpec = MethodSpec.methodBuilder(metaData.fromMethodName())
.addJavadoc("Return a \"with\"er for an existing record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), metaData.fromMethodName())
.returns(withType)
.addCode(codeBuilder.build())
.returns(buildWithTypeName())
.addStatement("return new $L$L(from)", metaData.fromWithClassName(), typeVariables.isEmpty() ? "" : "<>")
.build();
builder.addMethod(methodSpec);
}
@@ -690,31 +743,17 @@ class InternalRecordBuilderProcessor {
*/
var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE);
if (metaData.emptyDefaultForOptional()) {
TypeName thisOptionalType = null;
if (isOptional(component)) {
thisOptionalType = optionalType;
} else if (component.typeName().equals(optionalIntType)) {
thisOptionalType = optionalIntType;
} else if (component.typeName().equals(optionalLongType)) {
thisOptionalType = optionalLongType;
} else if (component.typeName().equals(optionalDoubleType)) {
thisOptionalType = optionalDoubleType;
}
if (thisOptionalType != null) {
var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType).build();
Optional<OptionalType> thisOptionalType = OptionalType.fromClassType(component);
if (thisOptionalType.isPresent()) {
var codeBlock = CodeBlock.builder()
.add("$T.empty()", thisOptionalType.get().typeName())
.build();
fieldSpecBuilder.initializer(codeBlock);
}
}
builder.addField(fieldSpecBuilder.build());
}
private boolean isOptional(ClassType component) {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
}
private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component, String methodName) {
/*
For a single record component, add a getter similar to:
@@ -726,7 +765,7 @@ class InternalRecordBuilderProcessor {
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName());
addAccessorAnnotations(component, methodSpecBuilder);
addAccessorAnnotations(component, methodSpecBuilder, this::filterOutValid);
classBuilder.addMethod(methodSpecBuilder.build());
}
@@ -734,6 +773,10 @@ class InternalRecordBuilderProcessor {
return !annotationSpec.type.equals(overrideType);
}
private boolean filterOutValid(AnnotationSpec annotationSpec) {
return !annotationSpec.type.equals(validType);
}
private void addConstructorAnnotations(RecordClassType component, ParameterSpec.Builder parameterSpecBuilder) {
if (metaData.inheritComponentAnnotations()) {
component.getCanonicalConstructorAnnotations()
@@ -744,12 +787,13 @@ class InternalRecordBuilderProcessor {
}
}
private void addAccessorAnnotations(RecordClassType component, MethodSpec.Builder methodSpecBuilder) {
private void addAccessorAnnotations(RecordClassType component, MethodSpec.Builder methodSpecBuilder, Predicate<AnnotationSpec> additionalFilter) {
if (metaData.inheritComponentAnnotations()) {
component.getAccessorAnnotations()
.stream()
.map(AnnotationSpec::get)
.filter(this::filterOutOverride)
.filter(additionalFilter)
.forEach(methodSpecBuilder::addAnnotation);
}
}
@@ -891,7 +935,7 @@ class InternalRecordBuilderProcessor {
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName())
.addStatement("return $L", component.name());
addAccessorAnnotations(component, methodSpecBuilder);
addAccessorAnnotations(component, methodSpecBuilder, __ -> true);
builder.addMethod(methodSpecBuilder.build());
}
@@ -926,6 +970,33 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec.build());
}
private void add1ConcreteOptionalSetterMethod(RecordClassType component) {
/*
For a single optional record component, add a concrete setter similar to:
public MyRecordBuilder p(T p) {
this.p = p;
return this;
}
*/
var optionalType = OptionalType.fromClassType(component);
if (optionalType.isEmpty()) {
return;
}
var type = optionalType.get();
var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName());
var parameterSpecBuilder = ParameterSpec.builder(type.valueType(), component.name());
methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addStatement("this.$L = $T.of($L)", component.name(), type.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
builder.addMethod(methodSpec.build());
}
private List<TypeVariableName> typeVariablesWithReturn() {
var variables = new ArrayList<TypeVariableName>();
variables.add(rType);
@@ -993,7 +1064,7 @@ class InternalRecordBuilderProcessor {
private String prefixedName(RecordClassType component, boolean isGetter) {
BiFunction<String, String, String> prefixer = (p, s) -> p.isEmpty()
? s : p + Character.toUpperCase(s.charAt(0)) + s.substring(1);
? s : p + Character.toUpperCase(s.charAt(0)) + s.substring(1);
boolean isBool = component.typeName().toString().toLowerCase(Locale.ROOT).equals("boolean");
if (isGetter) {
if (isBool) {

View File

@@ -33,6 +33,7 @@ import java.util.stream.Collectors;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordInterfaceAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class InternalRecordInterfaceProcessor {
private final ProcessingEnvironment processingEnv;
@@ -68,6 +69,9 @@ class InternalRecordInterfaceProcessor {
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
if (addRecordBuilder) {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());

View File

@@ -0,0 +1,61 @@
/**
* 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.processor;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
public record OptionalType(TypeName typeName, TypeName valueType) {
private static final TypeName optionalType = TypeName.get(Optional.class);
private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static boolean isOptional(ClassType component) {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)
&& parameterizedTypeName.rawType.equals(optionalType);
}
static Optional<OptionalType> fromClassType(final ClassType component) {
if (isOptional(component)) {
if (!(component.typeName() instanceof ParameterizedTypeName parameterizedType)) {
return Optional.of(new OptionalType(optionalType, TypeName.get(Object.class)));
}
final TypeName containingType = parameterizedType.typeArguments.isEmpty()
? TypeName.get(Object.class)
: parameterizedType.typeArguments.get(0);
return Optional.of(new OptionalType(optionalType, containingType));
}
if (component.typeName().equals(optionalIntType)) {
return Optional.of(new OptionalType(optionalIntType, TypeName.get(int.class)));
}
if (component.typeName().equals(optionalLongType)) {
return Optional.of(new OptionalType(optionalLongType, TypeName.get(long.class)));
}
if (component.typeName().equals(optionalDoubleType)) {
return Optional.of(new OptionalType(optionalDoubleType, TypeName.get(double.class)));
}
return Optional.empty();
}
}

View File

@@ -19,6 +19,7 @@ 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.RecordBuilderGenerated;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.AbstractProcessor;
@@ -46,6 +47,7 @@ public class RecordBuilderProcessor
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();
static final AnnotationSpec recordBuilderGeneratedAnnotation = AnnotationSpec.builder(RecordBuilderGenerated.class).build();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>32</version>
<version>33</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -66,6 +66,48 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>default-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<includes>
<include>io/soabase/recordbuilder/test/jacoco/*</include>
</includes>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>COMPLEXITY</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -22,5 +22,5 @@ import java.util.List;
import java.util.Map;
@RecordBuilderFull
public record FullRecord(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecord> fullRecords) {
public record FullRecord(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecord> fullRecords, @NotNull String justAString) {
}

View File

@@ -0,0 +1,23 @@
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
/**
* 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.Options(addStaticBuilder = false)
@RecordBuilder
public record NoStaticBuilder(String foo) {
}

View File

@@ -22,6 +22,6 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
@RecordBuilder.Options(emptyDefaultForOptional = true)
@RecordBuilder.Options(emptyDefaultForOptional = true, addConcreteSettersForOptional = true)
@RecordBuilder
public record RecordWithOptional(Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}

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.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Options(emptyDefaultForOptional = true)
@RecordBuilder
public record RecordWithOptional2(Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}

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 javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@RecordBuilder
@RecordBuilder.Options(useValidationApi = true)
public record RequestWithValid(@NotNull @Valid Part part) implements RequestWithValidBuilder.With {
public record Part(@NotBlank String name) {}
}

View File

@@ -18,8 +18,9 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.constraints.NotNull;
import java.util.List;
@RecordBuilder.Options(interpretNotNulls = true)
@RecordBuilder
public record RequiredRecord(@NotNull String hey, @NotNull int i) implements RequiredRecordBuilder.With {
public record RequiredRecord(@NotNull String hey, @NotNull int i, @NotNull List<String> l) implements RequiredRecordBuilder.With {
}

View File

@@ -0,0 +1,25 @@
/**
* 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
@RecordBuilder.Options(
enableGetters = false,
enableWither = false
)
public record StrippedFeaturesRecord(int aField) {}

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.jacoco;
import io.soabase.recordbuilder.core.RecordBuilderFull;
import io.soabase.recordbuilder.core.RecordBuilderGenerated;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@RecordBuilderFull
@RecordBuilderGenerated
public record FullRecordForJacoco(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecordForJacoco> fullRecords, @NotNull String justAString) {
}

View File

@@ -33,4 +33,36 @@ class TestOptional {
Assertions.assertEquals(OptionalLong.empty(), record.l());
Assertions.assertEquals(OptionalDouble.empty(), record.d());
}
@Test
void testRawSetters() {
var record = RecordWithOptionalBuilder.builder()
.value("value")
.raw("rawValue")
.i(42)
.l(424242L)
.d(42.42)
.build();
Assertions.assertEquals(Optional.of("value"), record.value());
Assertions.assertEquals(Optional.of("rawValue"), record.raw());
Assertions.assertEquals(OptionalInt.of(42), record.i());
Assertions.assertEquals(OptionalLong.of(424242L), record.l());
Assertions.assertEquals(OptionalDouble.of(42.42), record.d());
}
@Test
void testOptionalSetters() {
var record = RecordWithOptional2Builder.builder()
.value(Optional.of("value"))
.raw(Optional.of("rawValue"))
.i(OptionalInt.of(42))
.l(OptionalLong.of(424242L))
.d(OptionalDouble.of(42.42))
.build();
Assertions.assertEquals(Optional.of("value"), record.value());
Assertions.assertEquals(Optional.of("rawValue"), record.raw());
Assertions.assertEquals(OptionalInt.of(42), record.i());
Assertions.assertEquals(OptionalLong.of(424242L), record.l());
Assertions.assertEquals(OptionalDouble.of(42.42), record.d());
}
}

View File

@@ -20,11 +20,15 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class TestRecordBuilderFull {
@Test
void testNonNull() {
Assertions.assertThrows(NullPointerException.class, () -> FullRecordBuilder.builder().build());
var record = FullRecordBuilder.builder().justAString("").build();
Assertions.assertEquals(List.of(), record.numbers());
Assertions.assertEquals(Map.of(), record.fullRecords());
}
@Test
@@ -32,6 +36,7 @@ class TestRecordBuilderFull {
var record = FullRecordBuilder.builder()
.fullRecords(new HashMap<>())
.numbers(new ArrayList<>())
.justAString("")
.build();
Assertions.assertThrows(UnsupportedOperationException.class, () -> record.fullRecords().put(1, record));
Assertions.assertThrows(UnsupportedOperationException.class, () -> record.numbers().add(1));

View File

@@ -19,6 +19,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.validation.ValidationException;
import java.util.List;
class TestValidation {
@Test
@@ -33,7 +34,7 @@ class TestValidation {
@Test
void testNotNullsWithNewProperty() {
var valid = RequiredRecordBuilder.builder().hey("hey").i(1).build();
var valid = RequiredRecordBuilder.builder().hey("hey").i(1).l(List.of()).build();
Assertions.assertThrows(NullPointerException.class, () -> valid.withHey(null));
}
@@ -42,4 +43,14 @@ class TestValidation {
var valid = RequiredRecord2Builder.builder().hey("hey").i(1).build();
Assertions.assertThrows(ValidationException.class, () -> valid.withHey(null));
}
@Test
void testRequestWithValid() {
Assertions.assertDoesNotThrow(() -> RequestWithValidBuilder.builder()
.part(new RequestWithValid.Part("jsfjsf"))
.build());
Assertions.assertThrows(ValidationException.class, () -> RequestWithValidBuilder.builder()
.part(new RequestWithValid.Part(""))
.build());
}
}

View File

@@ -15,13 +15,14 @@
*/
package io.soabase.recordbuilder.test;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.stream.Stream;
public class TestCustomMethodNames {
import static org.junit.jupiter.api.Assertions.*;
public class TestVariousOptions {
@Test
public void builderGetsCustomSetterAndGetterNames() {
@@ -54,4 +55,15 @@ public class TestCustomMethodNames {
assertEquals(List.of(2), obj.getTheList());
assertTrue(obj.isTheBoolean());
}
@Test
public void noStaticBuilder() {
boolean hasStaticBuilder = Stream.of(NoStaticBuilderBuilder.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("NoStaticBuilder"));
assertFalse(hasStaticBuilder);
hasStaticBuilder = Stream.of(SimpleRecordBuilder.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("SimpleRecord"));
assertTrue(hasStaticBuilder);
}
}

View File

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