Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enforce mandatory fields through Step Builder #17

Merged
merged 1 commit into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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