Skip to content

Commit

Permalink
feat: enforce mandatory fields through Step Builder
Browse files Browse the repository at this point in the history
This should close: jonas-grgt#9
  • Loading branch information
jonas-grgt committed Apr 21, 2024
1 parent bc9240f commit 6134f81
Show file tree
Hide file tree
Showing 17 changed files with 768 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public enum ConstructorPolicy {
*/
ENFORCED,

ENFORCED_STEPWISE,

/**
* Requires all fields
* to be explicitly set with a concrete value or {@code null} in the
Expand Down
4 changes: 2 additions & 2 deletions processor/src/main/java/io/jonasg/bob/BuildableField.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
/**
* Represents a field that is buildable
*
* @param fieldName the name of the field as declared in the type that will be built
* @param name the name of the field as declared in the type that will be built
* @param isConstructorArgument indicates if the field can be set through the constructor
* @param setterMethodName the name of the setter method to access the field.
* @param type the type of the field
*/
public record BuildableField(
String fieldName,
String name,
boolean isConstructorArgument,
boolean isMandatory,
Optional<String> setterMethodName,
Expand Down
19 changes: 13 additions & 6 deletions processor/src/main/java/io/jonasg/bob/BuilderGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.squareup.javapoet.TypeSpec;
import io.jonasg.bob.definitions.TypeDefinition;

import java.util.List;

public class BuilderGenerator {

private final Filer filer;
Expand All @@ -15,15 +17,20 @@ public BuilderGenerator(Filer filer) {
}

public void generate(TypeDefinition typeDefinition, Buildable buildable, Types typeUtils) {
var abstractTypeSpecFactory = new BuilderTypeSpecFactory(typeDefinition, buildable, typeUtils);
TypeSpec typeSpec = abstractTypeSpecFactory.typeSpec();
String result;
String packageName = getPackageName(typeDefinition, buildable);
var abstractTypeSpecFactory = new BuilderTypeSpecFactory(typeDefinition, buildable, typeUtils, packageName);
List<TypeSpec> typeSpecs = abstractTypeSpecFactory.typeSpecs();
typeSpecs.forEach(t -> TypeWriter.write(filer, packageName, t));
}

private String getPackageName(TypeDefinition typeDefinition, Buildable buildable) {
String packageName;
if (!buildable.packageName().isEmpty()) {
result = buildable.packageName();
packageName = buildable.packageName();
} else {
result = String.format("%s.builder", typeDefinition.packageName());
packageName = String.format("%s.builder", typeDefinition.packageName());
}
TypeWriter.write(filer, result, typeSpec);
return packageName;
}

}
54 changes: 37 additions & 17 deletions processor/src/main/java/io/jonasg/bob/BuilderTypeSpecFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.jonasg.bob.StepBuilderInterfaceTypeSpecFactory.BuilderDetails;
import io.jonasg.bob.definitions.ConstructorDefinition;
import io.jonasg.bob.definitions.FieldDefinition;
import io.jonasg.bob.definitions.GenericParameterDefinition;
Expand All @@ -41,16 +42,24 @@ public class BuilderTypeSpecFactory {

private final Types typeUtils;

private final String packageName;

private String builderTypeName(TypeDefinition source) {
return Formatter.format("$typeName$suffix", source.typeName(), "Builder");
String name = Formatter.format("$typeName$suffix", source.typeName(), "Builder");
if (this.buildable.constructorPolicy().equals(ConstructorPolicy.ENFORCED_STEPWISE)) {
name = "Default" + name;
}
return name;
}

protected BuilderTypeSpecFactory(TypeDefinition typeDefinition, Buildable buildable, Types typeUtils) {
protected BuilderTypeSpecFactory(TypeDefinition typeDefinition, Buildable buildable, Types typeUtils,
String packageName) {
this.typeDefinition = typeDefinition;
this.buildable = buildable;
this.constructorDefinition = extractConstructorDefinitionFrom(typeDefinition);
this.buildableFields = extractBuildableFieldsFrom(typeDefinition);
this.typeUtils = typeUtils;
this.packageName = packageName;
}

private List<BuildableField> extractBuildableFieldsFrom(TypeDefinition typeDefinition) {
Expand Down Expand Up @@ -93,19 +102,30 @@ private ConstructorDefinition extractConstructorDefinitionFrom(TypeDefinition ty
}
}

public TypeSpec typeSpec() {
TypeSpec.Builder builder = TypeSpec.classBuilder(builderTypeName(this.typeDefinition))
public List<TypeSpec> typeSpecs() {
List<TypeSpec> typeSpecs = new ArrayList<>();
String builderName = builderTypeName(this.typeDefinition);
TypeSpec.Builder builder = TypeSpec.classBuilder(builderName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
if (!this.typeDefinition.genericParameters().isEmpty())
if (this.buildable.constructorPolicy().equals(ConstructorPolicy.ENFORCED_STEPWISE)) {
var factory = new StepBuilderInterfaceTypeSpecFactory(this.typeDefinition, this.buildable,
this.buildableFields, this.packageName);
BuilderDetails builderDetails = factory.typeSpec(builderName);
typeSpecs.add(builderDetails.typeSpec());
builderDetails.interfaces().forEach(builder::addSuperinterface);
}
if (!this.typeDefinition.genericParameters().isEmpty()) {
builder.addTypeVariables(toTypeVariableNames(this.typeDefinition));
}
builder.addMethods(generateSetters());
builder.addFields(generateFields());
builder.addMethod(generateBuildMethod());
builder.addMethod(generateConstructor());
if (!this.typeDefinition.genericParameters().isEmpty()) {
builder.addMethod(of());
}
return builder.build();
typeSpecs.add(builder.build());
return typeSpecs;
}

private List<MethodSpec> generateSetters() {
Expand All @@ -117,14 +137,14 @@ private List<MethodSpec> generateSetters() {

protected MethodSpec generateSetterForField(BuildableField field) {

var builder = MethodSpec.methodBuilder(setterName(field.fieldName()))
var builder = MethodSpec.methodBuilder(setterName(field.name()))
.addModifiers(Modifier.PUBLIC)
.returns(builderType())
.addParameter(TypeName.get(field.type()), field.fieldName());
.addParameter(TypeName.get(field.type()), field.name());
if (field.isConstructorArgument() && isAnEnforcedConstructorPolicy() || field.isMandatory()) {
builder.addStatement("this.$L.set($L)", field.fieldName(), field.fieldName());
builder.addStatement("this.$L.set($L)", field.name(), field.name());
} else {
builder.addStatement("this.$L = $L", field.fieldName(), field.fieldName());
builder.addStatement("this.$L = $L", field.name(), field.name());
}
return builder.addStatement("return this")
.build();
Expand Down Expand Up @@ -156,13 +176,13 @@ protected FieldSpec generateField(BuildableField field) {
: "nullableOfNameWithinType";
return FieldSpec
.builder(ParameterizedTypeName.get(ClassName.get(RequiredField.class),
TypeName.get(boxedType(field.type()))), field.fieldName(), Modifier.PRIVATE,
TypeName.get(boxedType(field.type()))), field.name(), Modifier.PRIVATE,
Modifier.FINAL)
.initializer("$T.$L(\"" + field.fieldName() + "\", \""
.initializer("$T.$L(\"" + field.name() + "\", \""
+ this.typeDefinition.typeName() + "\")", RequiredField.class, methodName)
.build();
} else {
return FieldSpec.builder(TypeName.get(field.type()), field.fieldName(), Modifier.PRIVATE)
return FieldSpec.builder(TypeName.get(field.type()), field.name(), Modifier.PRIVATE)
.build();
}
}
Expand Down Expand Up @@ -200,7 +220,7 @@ protected CodeBlock generateTypeInstantiationStatement() {

protected String toConstructorCallingStatement(ConstructorDefinition constructorDefinition) {
return constructorDefinition.parameters().stream()
.map(param -> this.buildableFields.stream().anyMatch(f -> Objects.equals(f.fieldName(), param.name()))
.map(param -> this.buildableFields.stream().anyMatch(f -> Objects.equals(f.name(), param.name()))
? String.format("%s%s", param.name(),
isAnEnforcedConstructorPolicy() ? ".orElseThrow()"
: "")
Expand All @@ -225,12 +245,12 @@ protected CodeBlock generateFieldAssignment(BuildableField field) {
if (field.isConstructorArgument() && isAnEnforcedConstructorPolicy() || field.isMandatory()) {
return CodeBlock.builder()
.addStatement("instance.$L(this.$L.orElseThrow())",
setterName(field.setterMethodName().orElseThrow()), field.fieldName())
setterName(field.setterMethodName().orElseThrow()), field.name())
.build();
} else {
return CodeBlock.builder()
.addStatement("instance.%s(this.%s)".formatted(setterName(field.setterMethodName().orElseThrow()),
field.fieldName()))
field.name()))
.build();
}
}
Expand Down Expand Up @@ -336,7 +356,7 @@ protected String setterName(String name) {
}

private boolean notExcluded(BuildableField field) {
return !Arrays.asList(buildable.excludeFields()).contains(field.fieldName());
return !Arrays.asList(buildable.excludeFields()).contains(field.name());
}

private MethodSpec generateConstructor() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package io.jonasg.bob;

import static io.jonasg.bob.ConstructorPolicy.ENFORCED;
import static io.jonasg.bob.ConstructorPolicy.ENFORCED_STEPWISE;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import javax.lang.model.element.Modifier;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeSpec.Builder;
import io.jonasg.bob.TypeSpecInterfaceBuilder.InterfaceBuilder;
import io.jonasg.bob.definitions.TypeDefinition;

public class StepBuilderInterfaceTypeSpecFactory {

private final Buildable buildable;

private final TypeDefinition typeDefinition;

private final List<BuildableField> buildableFields;

private final String packageName;

public StepBuilderInterfaceTypeSpecFactory(TypeDefinition typeDefinition,
Buildable buildable,
List<BuildableField> buildableFields,
String packageName) {
this.buildable = buildable;
this.typeDefinition = typeDefinition;
this.buildableFields = buildableFields;
this.packageName = packageName;
}

record BuilderDetails(TypeSpec typeSpec, Set<TypeName> interfaces) {

}

BuilderDetails typeSpec(String builderImplName) {
Set<TypeName> interfaces = new HashSet<>();
String builderInterfaceName = String.format("%sBuilder", this.typeDefinition.typeName());
Builder stepBuilderBuilder = TypeSpec.interfaceBuilder(builderInterfaceName)
.addModifiers(Modifier.PUBLIC);
interfaces.add(ClassName.get(this.packageName, builderInterfaceName));

// add static newBuilder method
stepBuilderBuilder.addMethod(MethodSpec.methodBuilder("newBuilder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(ClassName.get(this.packageName, builderInterfaceName))
.addStatement("return new $L()", builderImplName)
.build());

List<BuildableField> reversedBuildableFields = reverseList(this.buildableFields);

// add final BuildStep containing all none mandatory fields
InterfaceBuilder buildStepInterfaceBuilder = TypeSpecInterfaceBuilder.anInterface("BuildStep");
reversedBuildableFields.stream()
.filter(this::notExcluded)
.filter(field -> (!field.isConstructorArgument()) && !field.isMandatory())
.forEach(field -> buildStepInterfaceBuilder.addMethod(MethodSpec.methodBuilder(field.name())
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.returns(ClassName.get("", "BuildStep"))
.addParameter(TypeName.get(field.type()), field.name())
.build()));
// add terminal build method
buildStepInterfaceBuilder.addMethod(MethodSpec.methodBuilder("build")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.returns(ClassName.get(this.typeDefinition.packageName(), this.typeDefinition.typeName()))
.build());
buildStepInterfaceBuilder.build();
TypeSpec buildStep = buildStepInterfaceBuilder.build();
stepBuilderBuilder.addType(buildStep);
interfaces.add(ClassName.get("", builderInterfaceName + "." + "BuildStep"));

// add each mandatory field as a separate interface
// skipping the last element because that should be defined as a method within
// the interface itself
AtomicReference<TypeSpec> nextStep = new AtomicReference<>(buildStep);
List<BuildableField> mandatoryFields = reversedBuildableFields
.stream()
.filter(field -> (field.isConstructorArgument() && isEnforcedConstructorPolicy())
|| field.isMandatory())
.toList();
mandatoryFields
.subList(0, mandatoryFields.size() - 1)
.stream()
.filter(this::notExcluded)
.map(field -> {
String name = String.format("%sStep", capitalize(field.name()));
interfaces.add(ClassName.get("", builderInterfaceName + "." + name));
return TypeSpecInterfaceBuilder.functionalInterface(name)
.methodName(field.name())
.addArgument(TypeName.get(field.type()), field.name())
.returns(ClassName.get("", nextStep.get().name))
.build();
})
.peek(nextStep::set)
.forEach(stepBuilderBuilder::addType);

// the initial field to be built
BuildableField buildableField = mandatoryFields
.get(mandatoryFields.size() - 1);
stepBuilderBuilder.addMethod(MethodSpec.methodBuilder(buildableField.name())
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addParameter(TypeName.get(buildableField.type()), buildableField.name())
.returns(ClassName.get("", nextStep.get().name))
.build());
return new BuilderDetails(stepBuilderBuilder.build(), interfaces);
}

private String capitalize(String value) {
return value.substring(0, 1).toUpperCase() + value.substring(1);
}

private boolean isEnforcedConstructorPolicy() {
return List.of(ENFORCED, ENFORCED_STEPWISE).contains(this.buildable.constructorPolicy());
}

private boolean notExcluded(BuildableField field) {
return !Arrays.asList(buildable.excludeFields()).contains(field.name());
}

private <T> List<T> reverseList(List<T> originalList) {
List<T> reversedList = new ArrayList<>();
for (int i = originalList.size() - 1; i >= 0; i--) {
reversedList.add(originalList.get(i));
}
return reversedList;
}
}
Loading

0 comments on commit 6134f81

Please sign in to comment.