diff --git a/Readme.md b/Readme.md index f3f2631..1570aff 100644 --- a/Readme.md +++ b/Readme.md @@ -408,6 +408,12 @@ practically all aspects of the generated Builder (all of them are optional): name of the built class (for example, `person` when building a `Person` class). * `buildMethod` allows you to change the name of the final method invoked on the Builder to obtain an instance of the built class. The default name of that method is `build`. +* `toBuilder` allows you to set the name of the static method in the Builder class that creates a new instance of it, + initialized with values from the provided instance of the built class. + This is useful for easily creating copies of the built class with only a few properties changed, + while still keeping the original class immutable. + The default value of this attribute is the empty string, + which means this method will not be generated. ##### @BuilderInterfaces annotation diff --git a/src/main/java/org/jilt/Builder.java b/src/main/java/org/jilt/Builder.java index f331c76..1c4748d 100644 --- a/src/main/java/org/jilt/Builder.java +++ b/src/main/java/org/jilt/Builder.java @@ -310,9 +310,15 @@ String buildMethod() default ""; /** - * ToDo add a description here. But also change it to a String + * Allows generating a static method in the Builder class that creates a new instance of it, + * initialized with values from a provided instance of the built class. + * This is useful for easily creating copies of the built class with only a few properties changed, + * while still keeping the original class immutable. + *
+ * The value of this attribute is the name to use for the generated method.
+ * The default is the empty string, which means no method will be generated.
*/
- boolean toBuilder() default false;
+ String toBuilder() default "";
/**
* Annotation that ignores the given field of a class when generating a Builder for that class.
diff --git a/src/main/java/org/jilt/internal/AbstractBuilderGenerator.java b/src/main/java/org/jilt/internal/AbstractBuilderGenerator.java
index a135303..946b6d8 100644
--- a/src/main/java/org/jilt/internal/AbstractBuilderGenerator.java
+++ b/src/main/java/org/jilt/internal/AbstractBuilderGenerator.java
@@ -128,12 +128,15 @@ public final void generateBuilderClass() throws Exception {
}
private void addToBuilderMethod(TypeSpec.Builder builderClassBuilder) {
- if (!this.builderAnnotation.toBuilder()) {
+ // if the @Builder annotation has an empty toBuilder attribute,
+ // don't generate this method
+ if (this.builderAnnotation.toBuilder().isEmpty()) {
return;
}
+
String targetClassParam = Utils.deCapitalize(this.targetClassSimpleName().toString());
MethodSpec.Builder toBuilderMethod = MethodSpec
- .methodBuilder("toBuilder")
+ .methodBuilder(this.builderAnnotation.toBuilder())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(this.builderClassTypeParameters())
.returns(this.builderClassTypeName())
@@ -141,35 +144,35 @@ private void addToBuilderMethod(TypeSpec.Builder builderClassBuilder) {
.builder(this.targetClassTypeName(), targetClassParam)
.build());
- CodeBlock.Builder buildStatement = CodeBlock.builder();
- buildStatement.add("return new $T()", this.builderClassTypeName());
- // iterate through all attributes, and add them to the build statement
+ CodeBlock.Builder methodBody = CodeBlock.builder();
+ String returnVarName = Utils.deCapitalize(this.builderClassClassName.simpleName());
+ methodBody.addStatement("$T $L = new $T()", this.builderClassTypeName(),
+ returnVarName, this.builderClassTypeName());
+ // iterate through all attributes,
+ // and add a setter statement to the method body for each
for (VariableElement attribute : attributes) {
String attributeAccess = this.accessAttributeOfTargetClass(attribute);
- buildStatement.add("\n");
- buildStatement.indent();
- buildStatement.add(".$L($L.$L)",
+ methodBody.addStatement("$L.$L($L.$L)",
+ returnVarName,
this.setterMethodName(attribute),
targetClassParam, attributeAccess);
- buildStatement.unindent();
}
- buildStatement.add(";\n");
+ methodBody.addStatement("return $L", returnVarName);
builderClassBuilder.addMethod(toBuilderMethod
- .addCode(buildStatement.build())
+ .addCode(methodBody.build())
.build());
}
private String accessAttributeOfTargetClass(VariableElement attribute) {
String fieldName = this.attributeSimpleName(attribute);
- String getterName = "get" + Utils.capitalize(fieldName);
for (Element member : this.elements.getAllMembers(this.targetClassType)) {
// if there's a getter method, use it
- if (elementIsMethodWithoutArgumentsCalled(member, getterName)) {
+ if (elementIsMethodWithoutArgumentsCalled(member, "get" + Utils.capitalize(fieldName))) {
return member.getSimpleName().toString() + "()";
}
// if there's a no-argument method with the field name,
- // like done in Records, use that
+ // like with Records, use that
if (elementIsMethodWithoutArgumentsCalled(member, fieldName)) {
return member.getSimpleName().toString() + "()";
}
diff --git a/src/test/java/org/jilt/test/RecordsTest.java b/src/test/java/org/jilt/test/RecordsTest.java
index bcc23c5..0040745 100644
--- a/src/test/java/org/jilt/test/RecordsTest.java
+++ b/src/test/java/org/jilt/test/RecordsTest.java
@@ -29,4 +29,15 @@ public void builder_for_record_without_workaround_works() {
assertThat(record.name()).isNull();
assertThat(record.age()).isEqualTo(-1);
}
+
+ @Test
+ public void to_builder_works_for_records() {
+ RecordNoWorkaround original = new RecordNoWorkaround("Adam", 23);
+ RecordNoWorkaround copy = RecordNoWorkaroundBuilder.toBuilder(original)
+ .age(-1)
+ .build();
+
+ assertThat(copy.name()).isEqualTo(original.name());
+ assertThat(copy.age()).isEqualTo(-1);
+ }
}
diff --git a/src/test/java/org/jilt/test/data/record/RecordNoWorkaround.java b/src/test/java/org/jilt/test/data/record/RecordNoWorkaround.java
index e3a8439..ad800fb 100644
--- a/src/test/java/org/jilt/test/data/record/RecordNoWorkaround.java
+++ b/src/test/java/org/jilt/test/data/record/RecordNoWorkaround.java
@@ -4,5 +4,6 @@
import org.jilt.BuilderStyle;
import org.jilt.Opt;
-@Builder(style = BuilderStyle.STAGED)
-public record RecordNoWorkaround(@Opt String name, int age) {}
+@Builder(style = BuilderStyle.STAGED, toBuilder = "toBuilder")
+public record RecordNoWorkaround(@Opt String name, int age) {
+}
diff --git a/src/test/java/org/jilt/test/data/tobuilder/ToBuilderValue.java b/src/test/java/org/jilt/test/data/tobuilder/ToBuilderValue.java
index d4e79f0..fbcd14f 100644
--- a/src/test/java/org/jilt/test/data/tobuilder/ToBuilderValue.java
+++ b/src/test/java/org/jilt/test/data/tobuilder/ToBuilderValue.java
@@ -4,12 +4,12 @@
import java.util.List;
-@Builder(toBuilder = true)
public class ToBuilderValue {
private final int getterAttr;
private final List