From 8afd92c5a246b8bbffcb4b87ab0c409f43e88ae6 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Sat, 16 Mar 2024 19:20:52 -0700 Subject: [PATCH] Add documentation for toBuilder(). --- Readme.md | 6 ++++ src/main/java/org/jilt/Builder.java | 10 ++++-- .../internal/AbstractBuilderGenerator.java | 31 ++++++++++--------- src/test/java/org/jilt/test/RecordsTest.java | 11 +++++++ .../test/data/record/RecordNoWorkaround.java | 5 +-- .../test/data/tobuilder/ToBuilderValue.java | 8 ++--- 6 files changed, 49 insertions(+), 22 deletions(-) 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 methodAttr; final char fieldAttr; + @Builder(toBuilder = "toBuilder") public ToBuilderValue(int getterAttr, List methodAttr, char fieldAttr) { this.getterAttr = getterAttr; this.methodAttr = methodAttr; @@ -25,11 +25,11 @@ public List methodAttr() { } @Override - public boolean equals(Object obj) { - if (!(obj instanceof ToBuilderValue)) { + public boolean equals(Object object) { + if (!(object instanceof ToBuilderValue)) { return false; } - ToBuilderValue that = (ToBuilderValue) obj; + ToBuilderValue that = (ToBuilderValue) object; return this.getterAttr == that.getterAttr && this.methodAttr.equals(that.methodAttr) && this.fieldAttr == that.fieldAttr;