diff --git a/README.md b/README.md index f63c5399..67765c7e 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,9 @@ Project project = ProjectBuilder.create() .description("Update README and add Javadocs") .build()) ) - .metadata(Map.of("version", "1.0.0", "owner", "dev-team")) + .metadata(metadata -> metadata + .put("version", "1.0.0") + .put("owner", "dev-team")) .build(); ``` diff --git a/codecov.yml b/codecov.yml index 23ac5f69..46dd4db4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,10 +5,11 @@ coverage: status: project: default: - target: auto + target: 80% patch: default: - target: auto + target: 90% + threshold: 5% comment: layout: "condensed_header, condensed_files, condensed_footer" diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java index d081130b..5fe8c644 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java @@ -46,7 +46,7 @@ public class BuilderDefinitionDto { /** * List of all fields of target DTO which are supported by builder. A field could be supported by - * several functions in builder. + * multiple functions in builder. */ private final List setterFieldsForBuilder = new LinkedList<>(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 2930faf9..f4c96885 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -45,6 +45,7 @@ import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; +import org.javahelpers.simple.builders.core.builders.HashMapBuilder; import org.javahelpers.simple.builders.core.builders.HashSetBuilder; import org.javahelpers.simple.builders.processor.dtos.*; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; @@ -134,26 +135,38 @@ private static Optional createMethodDto( private static void addAdditionalHelperMethodsForField( FieldDto result, String fieldName, TypeName fieldType) { - if (isList(fieldType)) { + List innerTypes; + int innerTypesCnt; + if (fieldType instanceof TypeNameGeneric fieldTypeGeneric) { + innerTypes = fieldTypeGeneric.getInnerTypeArguments(); + innerTypesCnt = innerTypes.size(); + } else { + innerTypes = null; + innerTypesCnt = 0; + } + + if (isList(fieldType) && innerTypesCnt == 1) { result.addMethod( createFieldSetterWithTransform( fieldName, "List.of(%s)", - new TypeNameArray(fieldType.getInnerType().get(), false), + new TypeNameArray(innerTypes.get(0), false), result.getFieldGenerics())); - return; - } - if (isSet(fieldType)) { + } else if (isSet(fieldType) && innerTypesCnt == 1) { result.addMethod( createFieldSetterWithTransform( fieldName, "Set.of(%s)", - new TypeNameArray(fieldType.getInnerType().get(), true), + new TypeNameArray(innerTypes.get(0), true), result.getFieldGenerics())); - return; - } - if (isMap(fieldType)) { - // TODO MAP (having 2 inner classes, TypeNameGeneric is not able to address that yet) + } else if (isMap(fieldType) && innerTypesCnt == 2) { + TypeName mapEntryType = + new TypeNameArray( + new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), + false); + result.addMethod( + createFieldSetterWithTransform( + fieldName, "Map.ofEntries(%s)", mapEntryType, result.getFieldGenerics())); } } @@ -185,16 +198,33 @@ private static void addConsumerMethodsForField( && hasEmptyConstructor(fieldTypeElement, elementUtils)) { // Only generate a Consumer for concrete classes with an accessible empty constructor result.addMethod(createFieldConsumer(fieldName, fieldType)); - } else if (isList(fieldType)) { + } else if (isList(fieldType) + && fieldType instanceof TypeNameGeneric fieldTypeGeneric + && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { result.addMethod( createFieldConsumerWithBuilder( - fieldName, map2TypeName(ArrayListBuilder.class), fieldType.getInnerType().get())); - } else if (isMap(fieldType)) { - // TODO MAP (having 2 inner classes, TypeNameGeneric is not able to adress that yet) - } else if (isSet(fieldType)) { + fieldName, + map2TypeName(ArrayListBuilder.class), + fieldTypeGeneric.getInnerTypeArguments().get(0))); + } else if (isMap(fieldType) + && fieldType instanceof TypeNameGeneric fieldTypeGeneric + && fieldTypeGeneric.getInnerTypeArguments().size() == 2) { + TypeName builderTargetTypeName = + new TypeNameGeneric( + map2TypeName(HashMapBuilder.class), + fieldTypeGeneric.getInnerTypeArguments().get(0), + fieldTypeGeneric.getInnerTypeArguments().get(1)); + MethodDto mapConsumerWithBuilder = + BuilderDefinitionCreator.createFieldConsumerWithBuilder(fieldName, builderTargetTypeName); + result.addMethod(mapConsumerWithBuilder); + } else if (isSet(fieldType) + && fieldType instanceof TypeNameGeneric fieldTypeGeneric + && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { result.addMethod( createFieldConsumerWithBuilder( - fieldName, map2TypeName(HashSetBuilder.class), fieldType.getInnerType().get())); + fieldName, + map2TypeName(HashSetBuilder.class), + fieldTypeGeneric.getInnerTypeArguments().get(0))); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index 2670f420..23dfeaf9 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -762,6 +762,60 @@ public void setAge(int age) { "public PersonBuilder age(int age)"); } + @Test + void shouldHandleGenericWrapperTypes() { + + JavaFileObject optionalWrapperClass = + ProcessorTestUtils.forSource( + """ + package test; + import java.util.Optional; + import java.time.LocalDate; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class OptionalWrapper { + private Optional name; + private Optional count; + private Optional date; + + public Optional getName() { return name; } + public void setName(Optional name) { this.name = name; } + + public Optional getCount() { return count; } + public void setCount(Optional count) { this.count = count; } + + public Optional getDate() { return date; } + public void setDate(Optional date) { this.date = date; } + } + """); + + // Test builder generation (without usage) + Compilation compilation = compile(optionalWrapperClass); + String optionalBuilder = loadGeneratedSource(compilation, "OptionalWrapperBuilder"); + + // Verify Optional wrapper generates basic methods + assertGenerationSucceeded(compilation, "OptionalWrapperBuilder", optionalBuilder); + ProcessorAsserts.assertContaining( + optionalBuilder, + "public static OptionalWrapperBuilder create()", + "public OptionalWrapperBuilder name(Optional name)", + "public OptionalWrapperBuilder count(Optional count)", + "public OptionalWrapperBuilder date(Optional date)", + "public OptionalWrapper build()"); + + // Verify no additional helper methods are generated for these generic types + // (since they don't match List/Set/Map patterns) + ProcessorAsserts.assertNotContaining( + optionalBuilder, + "nameBuilderConsumer", + "countBuilderConsumer", + "dateBuilderConsumer", + "name(String...)", + "count(Integer...)", + "date(LocalDate...)"); + } + @Test void shouldGenerateSetterForClassInDifferentPackage() { // Given @@ -1004,42 +1058,6 @@ void shouldNotGenerateConsumersForPrimitiveAndArrayFields() { notContains("public PrimAndArrayBuilder names(Consumer")); } - @Test - void shouldNotGenerateSpecialMethodsForMapBeyondSetterAndSupplier() { - // Given - String packageName = "test"; - String className = "HasMap"; - String builderClassName = className + "Builder"; - - JavaFileObject sourceFile = - ProcessorTestUtils.simpleBuilderClass( - packageName, - className, - """ - private java.util.Map map; - - public java.util.Map getMap() { return map; } - public void setMap(java.util.Map map) { this.map = map; } - """); - - // When - Compilation compilation = compile(sourceFile); - - // Then - String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); - - // Expect only direct setter and supplier; no varargs/consumer for Map - ProcessorAsserts.assertingResult( - generatedCode, - contains("public HasMapBuilder map("), - contains("mapSupplier)"), - notContains("map(String..."), - notContains("map(java.lang.String..."), - notContains("mapBuilderConsumer"), - notContains("mapConsumer")); - } - @Test void shouldNotGenerateConsumerForAbstractHelper() { // Given @@ -1959,4 +1977,57 @@ void shouldHandleOverloadedSettersForSameFieldWithoutConflicts() { "public OverloadedNamesBuilder names(Supplier> namesSupplier)", "public OverloadedNamesBuilder names(String... names)"); } + + @Test + void collectionsWithRawTypes_shouldGenerateBuilders() { + + JavaFileObject rawCollectionsClass = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import java.util.List; + import java.util.Set; + import java.util.Map; + @SimpleBuilder + public class RawCollections { + private List rawList; + private Set rawSet; + private Map rawMap; + + public List getRawList() { return rawList; } + public void setRawList(List rawList) { this.rawList = rawList; } + + public Set getRawSet() { return rawSet; } + public void setRawSet(Set rawSet) { this.rawSet = rawSet; } + + public Map getRawMap() { return rawMap; } + public void setRawMap(Map rawMap) { this.rawMap = rawMap; } + } + """); + + // Test builder generation (without usage) + Compilation compilation = compile(rawCollectionsClass); + String generatedCode = loadGeneratedSource(compilation, "RawCollectionsBuilder"); + assertGenerationSucceeded(compilation, "RawCollectionsBuilder", generatedCode); + + // Verify basic builder methods are generated + ProcessorAsserts.assertContaining( + generatedCode, + "public static RawCollectionsBuilder create()", + "public RawCollectionsBuilder rawList(List rawList)", + "public RawCollectionsBuilder rawSet(Set rawSet)", + "public RawCollectionsBuilder rawMap(Map rawMap)", + "public RawCollections build()"); + + // Verify no additional helper methods are generated for raw types + ProcessorAsserts.assertNotContaining( + generatedCode, + "rawListBuilderConsumer", + "rawSetBuilderConsumer", + "rawMapBuilderConsumer", + "rawList(String...)", + "rawSet(String...)", + "rawMap(Map.Entry)"); + } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java index 768311af..3ce9eafd 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java @@ -1,6 +1,5 @@ package org.javahelpers.simple.builders.processor; -import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; @@ -8,18 +7,15 @@ import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; -import org.junit.jupiter.api.BeforeEach; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.junit.jupiter.api.Test; class ReadmeExampleTest { - protected BuilderProcessor processor; - protected Compiler compiler; - - @BeforeEach - protected void setUp() { - processor = new BuilderProcessor(); - compiler = Compiler.javac().withProcessors(processor); + private Compilation compileSources(JavaFileObject... sources) { + BuilderProcessor processor = new BuilderProcessor(); + Compiler compiler = Compiler.javac().withProcessors(processor); + return compiler.compile(sources); } @Test @@ -49,18 +45,41 @@ public class Person { } """); - Compilation compilation = compiler.compile(person); + JavaFileObject usage = + JavaFileObjects.forSourceString( + packageName + ".Usage", + """ + package readme; + public class Usage { + public static void main(){ + Person person = PersonBuilder.create() + .name("John Doe") + .age(30) + .emailAddresses("john@example.com", "j.doe@example.com") + .build(); + } + } + """); + + // Test 1: Builder generation (without usage) + Compilation compilation = compileSources(person); String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); + ProcessorAsserts.assertGenerationSucceeded(compilation, builderClassName, generatedCode); // Fixed expected strings based on README usage snippet - org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertingResult( + ProcessorAsserts.assertingResult( generatedCode, contains("public static PersonBuilder create()"), contains("public PersonBuilder name(String name)"), contains("public PersonBuilder age(int age)"), contains("public PersonBuilder emailAddresses(String... emailAddresses)"), contains("public Person build()")); + + // Test 2: Usage compilation (with usage) + Compilation compilationWithUsage = compileSources(person, usage); + + // Validate the full usage too + assertGenerationSucceeded(compilationWithUsage, "PersonBuilder"); } @Test @@ -120,31 +139,73 @@ public class Task { public enum ProjectStatus { PLANNING, IN_PROGRESS, COMPLETED, ON_HOLD } """); - Compilation compilation = compiler.compile(project, task, statusEnum); + JavaFileObject usage = + JavaFileObjects.forSourceString( + packageName + ".Usage", + """ + package readme; + public class Usage { + public static void main(){ + Project project = ProjectBuilder.create() + .name("Simple Builders with a bit of complexity") + .status(ProjectStatus.IN_PROGRESS) + .tasks(tasks -> tasks + .add(TaskBuilder.create() + .title("Implement core functionality") + .completed(true) + .build()) + .add(TaskBuilder.create() + .title("Add documentation") + .description("Update README and add Javadocs") + .build()) + ) + .metadata(metadata -> metadata + .put("version", "1.0.0") + .put("owner", "dev-team")) + .build(); + } + } + """); + + Compilation compilation = compileSources(project, task, statusEnum); String projectBuilder = loadGeneratedSource(compilation, "ProjectBuilder"); - assertGenerationSucceeded(compilation, "ProjectBuilder", projectBuilder); + ProcessorAsserts.assertGenerationSucceeded(compilation, "ProjectBuilder", projectBuilder); // Assert key capabilities corresponding to README usage - org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertingResult( + ProcessorAsserts.assertingResult( projectBuilder, contains("public static ProjectBuilder create()"), contains("public ProjectBuilder name(String name)"), contains( "public ProjectBuilder tasks(Consumer> tasksBuilderConsumer)"), - contains("public ProjectBuilder metadata(Map metadata)"), + contains( + "public ProjectBuilder metadata(Consumer> metadataBuilderConsumer"), contains("public ProjectBuilder status(ProjectStatus status)"), contains("public Project build()")); // Also validate TaskBuilder exists and typical fluent API String taskBuilder = loadGeneratedSource(compilation, "TaskBuilder"); - assertGenerationSucceeded(compilation, "TaskBuilder", taskBuilder); - org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertingResult( + ProcessorAsserts.assertGenerationSucceeded(compilation, "TaskBuilder", taskBuilder); + ProcessorAsserts.assertingResult( taskBuilder, contains("public static TaskBuilder create()"), contains("public TaskBuilder title(String title)"), contains("public TaskBuilder description(String description)"), contains("public TaskBuilder completed(boolean completed)"), contains("public Task build()")); + + // Validate the full usage too + Compilation compilationWithUsage = compileSources(project, task, statusEnum, usage); + assertGenerationSucceeded(compilationWithUsage, "TaskBuilder"); + } + + /** + * Asserts compilation succeeded and that the basic builder API exists in the provided generated + * source (build() and static create()). + */ + public static void assertGenerationSucceeded(Compilation compilation, String builderSimpleName) { + ProcessorAsserts.assertGenerationSucceeded( + compilation, builderSimpleName, loadGeneratedSource(compilation, builderSimpleName)); } }