From 67b72e4ea27bfb21bc19fbb072bd8d8d658dbe54 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 21 Dec 2025 01:09:02 +0100 Subject: [PATCH 1/3] Initial implementation of supporting a toString method on Builders --- core/pom.xml | 8 +++ .../core/util/BuilderToStringStyle.java | 56 +++++++++++++++++++ .../builders/example/BookDtoBuilder.java | 32 +++++++++++ .../example/MannschaftDtoBuilder.java | 15 +++++ .../builders/example/PersonDtoBuilder.java | 18 ++++++ .../example/ProductRecordBuilder.java | 16 ++++++ .../builders/example/SponsorDtoBuilder.java | 14 +++++ .../processor/util/JavaCodeGenerator.java | 41 ++++++++++++++ .../BuilderConfigurationReaderTest.java | 15 +++++ 9 files changed, 215 insertions(+) create mode 100644 core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java diff --git a/core/pom.xml b/core/pom.xml index eb125219..7a5c8e7e 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -66,6 +66,7 @@ 6.0.1 + 3.20.0 0.9.0 @@ -80,6 +81,13 @@ + + + org.apache.commons + commons-lang3 + ${apache-commons-lang3.version} + + org.junit.jupiter diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java b/core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java new file mode 100644 index 00000000..a9f421af --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.core.util; + +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * Custom ToStringStyle for builders that automatically handles TrackedValue fields. This style only + * includes fields that have been set (TrackedValue.isSet() returns true) and unwraps their values + * automatically. + */ +public class BuilderToStringStyle extends ToStringStyle { + + private static final long serialVersionUID = 1L; + + public static final BuilderToStringStyle INSTANCE = new BuilderToStringStyle(); + + public BuilderToStringStyle() { + super(); + this.setUseClassName(true); + this.setUseIdentityHashCode(false); + } + + @Override + public void append(StringBuffer buffer, String fieldName, Object value, Boolean fullDetail) { + if (value instanceof TrackedValue trackedValue) { + if (trackedValue.isSet()) { + super.append(buffer, fieldName, trackedValue.value(), fullDetail); + } + } else { + super.append(buffer, fieldName, value, fullDetail); + } + } +} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index a0b0cd9e..f9aee825 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -11,8 +11,10 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** @@ -440,4 +442,34 @@ public BookDto build() { public static BookDtoBuilder create() { return new BookDtoBuilder(); } + + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("title", this.title) + .append("author", this.author) + .append("isbn", this.isbn) + .append("pages", this.pages) + .append("price", this.price) + .append("exactPrice", this.exactPrice) + .append("available", this.available) + .append("rating", this.rating) + .append("edition", this.edition) + .append("salesCount", this.salesCount) + .append("discount", this.discount) + .append("category", this.category) + .append("publishDate", this.publishDate) + .append("lastUpdated", this.lastUpdated) + .append("subtitle", this.subtitle) + .append("tags", this.tags) + .append("genres", this.genres) + .append("metadata", this.metadata) + .append("publisher", this.publisher) + .toString(); + } } diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 96783a5d..5f344cce 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -9,9 +9,11 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.processing.Generated; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.builders.HashSetBuilderWithElementBuilders; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** @@ -189,6 +191,19 @@ public MannschaftDtoBuilder conditional(BooleanSupplier condition, return conditional(condition, yesCondition, null); } + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("name", this.name) + .append("sponsoren", this.sponsoren) + .toString(); + } + /** * Interface that can be implemented by the DTO to provide fluent modification methods. */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 7aa00344..b1d8f8d2 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -10,9 +10,11 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.processing.Generated; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** @@ -311,6 +313,22 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, return conditional(condition, yesCondition, null); } + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("name", this.name) + .append("nickNames", this.nickNames) + .append("nickNames2", this.nickNames2) + .append("birthdate", this.birthdate) + .append("mannschaft", this.mannschaft) + .toString(); + } + /** * Interface that can be implemented by the DTO to provide fluent modification methods. */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index 3a6ab3c8..eb6fdee0 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -8,8 +8,10 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.processing.Generated; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** @@ -222,6 +224,20 @@ public ProductRecordBuilder conditional(BooleanSupplier condition, return conditional(condition, yesCondition, null); } + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("name", this.name) + .append("price", this.price) + .append("category", this.category) + .toString(); + } + /** * Interface that can be implemented by the DTO to provide fluent modification methods. */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index adf74258..5bbfaed8 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -8,8 +8,10 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.processing.Generated; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** @@ -133,6 +135,18 @@ public SponsorDtoBuilder conditional(BooleanSupplier condition, return conditional(condition, yesCondition, null); } + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("name", this.name) + .toString(); + } + /** * Interface that can be implemented by the DTO to provide fluent modification methods. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 4e60003a..e4c2b58a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -191,6 +191,11 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep createMethodConditionalPositiveOnly(builderTypeName, methodAccessModifier)); } + // Add toString method + classBuilder.addMethod( + createMethodToString( + builderDef.getConstructorFieldsForBuilder(), builderDef.getSetterFieldsForBuilder())); + // Adding nested types (e.g., With interface) for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { TypeSpec nestedTypeSpec = createNestedType(nestedType); @@ -538,6 +543,42 @@ private MethodSpec createMethodConditional( return mb.build(); } + private MethodSpec createMethodToString( + List constructorFields, List setterFields) { + MethodSpec.Builder mb = + MethodSpec.methodBuilder("toString") + .addModifiers(PUBLIC) + .addAnnotation(Override.class) + .returns(String.class) + .addJavadoc( + """ + Returns a string representation of this builder, including only fields that have been set. + + @return string representation of the builder + """); + + // Combine all fields + List allFields = new java.util.ArrayList<>(); + allFields.addAll(constructorFields); + allFields.addAll(setterFields); + + // Build fluent chain of append calls using CodeBlock.Builder + CodeBlock.Builder codeBuilder = CodeBlock.builder(); + codeBuilder.add( + "return new $T(this, $T.INSTANCE)", + ClassName.get("org.apache.commons.lang3.builder", "ToStringBuilder"), + ClassName.get("org.javahelpers.simple.builders.core.util", "BuilderToStringStyle")); + + for (FieldDto field : allFields) { + codeBuilder.add("\n .append($S, this.$N)", field.getFieldName(), field.getFieldName()); + } + + codeBuilder.add("\n .toString()"); + mb.addStatement(codeBuilder.build()); + + return mb.build(); + } + private MethodSpec createMethodConditionalPositiveOnly( com.palantir.javapoet.TypeName builderType, Modifier methodAccessModifier) { MethodSpec.Builder mb = MethodSpec.methodBuilder("conditional"); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index 53120c6a..ff164509 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -461,7 +461,9 @@ public class PersonDto { import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; import java.util.List; + import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; + import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** @@ -532,6 +534,19 @@ public PersonDto build() { public static PersonDtoMinimalBuilder create() { return new PersonDtoMinimalBuilder(); } + + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("name", this.name) + .append("tags", this.tags) + .toString(); + } } """; From 0a0bdbbcb886b85b2cf247641f82364d3d9f1165 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 21 Dec 2025 01:15:26 +0100 Subject: [PATCH 2/3] Adding explicit tests for behavior --- .../example/ToStringBehaviorTest.java | 147 +++++++++++ .../processor/ToStringGenerationTest.java | 240 ++++++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java new file mode 100644 index 00000000..05ea1860 --- /dev/null +++ b/example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java @@ -0,0 +1,147 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.example; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Runtime tests for toString behavior in generated builders. + * + *

Verifies that: + * + *

    + *
  • Only set fields appear in toString output + *
  • Unset fields are hidden from toString output + *
  • BuilderToStringStyle correctly filters TrackedValue fields + *
+ */ +class ToStringBehaviorTest { + + @Test + void toString_onlyShowsSetFields() { + BookDtoBuilder builder = BookDtoBuilder.create().title("The Great Book").author("John Doe"); + + String result = builder.toString(); + + assertTrue(result.contains("title=The Great Book"), "Should show set title field: " + result); + assertTrue(result.contains("author=John Doe"), "Should show set author field: " + result); + assertFalse(result.contains("isbn"), "Should NOT show unset isbn field: " + result); + assertFalse(result.contains("pages"), "Should NOT show unset pages field: " + result); + assertFalse(result.contains("price"), "Should NOT show unset price field: " + result); + } + + @Test + void toString_emptyBuilderShowsNoFields() { + BookDtoBuilder builder = BookDtoBuilder.create(); + + String result = builder.toString(); + + assertTrue(result.contains("BookDtoBuilder"), "Should contain builder class name: " + result); + assertFalse(result.contains("title="), "Should NOT show unset title field: " + result); + assertFalse(result.contains("author="), "Should NOT show unset author field: " + result); + assertFalse(result.contains("isbn="), "Should NOT show unset isbn field: " + result); + } + + @Test + void toString_partiallySetFieldsOnlyShowsSet() { + BookDtoBuilder builder = + BookDtoBuilder.create().title("Partial Book").pages(250); + + String result = builder.toString(); + + assertTrue(result.contains("title=Partial Book"), "Should show set title field: " + result); + assertTrue(result.contains("pages=250"), "Should show set pages field: " + result); + assertFalse(result.contains("author="), "Should NOT show unset author field: " + result); + assertFalse(result.contains("isbn"), "Should NOT show unset isbn field: " + result); + } + + @Test + void toString_allFieldsSetShowsAll() { + BookDtoBuilder builder = + BookDtoBuilder.create() + .title("Complete Book") + .author("Jane Smith") + .isbn("978-1234567890") + .pages(350); + + String result = builder.toString(); + + assertTrue(result.contains("title=Complete Book"), "Should show title: " + result); + assertTrue(result.contains("author=Jane Smith"), "Should show author: " + result); + assertTrue(result.contains("isbn=978-1234567890"), "Should show isbn: " + result); + assertTrue(result.contains("pages=350"), "Should show pages: " + result); + } + + @Test + void toString_builderFromInstanceShowsAllFields() { + BookDto book = new BookDto(); + book.setTitle("Instance Book"); + book.setAuthor("Bob Johnson"); + book.setIsbn("978-9876543210"); + book.setPages(400); + + BookDtoBuilder builder = new BookDtoBuilder(book); + + String result = builder.toString(); + + assertTrue(result.contains("title=Instance Book"), "Should show title from instance: " + result); + assertTrue(result.contains("author=Bob Johnson"), "Should show author from instance: " + result); + assertTrue(result.contains("isbn=978-9876543210"), "Should show isbn from instance: " + result); + assertTrue(result.contains("pages=400"), "Should show pages from instance: " + result); + } + + @Test + void toString_modifiedBuilderShowsUpdatedValues() { + BookDtoBuilder builder = + BookDtoBuilder.create() + .title("Original Title") + .author("Original Author") + .pages(200); + + builder.title("Updated Title"); + + String result = builder.toString(); + + assertTrue(result.contains("title=Updated Title"), "Should show updated title: " + result); + assertFalse(result.contains("Original Title"), "Should NOT show old title: " + result); + assertTrue(result.contains("author=Original Author"), "Should still show author: " + result); + assertTrue(result.contains("pages=200"), "Should still show pages: " + result); + } + + @Test + void toString_nullValueIsShown() { + BookDtoBuilder builder = BookDtoBuilder.create().title("Test Book").author(null); + + String result = builder.toString(); + + assertTrue(result.contains("title=Test Book"), "Should show title: " + result); + assertTrue( + result.contains("author=") || result.contains("author=null"), + "Should show null value for set field: " + result); + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java new file mode 100644 index 00000000..457448e3 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java @@ -0,0 +1,240 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.testing.compile.Compilation; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests for toString method generation in builders. + * + *

Verifies that: + * + *

    + *
  • toString method is generated with fluent ToStringBuilder API + *
  • Custom BuilderToStringStyle is used + *
  • All fields are included in the toString output + *
  • The generated code compiles successfully + *
+ */ +class ToStringGenerationTest { + + @Test + void toString_generatedWithFluentAPI() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class PersonDto { + private String name; + private int age; + private String email; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + assertThat(compilation).succeeded(); + + String generatedCode = loadGeneratedSource(compilation, "PersonDtoBuilder"); + + // Verify toString method exists + assertTrue( + generatedCode.contains("public String toString()"), "toString method should be generated"); + + // Verify it uses BuilderToStringStyle + assertTrue( + generatedCode.contains("BuilderToStringStyle.INSTANCE"), + "Should use BuilderToStringStyle.INSTANCE"); + + // Verify it uses ToStringBuilder + assertTrue( + generatedCode.contains("new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)"), + "Should create ToStringBuilder with custom style"); + + // Verify fluent API with all fields + assertTrue(generatedCode.contains(".append(\"name\", this.name)"), "Should append name field"); + assertTrue(generatedCode.contains(".append(\"age\", this.age)"), "Should append age field"); + assertTrue( + generatedCode.contains(".append(\"email\", this.email)"), "Should append email field"); + + // Verify method ends with .toString() + assertTrue(generatedCode.contains(".toString();"), "Should end with .toString() call"); + + // Verify imports + assertTrue( + generatedCode.contains("import org.apache.commons.lang3.builder.ToStringBuilder;"), + "Should import ToStringBuilder"); + assertTrue( + generatedCode.contains( + "import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;"), + "Should import BuilderToStringStyle"); + } + + @Test + void toString_includesConstructorAndSetterFields() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class BookDto { + private String title; + private String author; + private int pages; + + public BookDto(String title, String author) { + this.title = title; + this.author = author; + } + + public String getTitle() { return title; } + public String getAuthor() { return author; } + public int getPages() { return pages; } + public void setPages(int pages) { this.pages = pages; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + assertThat(compilation).succeeded(); + + String generatedCode = loadGeneratedSource(compilation, "BookDtoBuilder"); + + // Verify constructor fields are included + assertTrue( + generatedCode.contains(".append(\"title\", this.title)"), + "Should append constructor field: title"); + assertTrue( + generatedCode.contains(".append(\"author\", this.author)"), + "Should append constructor field: author"); + + // Verify setter fields are included + assertTrue( + generatedCode.contains(".append(\"pages\", this.pages)"), + "Should append setter field: pages"); + } + + @Test + void toString_generatedCodeStructure() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class ProductDto { + private String name; + private double price; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public double getPrice() { return price; } + public void setPrice(double price) { this.price = price; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + assertThat(compilation).succeeded(); + + String generatedCode = loadGeneratedSource(compilation, "ProductDtoBuilder"); + + // Verify the complete structure matches expected pattern + String expectedPattern = + """ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) + .append("name", this.name) + .append("price", this.price) + .toString(); + } + """; + + // Normalize whitespace for comparison + String normalizedGenerated = generatedCode.replaceAll("\\s+", " ").trim(); + String normalizedExpected = expectedPattern.replaceAll("\\s+", " ").trim(); + + assertTrue( + normalizedGenerated.contains(normalizedExpected), + "Generated toString method should match expected structure.\nExpected pattern: " + + normalizedExpected + + "\nGenerated code: " + + normalizedGenerated); + } + + @Test + void toString_hasCorrectJavadoc() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class SimpleDto { + private String value; + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + assertThat(compilation).succeeded(); + + String generatedCode = loadGeneratedSource(compilation, "SimpleDtoBuilder"); + + // Verify javadoc is present + assertTrue( + generatedCode.contains( + "Returns a string representation of this builder, including only fields that have been" + + " set."), + "Should have descriptive javadoc"); + assertTrue( + generatedCode.contains("@return string representation of the builder"), + "Should have @return tag in javadoc"); + } +} From a1f369befc26dd6df118d8e8a0b9080328956b88 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 21 Dec 2025 01:20:39 +0100 Subject: [PATCH 3/3] Adding test for BuilderToStringStyle --- .../core/util/BuilderToStringStyleTest.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java new file mode 100644 index 00000000..95bd72ce --- /dev/null +++ b/core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java @@ -0,0 +1,232 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.core.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link BuilderToStringStyle}. + * + *

Verifies that: + * + *

    + *
  • TrackedValue fields that are set are included in toString output + *
  • TrackedValue fields that are unset are excluded from toString output + *
  • Regular (non-TrackedValue) fields are always included + *
  • The style correctly unwraps TrackedValue.value() + *
+ */ +class BuilderToStringStyleTest { + + @Test + void instance_isNotNull() { + assertNotNull(BuilderToStringStyle.INSTANCE, "INSTANCE should not be null"); + } + + @Test + void toString_includesSetTrackedValue() { + TestObject obj = new TestObject(); + obj.setField = TrackedValue.changedValue("SetValue"); + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("setField", obj.setField) + .toString(); + + assertTrue( + result.contains("setField=SetValue"), "Should include set TrackedValue field: " + result); + } + + @Test + void toString_excludesUnsetTrackedValue() { + TestObject obj = new TestObject(); + obj.unsetField = TrackedValue.unsetValue(); + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("unsetField", obj.unsetField) + .toString(); + + assertFalse( + result.contains("unsetField"), "Should NOT include unset TrackedValue field: " + result); + } + + @Test + void toString_includesInitialTrackedValue() { + TestObject obj = new TestObject(); + obj.initialField = TrackedValue.initialValue("InitialValue"); + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("initialField", obj.initialField) + .toString(); + + assertTrue( + result.contains("initialField=InitialValue"), + "Should include initial TrackedValue field: " + result); + } + + @Test + void toString_includesRegularField() { + TestObject obj = new TestObject(); + obj.regularField = "RegularValue"; + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("regularField", obj.regularField) + .toString(); + + assertTrue( + result.contains("regularField=RegularValue"), "Should include regular field: " + result); + } + + @Test + void toString_mixedFieldsShowCorrectly() { + TestObject obj = new TestObject(); + obj.setField = TrackedValue.changedValue("SetValue"); + obj.unsetField = TrackedValue.unsetValue(); + obj.initialField = TrackedValue.initialValue("InitialValue"); + obj.regularField = "RegularValue"; + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("setField", obj.setField) + .append("unsetField", obj.unsetField) + .append("initialField", obj.initialField) + .append("regularField", obj.regularField) + .toString(); + + assertTrue(result.contains("setField=SetValue"), "Should include set field: " + result); + assertFalse(result.contains("unsetField"), "Should NOT include unset field: " + result); + assertTrue( + result.contains("initialField=InitialValue"), "Should include initial field: " + result); + assertTrue( + result.contains("regularField=RegularValue"), "Should include regular field: " + result); + } + + @Test + void toString_nullTrackedValueIsShown() { + TestObject obj = new TestObject(); + obj.setField = TrackedValue.changedValue(null); + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("setField", obj.setField) + .toString(); + + assertTrue( + result.contains("setField=") || result.contains("setField=null"), + "Should show null value for set TrackedValue: " + result); + } + + @Test + void toString_nullRegularFieldIsShown() { + TestObject obj = new TestObject(); + obj.regularField = null; + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("regularField", obj.regularField) + .toString(); + + assertTrue( + result.contains("regularField=") || result.contains("regularField=null"), + "Should show null value for regular field: " + result); + } + + @Test + void toString_containsClassName() { + TestObject obj = new TestObject(); + + String result = new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE).toString(); + + assertTrue(result.contains("TestObject"), "Should contain class name: " + result); + } + + @Test + void toString_doesNotContainIdentityHashCode() { + TestObject obj = new TestObject(); + + String result = new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE).toString(); + + assertFalse(result.contains("@"), "Should NOT contain identity hash code (@): " + result); + } + + @Test + void toString_emptyBuilderShowsOnlyClassName() { + TestObject obj = new TestObject(); + + String result = new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE).toString(); + + assertTrue(result.contains("TestObject"), "Should contain class name: " + result); + assertTrue(result.contains("["), "Should contain opening bracket: " + result); + assertTrue(result.contains("]"), "Should contain closing bracket: " + result); + } + + @Test + void toString_multipleSetTrackedValues() { + TestObject obj = new TestObject(); + obj.setField = TrackedValue.changedValue("Value1"); + obj.initialField = TrackedValue.initialValue("Value2"); + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("setField", obj.setField) + .append("initialField", obj.initialField) + .toString(); + + assertTrue(result.contains("setField=Value1"), "Should include first set field: " + result); + assertTrue( + result.contains("initialField=Value2"), "Should include second set field: " + result); + } + + @Test + void toString_multipleUnsetTrackedValues() { + TestObject obj = new TestObject(); + obj.setField = TrackedValue.unsetValue(); + obj.unsetField = TrackedValue.unsetValue(); + + String result = + new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE) + .append("setField", obj.setField) + .append("unsetField", obj.unsetField) + .toString(); + + assertFalse(result.contains("setField"), "Should NOT include first unset field: " + result); + assertFalse(result.contains("unsetField"), "Should NOT include second unset field: " + result); + } + + static class TestObject { + TrackedValue setField; + TrackedValue unsetField; + TrackedValue initialField; + String regularField; + } +}