Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.
*
* <p>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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
Loading