Support options for RecordInterface

- Copy any @RecordBuilder.Options to the generated Record
- Support RecordInterface templates as well

Closes #64
This commit is contained in:
Jordan Zimmerman
2021-08-26 15:50:07 -05:00
parent 5f82af97ae
commit 76f93039b0
10 changed files with 157 additions and 61 deletions

View File

@@ -84,3 +84,6 @@ public @interface MyCoRecordBuilder {
Now, you can use `@MyCoRecordBuilder` instead of `@RecordBuilder` and the record
will be built with options as specified.
Note: the template mechanism also supports `@RecordInterface` templates via the `asRecordInterface` attribute.
When it is set a `@RecordInterface` template is created instead.

View File

@@ -151,5 +151,7 @@ public @interface RecordBuilder {
@Inherited
@interface Template {
RecordBuilder.Options options();
boolean asRecordInterface() default false;
}
}

View File

@@ -15,11 +15,7 @@
*/
package io.soabase.recordbuilder.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.*;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)

View File

@@ -55,7 +55,7 @@ class InternalRecordBuilderProcessor {
InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
var recordActualPackage = ElementUtils.getPackageName(record);
this.metaData = getMetaData(record, metaData);
this.metaData = metaData;
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = packageNameOpt.orElse(recordActualPackage);
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
@@ -128,11 +128,6 @@ class InternalRecordBuilderProcessor {
.collect(Collectors.toList());
}
private RecordBuilder.Options getMetaData(TypeElement record, RecordBuilder.Options metaData) {
var recordSpecificMetaData = record.getAnnotation(RecordBuilder.Options.class);
return (recordSpecificMetaData != null) ? recordSpecificMetaData : metaData;
}
private void addWithNestedClass() {
/*
Adds a nested interface that adds withers similar to:

View File

@@ -15,13 +15,10 @@
*/
package io.soabase.recordbuilder.processor;
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 com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -29,13 +26,7 @@ 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.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.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -56,9 +47,10 @@ class InternalRecordInterfaceProcessor {
private static final Set<String> javaBeanPrefixes = Set.of("get", "is");
private record Component(ExecutableElement element, Optional<String> alternateName){}
private record Component(ExecutableElement element, Optional<String> alternateName) {
}
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageNameOpt, boolean fromTemplate) {
this.processingEnv = processingEnv;
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);
@@ -81,6 +73,14 @@ class InternalRecordInterfaceProcessor {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
builder.addAnnotation(RecordBuilder.class);
builder.addSuperinterface(builderClassType.typeName());
if (fromTemplate) {
builder.addAnnotation(AnnotationSpec.get(metaData));
} else {
var options = iface.getAnnotation(RecordBuilder.Options.class);
if (options != null) {
builder.addAnnotation(AnnotationSpec.get(options));
}
}
}
alternateMethods = buildAlternateMethods(recordComponents);
@@ -88,8 +88,7 @@ class InternalRecordInterfaceProcessor {
recordType = builder.build();
}
boolean isValid()
{
boolean isValid() {
return !recordComponents.isEmpty();
}
@@ -105,8 +104,7 @@ class InternalRecordInterfaceProcessor {
return recordClassType;
}
String toRecord(String classSource)
{
String toRecord(String classSource) {
// javapoet does yet support records - so a class was created and we can reshape it
// The class will look something like this:
/*
@@ -139,8 +137,7 @@ class InternalRecordInterfaceProcessor {
return fixedRecord.toString();
}
private MethodSpec generateArgumentList()
{
private MethodSpec generateArgumentList() {
MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME);
recordComponents.forEach(component -> {
String name = component.alternateName.orElseGet(() -> component.element.getSimpleName().toString());
@@ -153,18 +150,18 @@ class InternalRecordInterfaceProcessor {
private List<String> buildAlternateMethods(List<Component> recordComponents) {
return recordComponents.stream()
.filter(component -> component.alternateName.isPresent())
.map(component -> {
var method = MethodSpec.methodBuilder(component.element.getSimpleName().toString())
.addAnnotation(Override.class)
.addAnnotation(generatedRecordInterfaceAnnotation)
.returns(ClassName.get(component.element.getReturnType()))
.addModifiers(Modifier.PUBLIC)
.addCode("return $L();", component.alternateName.get())
.build();
return method.toString();
})
.collect(Collectors.toList());
.filter(component -> component.alternateName.isPresent())
.map(component -> {
var method = MethodSpec.methodBuilder(component.element.getSimpleName().toString())
.addAnnotation(Override.class)
.addAnnotation(generatedRecordInterfaceAnnotation)
.returns(ClassName.get(component.element.getReturnType()))
.addModifiers(Modifier.PUBLIC)
.addCode("return $L();", component.alternateName.get())
.build();
return method.toString();
})
.collect(Collectors.toList());
}
private List<Component> getRecordComponents(TypeElement iface) {
@@ -180,8 +177,8 @@ class InternalRecordInterfaceProcessor {
}
return components;
}
private static class IllegalInterface extends RuntimeException
{
private static class IllegalInterface extends RuntimeException {
public IllegalInterface(String message) {
super(message);
}
@@ -219,14 +216,13 @@ class InternalRecordInterfaceProcessor {
});
}
private Optional<String> stripBeanPrefix(String name)
{
private Optional<String> stripBeanPrefix(String name) {
return javaBeanPrefixes.stream()
.filter(prefix -> name.startsWith(prefix) && (name.length() > prefix.length()))
.findFirst()
.map(prefix -> {
var stripped = name.substring(prefix.length());
return Character.toLowerCase(stripped.charAt(0)) + stripped.substring(1);
});
.filter(prefix -> name.startsWith(prefix) && (name.length() > prefix.length()))
.findFirst()
.map(prefix -> {
var stripped = name.substring(prefix.length());
return Character.toLowerCase(stripped.charAt(0)) + stripped.substring(1);
});
}
}

View File

@@ -77,12 +77,12 @@ public class RecordBuilderProcessor
{
String annotationClass = annotation.getQualifiedName().toString();
if (annotationClass.equals(RECORD_BUILDER)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processRecordBuilder((TypeElement) element, metaData, Optional.empty());
var typeElement = (TypeElement) element;
processRecordBuilder(typeElement, getMetaData(typeElement), Optional.empty());
}
else if (annotationClass.equals(RECORD_INTERFACE)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
var typeElement = (TypeElement) element;
processRecordInterface(typeElement, element.getAnnotation(RecordInterface.class).addRecordBuilder(), getMetaData(typeElement), Optional.empty(), false);
}
else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
@@ -90,11 +90,20 @@ public class RecordBuilderProcessor
} else {
var recordBuilderTemplate = annotation.getAnnotation(RecordBuilder.Template.class);
if (recordBuilderTemplate != null) {
processRecordBuilder((TypeElement) element, recordBuilderTemplate.options(), Optional.empty());
if (recordBuilderTemplate.asRecordInterface()) {
processRecordInterface((TypeElement) element, true, recordBuilderTemplate.options(), Optional.empty(), true);
} else {
processRecordBuilder((TypeElement) element, recordBuilderTemplate.options(), Optional.empty());
}
}
}
}
private RecordBuilder.Options getMetaData(TypeElement typeElement) {
var recordSpecificMetaData = typeElement.getAnnotation(RecordBuilder.Options.class);
return (recordSpecificMetaData != null) ? recordSpecificMetaData : RecordBuilderOptions.build(processingEnv.getOptions());
}
private void processIncludes(Element element, RecordBuilder.Options metaData, String annotationClass)
{
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
@@ -121,7 +130,7 @@ public class RecordBuilderProcessor
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));
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName), false);
}
else {
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
@@ -158,13 +167,13 @@ public class RecordBuilderProcessor
return findPackageElement(actualElement, includedClass.getEnclosingElement());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageName)
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageName, boolean fromTemplate)
{
if (!element.getKind().isInterface()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
return;
}
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName, fromTemplate);
if (!internalProcessor.isValid()) {
return;
}

View File

@@ -0,0 +1,36 @@
/**
* 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;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordInterface
@RecordBuilder.Options(useImmutableCollections = true)
public interface CollectionInterface<T, X extends Point> {
List<T> l();
Set<T> s();
Map<T, X> m();
Collection<X> c();
}

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.
*/
package io.soabase.recordbuilder.test;
@MyInterfaceTemplate
public interface InterfaceTemplateTest {
String name();
int age();
}

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 io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Template(options = @RecordBuilder.Options(
fileComment = "This is a test",
withClassName = "Com"),
asRecordInterface = true
)
public @interface MyInterfaceTemplate {
}

View File

@@ -15,6 +15,7 @@
*/
package io.soabase.recordbuilder.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.*;
@@ -23,6 +24,15 @@ import static io.soabase.recordbuilder.test.foo.PointBuilder.Point;
import static org.junit.jupiter.api.Assertions.*;
class TestCollections {
@Test
void testRecordBuilderOptionsCopied() {
try {
assertNotNull(CollectionInterfaceRecordBuilder.class.getDeclaredMethod("__list", List.class));
} catch (NoSuchMethodException e) {
Assertions.fail(e);
}
}
@Test
void testCollectionRecordDefaultValues() {
var defaultValues = CollectionRecordBuilder.builder().build();