diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java index ad05a2b4..bb7696f4 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java @@ -30,9 +30,17 @@ import java.lang.annotation.Target; /** - * Annotation to be placed on a constructor, if a specific constructor should be used for building - * the resulting DTO. + * Annotation to be placed on a constructor to indicate that this specific constructor should be + * used for building the resulting DTO. + * + *
This annotation supports both regular class constructors and record constructors. Note that + * {@code ElementType.METHOD} is included in the target to support record constructors (which are + * treated as methods in certain contexts), but this annotation should only be applied to actual + * constructors. When placed on regular methods, the annotation will be silently ignored. + * + *
If multiple constructors exist and none is annotated, the processor will select the + * constructor with the most parameters. */ -@Target({ElementType.CONSTRUCTOR}) +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) @Retention(RetentionPolicy.CLASS) public @interface SimpleBuilderConstructor {} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationValidator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationValidator.java index 6149e108..025ce74a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationValidator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationValidator.java @@ -46,10 +46,13 @@ private AnnotationValidator() {} * @throws BuilderException if annotation-target does not match supported types */ public static void validateAnnotatedElement(Element annotatedElement) throws BuilderException { - if (annotatedElement.getKind() != ElementKind.CLASS) { + if (annotatedElement.getKind() != ElementKind.CLASS + && annotatedElement.getKind() != ElementKind.RECORD) { throw new BuilderException( annotatedElement, - "The " + SimpleBuilder.class.getSimpleName() + " should annotated " + " on class."); + "The " + + SimpleBuilder.class.getSimpleName() + + " should be annotated on a class or record."); } if (annotatedElement.getModifiers().contains(Modifier.ABSTRACT)) { 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 c1fe3630..9a9c28fa 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 @@ -270,7 +270,45 @@ public AnnotatedCtorChoice(int a, String name, boolean flag) { } @Test - @Disabled("TODO: Records are not supported for builder generation yet") + void shouldIgnoreSimpleBuilderConstructorOnRegularMethod() { + // Given: @SimpleBuilderConstructor incorrectly placed on a regular method (not constructor) + String className = "WrongMethodAnnotation"; + String builderClassName = className + "Builder"; + + JavaFileObject source = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; + + @SimpleBuilder + public class WrongMethodAnnotation { + private String name; + + @SimpleBuilderConstructor + public void notAConstructor(String name) { + this.name = name; + } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When + Compilation compilation = compile(source); + + // Then: Builder should still be generated, but annotation on method should be ignored + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + // Builder should have method from setter, not from annotated regular method + ProcessorAsserts.assertContaining( + generatedCode, "public WrongMethodAnnotationBuilder name(String name)"); + } + + @Test void shouldHandleRecordWithoutGeneratingBuilder() { // Given String recordName = "PersonRecord"; @@ -299,9 +337,49 @@ public record PersonRecord(String name, int age) { ProcessorAsserts.assertContaining( generatedCode, "public PersonRecordBuilder name(String name)", - "this.name = name;", + "this.name = changedValue(name);", "public PersonRecordBuilder age(int age)", - "this.age = age;"); + "this.age = changedValue(age);"); + } + + @Test + void shouldHandleRecordWithMultipleConstructorsAndAnnotatedNonCanonical() { + // Given: Record with canonical constructor (name, age, email) but a secondary + // constructor with fewer params is annotated with @SimpleBuilderConstructor + String recordName = "PersonWithEmail"; + String builderClassName = recordName + "Builder"; + + JavaFileObject recordFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; + + @SimpleBuilder + public record PersonWithEmail(String name, int age, String email) { + @SimpleBuilderConstructor + public PersonWithEmail(String name, int age) { + this(name, age, null); + } + } + """); + + // When + Compilation compilation = compile(recordFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + // Builder should only generate methods for the annotated constructor (name, age) + // NOT for email, since records don't have setters and email is not in the annotated ctor + ProcessorAsserts.assertContaining( + generatedCode, + "public PersonWithEmailBuilder name(String name)", + "public PersonWithEmailBuilder age(int age)"); + // Email should NOT have a builder method since it's not in the annotated constructor + ProcessorAsserts.assertNotContaining(generatedCode, "public PersonWithEmailBuilder email"); } @Test