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 2, 2024
1 parent 66458a7 commit 74f2e52
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,8 @@
*/
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 javax.lang.model.element.Modifier;
import java.lang.annotation.*;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
Expand Down Expand Up @@ -321,4 +316,17 @@

boolean asRecordInterface() default false;
}

/**
* 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 to use as the initializer
*/
String value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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;

class InitializerUtil {
static Map<String, CodeBlock> detectInitializers(ProcessingEnvironment processingEnv, TypeElement record) {
return record.getEnclosedElements().stream().flatMap(element -> {
RecordBuilder.Initializer annotation = element.getAnnotation(RecordBuilder.Initializer.class);
if (annotation == null) {
return Stream.of();
}

String name = annotation.value();
Optional<CodeBlock> initializer = record.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()", record, name).build());
}

if ((enclosedElement.getKind() == ElementKind.FIELD)
&& isValid(processingEnv, element, (VariableElement) enclosedElement)) {
return Stream.of(CodeBlock.builder().add("$T.$L", record, 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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 @@ -72,6 +73,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 @@ -649,13 +651,21 @@ 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 (metaData.emptyDefaultForOptional() && (initializer == null)) {
Optional<OptionalType> thisOptionalType = OptionalType.fromClassType(component);
if (thisOptionalType.isPresent()) {
var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType.get().typeName()).build();
fieldSpecBuilder.initializer(codeBlock);
}
}

if (initializer != null) {
fieldSpecBuilder.initializer(initializer);
}

builder.addField(fieldSpecBuilder.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ private void writeRecordBuilderJavaFile(TypeElement record, String packageName,
String fullyQualifiedName = packageName.isEmpty() ? builderClassType.name()
: (packageName + "." + builderClassType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
if (record.getSimpleName().toString().equals("Initialized")) {
System.out.println();
}
try (Writer writer = sourceFile.openWriter()) {
javaFile.writeTo(writer);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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("defaultAltName") Optional<String> altName) {
public static final String DEFAULT_NAME = "hey";

public static Optional<String> defaultAltName() {
return Optional.of("alt");
}

public static int defaultAge() {
return 18;
}
}
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.defaultAltName(), initialized.altName());
Assertions.assertEquals(Optional.empty(), initialized.dummy());
}
}

0 comments on commit 74f2e52

Please sign in to comment.