From 779854f567d05fedce74b3ea2a9f9657f6042456 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk <80816836+andriy-dmytruk@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:03:49 -0400 Subject: [PATCH] Add an ability to introspect builders (#135) Adds the ability to specify `annotatedWith` on `@Builder` and `@SuperBuilder` thus allowing additional annotations to be added to the generated code. --- .../sourcegen/annotations/Builder.java | 13 ++++++++ .../sourcegen/annotations/SuperBuilder.java | 13 ++++++++ .../visitors/BuilderAnnotationVisitor.java | 31 ++++++++++++++++--- .../SuperBuilderAnnotationVisitor.java | 11 +++++-- .../BuilderAnnotationVisitorSpec.groovy | 4 +-- .../example/AnimalSuperBuilderTest.java | 11 +++++++ .../sourcegen/example/PersonBuilderTest.java | 10 ++++++ 7 files changed, 84 insertions(+), 9 deletions(-) diff --git a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java b/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java index 625bbe7d..ff258d15 100644 --- a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java +++ b/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java @@ -15,6 +15,9 @@ */ package io.micronaut.sourcegen.annotations; +import io.micronaut.core.annotation.Introspected; + +import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -32,4 +35,14 @@ @Retention(RUNTIME) @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) public @interface Builder { + + /** + * Define what annotations should be added to the generated builder. By default, + * the builder will have {@link io.micronaut.core.annotation.Introspected} annotation + * so that introspection can be created for it. + * + * @return Array of annotations to apply on the builder + */ + Class[] annotatedWith() default Introspected.class; + } diff --git a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/SuperBuilder.java b/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/SuperBuilder.java index d5e967af..73ceddcb 100644 --- a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/SuperBuilder.java +++ b/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/SuperBuilder.java @@ -15,6 +15,9 @@ */ package io.micronaut.sourcegen.annotations; +import io.micronaut.core.annotation.Introspected; + +import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -33,4 +36,14 @@ @Retention(RUNTIME) @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) public @interface SuperBuilder { + + /** + * Define what annotations should be added to the generated builder. By default, + * the builder will have {@link io.micronaut.core.annotation.Introspected} annotation + * so that introspection can be created for it. + * + * @return Array of annotations to apply on the builder + */ + Class[] annotatedWith() default Introspected.class; + } diff --git a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java index c25649fd..32880ae9 100644 --- a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java +++ b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java @@ -15,7 +15,11 @@ */ package io.micronaut.sourcegen.generator.visitors; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.bind.annotation.Bindable; @@ -32,6 +36,7 @@ import io.micronaut.sourcegen.generator.SourceGenerator; import io.micronaut.sourcegen.generator.SourceGenerators; import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder; import io.micronaut.sourcegen.model.ClassTypeDef; import io.micronaut.sourcegen.model.ExpressionDef; import io.micronaut.sourcegen.model.FieldDef; @@ -41,7 +46,6 @@ import io.micronaut.sourcegen.model.TypeDef; import io.micronaut.sourcegen.model.VariableDef; -import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; import javax.lang.model.element.Modifier; @@ -71,6 +75,8 @@ @Internal public final class BuilderAnnotationVisitor implements TypeElementVisitor { + public static final String BUILDER_ANNOTATED_WITH_MEMBER = "annotatedWith"; + private final Set processed = new HashSet<>(); @Override @@ -101,6 +107,7 @@ public void visitClass(ClassElement element, VisitorContext context) { ClassDef.ClassDefBuilder builder = ClassDef.builder(builderClassName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + addAnnotations(builder, element.getAnnotation(Builder.class)); List properties = element.getBeanProperties(); for (PropertyElement beanProperty : properties) { @@ -150,8 +157,22 @@ public void visitClass(ClassElement element, VisitorContext context) { } } - private MethodDef createAllPropertiesConstructor(ClassTypeDef builderType, List properties) { - MethodDef.MethodDefBuilder builder = MethodDef.constructor(); + static void addAnnotations(ClassDefBuilder builder, AnnotationValue annotation) { + Optional annotatedWith = annotation.getConvertibleValues() + .get(BUILDER_ANNOTATED_WITH_MEMBER, AnnotationClassValue[].class); + if (annotatedWith.isEmpty()) { + // Apply the default annotation + builder.addAnnotation(Introspected.class); + } else { + for (AnnotationClassValue value: annotatedWith.get()) { + builder.addAnnotation(value.getName()); + } + } + } + + static MethodDef createAllPropertiesConstructor(ClassTypeDef builderType, List properties) { + MethodDef.MethodDefBuilder builder = MethodDef.constructor() + .addAnnotation(Creator.class); VariableDef.This self = new VariableDef.This(builderType); for (PropertyElement parameter : properties) { ParameterDef parameterDef = ParameterDef.of(parameter.getName(), TypeDef.of(parameter.getType())); @@ -180,7 +201,7 @@ private MethodDef createAllPropertiesConstructor(ClassTypeDef builderType, List< return builder.build(); } - private StatementDef iterableToArrayListStatement(VariableDef.This self, ParameterDef parameterDef) { + private static StatementDef iterableToArrayListStatement(VariableDef.This self, ParameterDef parameterDef) { return ClassTypeDef.of(ArrayList.class) .instantiate() .newLocal(parameterDef.getName() + "ArrayList", arrayListVar -> @@ -198,7 +219,7 @@ private StatementDef iterableToArrayListStatement(VariableDef.This self, Paramet ))); } - private StatementDef mapToArrayListStatement(VariableDef.This self, ParameterDef parameterDef) { + private static StatementDef mapToArrayListStatement(VariableDef.This self, ParameterDef parameterDef) { return self.field(parameterDef.getName(), parameterDef.getType()) .assign( ClassTypeDef.of(ArrayList.class) diff --git a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/SuperBuilderAnnotationVisitor.java b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/SuperBuilderAnnotationVisitor.java index c9c42691..c90932ec 100644 --- a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/SuperBuilderAnnotationVisitor.java +++ b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/SuperBuilderAnnotationVisitor.java @@ -30,11 +30,12 @@ import io.micronaut.sourcegen.model.MethodDef; import io.micronaut.sourcegen.model.TypeDef; -import java.util.HashSet; -import java.util.Set; import javax.lang.model.element.Modifier; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import static io.micronaut.sourcegen.generator.visitors.BuilderAnnotationVisitor.addAnnotations; import static io.micronaut.sourcegen.generator.visitors.BuilderAnnotationVisitor.createModifyPropertyMethod; /** @@ -148,6 +149,12 @@ public void visitClass(ClassElement element, VisitorContext context) { ) ) ); + addAnnotations(builder, element.getAnnotation(SuperBuilder.class)); + + builder.addMethod(MethodDef.constructor().build()); + if (!properties.isEmpty()) { + builder.addMethod(BuilderAnnotationVisitor.createAllPropertiesConstructor(builderType, properties)); + } builder.addMethod(createSelfMethod()); builder.addMethod(BuilderAnnotationVisitor.createBuildMethod(element)); diff --git a/sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitorSpec.groovy b/sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitorSpec.groovy index c685ecad..0803a7e1 100644 --- a/sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitorSpec.groovy +++ b/sourcegen-generator/src/test/groovy/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitorSpec.groovy @@ -10,7 +10,7 @@ class BuilderAnnotationVisitorSpec extends AbstractTypeElementSpec { package test; import io.micronaut.sourcegen.annotations.Builder; - @Builder + @Builder(annotatedWith = {}) public record Walrus( String name, int age, @@ -35,7 +35,7 @@ class BuilderAnnotationVisitorSpec extends AbstractTypeElementSpec { package test; import io.micronaut.sourcegen.annotations.Builder; - @Builder + @Builder(annotatedWith = {}) public record Walrus() { } """) diff --git a/test-suite-java/src/test/java/io/micronaut/sourcegen/example/AnimalSuperBuilderTest.java b/test-suite-java/src/test/java/io/micronaut/sourcegen/example/AnimalSuperBuilderTest.java index 4edb6db4..d5fec082 100644 --- a/test-suite-java/src/test/java/io/micronaut/sourcegen/example/AnimalSuperBuilderTest.java +++ b/test-suite-java/src/test/java/io/micronaut/sourcegen/example/AnimalSuperBuilderTest.java @@ -16,9 +16,12 @@ package io.micronaut.sourcegen.example; import java.lang.reflect.Modifier; + +import io.micronaut.core.beans.BeanIntrospection; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class AnimalSuperBuilderTest { @@ -61,6 +64,14 @@ public void testDog() { } //end::test[] + @Test + public void dogIntrospection() { + var introspection = BeanIntrospection.getIntrospection(DogSuperBuilder.class); + assertNotNull(introspection); + assertEquals(0, introspection.getBeanProperties().size()); + assertEquals(6, introspection.getConstructorArguments().length); + } + @Test public void internalTest() { assertTrue(Modifier.isPublic(CatSuperBuilder.class.getModifiers())); diff --git a/test-suite-java/src/test/java/io/micronaut/sourcegen/example/PersonBuilderTest.java b/test-suite-java/src/test/java/io/micronaut/sourcegen/example/PersonBuilderTest.java index a73d35d6..fc453cf0 100644 --- a/test-suite-java/src/test/java/io/micronaut/sourcegen/example/PersonBuilderTest.java +++ b/test-suite-java/src/test/java/io/micronaut/sourcegen/example/PersonBuilderTest.java @@ -15,10 +15,12 @@ */ package io.micronaut.sourcegen.example; +import io.micronaut.core.beans.BeanIntrospection; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; class PersonBuilderTest { @@ -36,6 +38,14 @@ public void buildsPerson() { } //end::test[] + @Test + public void personIntrospection() { + var introspection = BeanIntrospection.getIntrospection(PersonBuilder.class); + assertNotNull(introspection); + assertEquals(0, introspection.getBeanProperties().size()); + assertEquals(3, introspection.getConstructorArguments().length); + } + @Test public void buildsPersonWithPrimitiveDefaults() { var person = Person2Builder.builder()