Skip to content

Commit

Permalink
feat: add fields to builder that are only mutable through setters
Browse files Browse the repository at this point in the history
This commit will close issue #2
  • Loading branch information
jonas-grgt committed Apr 3, 2024
1 parent 1e5a818 commit 8a7ec64
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 79 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ The location of the builder can be changed:
@Buildable(packageName = "my.other.garage")
public class Car {
```

### Pickup setter methods as buildable

When Bob encounters setters (with or without the set prefix)
and a corresponding field it will add the fields to the final builder.

In the below example,
if though `color` is not part of the constructor it will be part of the final generated Builder
because there is a setter available, which will be used.

```java
@Buildable
public class Car {
private Brand brand;
private String color;

public Car(Brand brand) {
this.brand = brand;
}

public void color(String color) {
this.color = color;
}
}
```

### Field exclusion

Expand Down
23 changes: 23 additions & 0 deletions processor/src/main/java/io/jonasg/bob/BuildableField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.jonasg.bob;

import java.util.Optional;

import javax.lang.model.type.TypeMirror;

/**
* Represents a field that is buildable
* @param fieldName 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,
boolean isConstructorArgument,
Optional<String> setterMethodName,
TypeMirror type) {

public static BuildableField fromConstructor(String fieldName, TypeMirror type) {
return new BuildableField(fieldName, true, Optional.empty(), type);
}
}
151 changes: 99 additions & 52 deletions processor/src/main/java/io/jonasg/bob/TypeSpecFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeMirror;
Expand All @@ -13,6 +16,7 @@
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.MethodSpec.Builder;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
Expand All @@ -32,7 +36,7 @@ public class TypeSpecFactory {

private final Buildable buildable;

private final List<ParameterDefinition> eligibleConstructorParams;
private final List<BuildableField> buildableFields;

private String builderTypeName(TypeDefinition source) {
return Formatter.format("$typeName$suffix", source.typeName(), "Builder");
Expand All @@ -41,27 +45,43 @@ private String builderTypeName(TypeDefinition source) {
private TypeSpecFactory(TypeDefinition typeDefinition, Buildable buildable) {
this.typeDefinition = typeDefinition;
this.buildable = buildable;
this.constructorDefinition = extractConstructorDefinitionFrom(typeDefinition);
this.buildableFields = extractBuildableFieldsFrom(typeDefinition);
}

private List<BuildableField> extractBuildableFieldsFrom(TypeDefinition typeDefinition) {
var fieldNames = typeDefinition.fields().stream()
.map(FieldDefinition::name)
.toList();
List<ParameterDefinition> eligibleConstructorParams = this.constructorDefinition.parameters()
.stream()
.filter(p -> fieldNames.contains(p.name()))
.toList();
Stream<BuildableField> constructorBuildableFields = this.constructorDefinition.parameters()
.stream()
.filter(p -> fieldNames.contains(p.name()))
.map(p -> BuildableField.fromConstructor(p.name(), p.type()));
Stream<BuildableField> setterBuildableFields = this.typeDefinition.getSetterMethods()
.stream()
.filter(field -> !eligibleConstructorParams
.contains(new ParameterDefinition(field.type(), field.fieldName())))
.map(p -> new BuildableField(p.fieldName(), false, Optional.of(p.methodName()), p.type()));
return Stream.concat(constructorBuildableFields, setterBuildableFields).toList();
}

private ConstructorDefinition extractConstructorDefinitionFrom(TypeDefinition typeDefinition) {
var buildableConstructors = typeDefinition.constructors().stream()
.filter(c -> c.isAnnotatedWith(BuildableConstructor.class))
.toList();
if (buildableConstructors.size() > 1) {
throw new IllegalArgumentException("Only one constructor can be annotated with @BuildableConstructor");
}
if (buildableConstructors.isEmpty()) {
this.constructorDefinition = typeDefinition.constructors().stream()
return typeDefinition.constructors().stream()
.max(Comparator.comparingInt(c -> c.parameters().size())).orElseThrow();
} else {
this.constructorDefinition = buildableConstructors.get(0);
return buildableConstructors.get(0);
}

var fieldNames = typeDefinition.fields().stream()
.map(FieldDefinition::name)
.toList();
this.eligibleConstructorParams = this.constructorDefinition.parameters()
.stream()
.filter(p -> fieldNames.contains(p.name()))
.toList();
}

public static TypeSpec produce(TypeDefinition typeDefinition, Buildable buildable) {
Expand All @@ -73,53 +93,79 @@ private TypeSpec typeSpec() {
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
if (!this.typeDefinition.genericParameters().isEmpty())
builder.addTypeVariables(toTypeVariableNames(this.typeDefinition));
builder.addMethods(setters());
builder.addFields(fields());
builder.addMethod(buildMethod());
builder.addMethod(constructor());
if (!this.typeDefinition.genericParameters().isEmpty())
builder.addMethods(generateSetters());
builder.addFields(generateFields());
builder.addMethod(genereateBuildMethod());
builder.addMethod(generateConstructor());
if (!this.typeDefinition.genericParameters().isEmpty()) {
builder.addMethod(of());
}
return builder.build();
}

private List<MethodSpec> setters() {
List<MethodSpec> setters = new ArrayList<>();
for (ParameterDefinition field : this.eligibleConstructorParams) {
if (notExcluded(field)) {
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName(field.name()))
private List<MethodSpec> generateSetters() {
return this.buildableFields.stream()
.filter(this::notExcluded)
.map(field -> MethodSpec.methodBuilder(setterName(field.fieldName()))
.addModifiers(Modifier.PUBLIC)
.returns(builderType())
.addParameter(TypeName.get(field.type()), field.name());
setter
.addStatement("this.$L = $L", field.name(), field.name())
.build();
setters.add(setter
.addParameter(TypeName.get(field.type()), field.fieldName())
.addStatement("this.$L = $L", field.fieldName(), field.fieldName())
.addStatement("return this")
.build());
}
}
return setters;
.build())
.toList();
}

private List<FieldSpec> fields() {
return this.eligibleConstructorParams.stream()
.map(field -> FieldSpec.builder(TypeName.get(field.type()), field.name(), Modifier.PRIVATE)
private List<FieldSpec> generateFields() {
return buildableFields.stream()
.map(field -> FieldSpec.builder(TypeName.get(field.type()), field.fieldName(), Modifier.PRIVATE)
.build())
.toList();
}

private MethodSpec buildMethod() {
MethodSpec.Builder builder = MethodSpec.methodBuilder("build")
private MethodSpec genereateBuildMethod() {
Builder builder = MethodSpec.methodBuilder("build")
.addModifiers(Modifier.PUBLIC)
.returns(className(this.typeDefinition));
builder.addStatement("return new $T($L)", className(this.typeDefinition),
this.constructorDefinition.parameters().stream()
.map(param -> this.eligibleConstructorParams.contains(param) ? param.name()
: defaultForType(param.type()))
.collect(Collectors.joining(", ")));
if (typeDefinition.containsSetterMethods()) {
createConstructorAndSetterAwareBuildMethod(builder);
} else {
createConstructorOnlyBuildMethod(builder);
}
return builder.build();
}

private void createConstructorAndSetterAwareBuildMethod(Builder builder) {
builder.addStatement("var instance = new $T($L)", className(this.typeDefinition),
this.toConstructorCallingStatement(this.constructorDefinition));
typeDefinition.getSetterMethods()
.forEach(method -> {
String fieldName;
if (method.methodName().startsWith("set")) {
fieldName = method.methodName().substring(3, 4).toLowerCase()
+ method.methodName().substring(4);
builder.addStatement("instance.%s(this.%s)".formatted(method.methodName(), fieldName));
} else {
fieldName = method.methodName();
builder.addStatement("instance.%s(this.%s)".formatted(setterName(fieldName), fieldName));
}
});
builder.addStatement("return instance");
}

private void createConstructorOnlyBuildMethod(Builder builder) {
builder.addStatement("return new $T($L)", className(this.typeDefinition),
toConstructorCallingStatement(this.constructorDefinition));
}

private String toConstructorCallingStatement(ConstructorDefinition constructorDefinition) {
return constructorDefinition.parameters().stream()
.map(param -> this.buildableFields.stream().anyMatch(f -> Objects.equals(f.fieldName(), param.name()))
? param.name()
: defaultForType(param.type()))
.collect(Collectors.joining(", "));
}

private String defaultForType(TypeMirror type) {
return switch (type.toString()) {
case "int" -> "0";
Expand All @@ -134,15 +180,16 @@ private String defaultForType(TypeMirror type) {
private MethodSpec of() {
CodeBlock.Builder body = CodeBlock.builder();
body.addStatement("return new $L<>()", builderTypeName(this.typeDefinition));
MethodSpec.Builder of = MethodSpec.methodBuilder("of")
Builder builder = MethodSpec.methodBuilder("of")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(builderTypeGenerics())
.returns(builderType());
for (ParameterDefinition parameter : this.typeDefinition.genericParameters())
of.addParameter(ParameterizedTypeName.get(ClassName.get("java.lang", "Class"),
for (ParameterDefinition parameter : this.typeDefinition.genericParameters()) {
builder.addParameter(ParameterizedTypeName.get(ClassName.get("java.lang", "Class"),
TypeVariableName.get(parameter.name())), String.format("%stype", parameter.name()));
of.addCode(body.build());
return of.build();
}
builder.addCode(body.build());
return builder.build();
}

private TypeName builderType() {
Expand All @@ -161,7 +208,7 @@ private TypeName builderType() {
else
result = String.format("%s.builder", this.typeDefinition.packageName());
return ParameterizedTypeName.get(ClassName.get(result, builderTypeName(this.typeDefinition)),
typeVariableNames.toArray(new TypeName[typeVariableNames.size()]));
typeVariableNames.toArray(new TypeName[0]));
}

private List<TypeVariableName> builderTypeGenerics() {
Expand All @@ -171,7 +218,7 @@ private List<TypeVariableName> builderTypeGenerics() {
for (SimpleTypeDefinition definition : param.bounds()) {
bounds.add(TypeVariableName.get(definition.typeName()));
}
typeVariableNames.add(TypeVariableName.get(param.name(), bounds.toArray(new TypeName[bounds.size()])));
typeVariableNames.add(TypeVariableName.get(param.name(), bounds.toArray(new TypeName[0])));
}
return typeVariableNames;
}
Expand All @@ -186,7 +233,7 @@ private TypeName className(TypeDefinition definition) {
} else {
List<TypeVariableName> genericParameters = toTypeVariableNames(definition);
return ParameterizedTypeName.get(ClassName.get(definition.packageName(), definition.fullTypeName()),
genericParameters.toArray(new TypeName[genericParameters.size()]));
genericParameters.toArray(new TypeName[0]));
}
}

Expand All @@ -206,19 +253,19 @@ private List<TypeName> simpleClassNames(List<SimpleTypeDefinition> definitions)
return typeNames;
}

private String fieldName(String name) {
private String setterName(String name) {
if (buildable.setterPrefix().isEmpty()) {
return name;
}
return Formatter.format("$setterPrefix$name", buildable.setterPrefix(),
name.substring(0, 1).toUpperCase() + name.substring(1));
}

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

private MethodSpec constructor() {
private MethodSpec generateConstructor() {
return MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,5 @@

import javax.lang.model.type.TypeMirror;

public class FieldDefinition {

private final String name;

private final TypeMirror type;

public FieldDefinition(String name, TypeMirror type) {
this.name = name;
this.type = type;
}

public String name() {
return name;
}

public TypeMirror type() {
return type;
}
public record FieldDefinition(String name, TypeMirror type) {
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package io.jonasg.bob.definitions;

public class MethodDefinition {
private final String name;
import java.util.List;

public MethodDefinition(String name) {
this.name = name;
}
import javax.lang.model.type.TypeMirror;

public String name() {
return name;
}
public record MethodDefinition(String name,
List<TypeMirror> parameters) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.jonasg.bob.definitions;

import javax.lang.model.type.TypeMirror;

public record SetterMethodDefinition(String methodName,
String fieldName,
TypeMirror type) {
}
Loading

0 comments on commit 8a7ec64

Please sign in to comment.