Skip to content

Commit

Permalink
Support record component initializers
Browse files Browse the repository at this point in the history
New component annotation that specifies the name of either
a public static final field or a public static method that will be
used to initialize each record component in the generated builder
so as to support default values.

Closes #110
  • Loading branch information
Randgalt committed Jan 4, 2024
1 parent 60e9f35 commit be32ef3
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,23 @@
enum BuilderMode {
STANDARD, STAGED, STANDARD_AND_STAGED,
}

/**
* Apply to record components to specify a field initializer for the generated builder
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
@Inherited
@interface Initializer {
/**
* The name of a public static method or a public static final field in the source to use as the initializer
*/
String value();

/**
* The source class that contains the method/field specified by {@code value()}. If not specified, the target
* record is the source class.
*/
Class<?> source() default Object.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,9 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilder;

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.ElementKind;
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.element.*;
import javax.lang.model.type.TypeMirror;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -56,6 +50,13 @@ public static List<TypeMirror> getAttributeTypeMirrorList(AnnotationValue attrib
return values.stream().map(v -> (TypeMirror) v.getValue()).collect(Collectors.toList());
}

public static Optional<TypeMirror> getAttributeTypeMirror(AnnotationValue attribute) {
if (attribute == null) {
return Optional.empty();
}
return Optional.ofNullable((TypeMirror) attribute.getValue());
}

@SuppressWarnings("unchecked")
public static List<String> getAttributeStringList(AnnotationValue attribute) {
List<? extends AnnotationValue> values = (attribute != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2019 The original author or authors
*
* 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 com.squareup.javapoet.CodeBlock;
import io.soabase.recordbuilder.core.RecordBuilder;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.soabase.recordbuilder.processor.ElementUtils.getAnnotationValue;
import static io.soabase.recordbuilder.processor.ElementUtils.getStringAttribute;

class InitializerUtil {
static Map<String, CodeBlock> detectInitializers(ProcessingEnvironment processingEnv, TypeElement record) {
return record.getEnclosedElements().stream().flatMap(element -> {
var annotation = ElementUtils.findAnnotationMirror(processingEnv, element,
RecordBuilder.Initializer.class.getName().replace("$", "."));
var value = annotation.flatMap(mirror -> getAnnotationValue(mirror.getElementValues(), "value"));
if (value.isEmpty()) {
return Stream.of();
}

var source = annotation.flatMap(mirror -> getAnnotationValue(mirror.getElementValues(), "source"));

var name = getStringAttribute(value.get(), "");
var sourceElement = findSourceElement(processingEnv, record, source);

Optional<CodeBlock> initializer = sourceElement.getEnclosedElements().stream()
.filter(enclosedElement -> enclosedElement.getSimpleName().toString().equals(name))
.flatMap(enclosedElement -> {
if ((enclosedElement.getKind() == ElementKind.METHOD)
&& isValid(processingEnv, element, (ExecutableElement) enclosedElement)) {
return Stream.of(CodeBlock.builder().add("$T.$L()", sourceElement, name).build());
}

if ((enclosedElement.getKind() == ElementKind.FIELD)
&& isValid(processingEnv, element, (VariableElement) enclosedElement)) {
return Stream.of(CodeBlock.builder().add("$T.$L", sourceElement, name).build());
}

return Stream.of();
}).findFirst();

if (initializer.isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"No matching public static field or method found for initializer named: " + name, element);
}

return initializer.map(codeBlock -> Map.entry(element.getSimpleName().toString(), codeBlock)).stream();
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private static boolean isValid(ProcessingEnvironment processingEnv, Element element,
ExecutableElement executableElement) {
if (executableElement.getModifiers().contains(Modifier.PUBLIC)
&& executableElement.getModifiers().contains(Modifier.STATIC)) {
return processingEnv.getTypeUtils().isSameType(executableElement.getReturnType(), element.asType());
}
return false;
}

private static boolean isValid(ProcessingEnvironment processingEnv, Element element,
VariableElement variableElement) {
if (variableElement.getModifiers().contains(Modifier.PUBLIC)
&& variableElement.getModifiers().contains(Modifier.STATIC)
&& variableElement.getModifiers().contains(Modifier.FINAL)) {
return processingEnv.getTypeUtils().isSameType(variableElement.asType(), element.asType());
}
return false;
}

private static TypeElement findSourceElement(ProcessingEnvironment processingEnv, TypeElement record,
Optional<? extends AnnotationValue> source) {
return source.flatMap(ElementUtils::getAttributeTypeMirror)
.map(sourceMirror -> (TypeElement) processingEnv.getTypeUtils().asElement(sourceMirror)).orElse(record);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class InternalRecordBuilderProcessor {
private static final TypeVariableName rType = TypeVariableName.get("R");
private final ProcessingEnvironment processingEnv;
private final Modifier constructorVisibilityModifier;
private final Map<String, CodeBlock> initializers;

InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record,
RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
Expand All @@ -73,6 +74,7 @@ class InternalRecordBuilderProcessor {
notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern());
collectionBuilderUtils = new CollectionBuilderUtils(recordComponents, this.metaData);
constructorVisibilityModifier = metaData.publicBuilderConstructors() ? Modifier.PUBLIC : Modifier.PRIVATE;
initializers = InitializerUtil.detectInitializers(processingEnv, record);

builder = TypeSpec.classBuilder(builderClassType.name()).addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(metaData.builderClassModifiers()).addTypeVariables(typeVariables);
Expand Down Expand Up @@ -760,13 +762,18 @@ private void add1Field(ClassType component) {
* private T p;
*/
var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE);
if (metaData.emptyDefaultForOptional()) {

CodeBlock initializer = initializers.get(component.name());
if (initializer != null) {
fieldSpecBuilder.initializer(initializer);
} else if (metaData.emptyDefaultForOptional()) {
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());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2019 The original author or authors
*
* 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.RecordBuilderFull;

import java.util.Optional;

@RecordBuilderFull
public record Initialized(@RecordBuilder.Initializer("DEFAULT_NAME") String name,
@RecordBuilder.Initializer("defaultAge") int age, Optional<String> dummy,
@RecordBuilder.Initializer(value = "defaultAltName", source = AltSource.class) Optional<String> altName) {

public static final String DEFAULT_NAME = "hey";

public static int defaultAge() {
return 18;
}

public static class AltSource {
public static Optional<String> defaultAltName() {
return Optional.of("alt");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2019 The original author or authors
*
* 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 org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Optional;

class TestInitialized {
@Test
void testInitialized() {
var initialized = InitializedBuilder.builder().build();
Assertions.assertEquals(Initialized.DEFAULT_NAME, initialized.name());
Assertions.assertEquals(Initialized.defaultAge(), initialized.age());
Assertions.assertEquals(Initialized.AltSource.defaultAltName(), initialized.altName());
Assertions.assertEquals(Optional.empty(), initialized.dummy());
}
}

0 comments on commit be32ef3

Please sign in to comment.