Repository: cjbooms/fabrikt
Pull request #102 (2022-01-04 10:34:48)

ISSUE-101: Add Support for @ReflectiveAccess Micronaut annotations
ISSUE-101: Add Support for @ReflectiveAccess Micronaut annotations


Patch:
diff --git a/README.md b/README.md
index 51e3aab..4f8d9f6 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,7 @@ This section documents the available CLI parameters for controlling what gets ge
  |                        |   `JAVA_SERIALIZATION` - This option adds Java Serializable interface to the generated models
  |                        |   `QUARKUS_REFLECTION` - This option adds @RegisterForReflection to the generated models. Requires dependency "'io.quarkus:quarkus-core:+"
  |                        |   `MICRONAUT_INTROSPECTION` - This option adds @Introspected to the generated models. Requires dependency "'io.micronaut:micronaut-core:+"
+ |                        |   `MICRONAUT_REFLECTION` - This option adds @ReflectiveAccess to the generated models. Requires dependency "'io.micronaut:micronaut-core:+"
  |   `--output-directory` | Allows the generation dir to be overridden. Defaults to current dir
  |   `--targets`          | Targets are the parts of the application that you want to be generated.
  |                        | CHOOSE ANY OF:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
index dabcd39..00728fd 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -27,7 +27,8 @@ enum class ModelCodeGenOptionType(val description: String) {
     X_EXTENSIBLE_ENUMS("This option treats x-extensible-enums as enums"),
     JAVA_SERIALIZATION("This option adds Java Serializable interface to the generated models"),
     QUARKUS_REFLECTION("This option adds @RegisterForReflection to the generated models. Requires dependency \"'io.quarkus:quarkus-core:+\""),
-    MICRONAUT_INTROSPECTION("This option adds @Introspected to the generated models. Requires dependency \"'io.micronaut:micronaut-core:+\"");
+    MICRONAUT_INTROSPECTION("This option adds @Introspected to the generated models. Requires dependency \"'io.micronaut:micronaut-core:+\""),
+    MICRONAUT_REFLECTION("This option adds @ReflectiveAccess to the generated models. Requires dependency \"'io.micronaut:micronaut-core:+\"");
 
     override fun toString() = "`${super.toString()}` - $description"
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
index 53a65c4..c1fdb22 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -290,6 +290,7 @@ class JacksonModelGenerator(
             )
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
+            .addMicronautReflectionAnnotation()
         enum.entries.forEach {
             classBuilder.addEnumConstant(
                 it.toEnumName(),
@@ -336,6 +337,7 @@ class JacksonModelGenerator(
             .addSerializableInterface()
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
+            .addMicronautReflectionAnnotation()
         properties.addToClass(classBuilder, ClassType.VANILLA_MODEL)
         return classBuilder.build()
     }
@@ -367,6 +369,7 @@ class JacksonModelGenerator(
         classBuilder.addAnnotation(polymorphicSubTypes(mappings, maybeEnumDiscriminator))
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
+            .addMicronautReflectionAnnotation()
 
         properties.addToClass(classBuilder, ClassType.SUPER_MODEL)
 
@@ -382,6 +385,7 @@ class JacksonModelGenerator(
             .addSerializableInterface()
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
+            .addMicronautReflectionAnnotation()
             .superclass(
                 toModelType(packages.base, KotlinTypeInfo.from(superType.schema, superType.name))
             )
@@ -423,18 +427,28 @@ class JacksonModelGenerator(
         return this
     }
 
-    private fun TypeSpec.Builder.addQuarkusReflectionAnnotation(): TypeSpec.Builder {
-        if (options.any { it == ModelCodeGenOptionType.QUARKUS_REFLECTION })
-            this.addAnnotation(
-                AnnotationSpec.builder("RegisterForReflection".toClassName("io.quarkus.runtime.annotations")).build()
-            )
-        return this
-    }
+    private fun TypeSpec.Builder.addQuarkusReflectionAnnotation(): TypeSpec.Builder =
+        this.addOptionalAnnotation(
+            ModelCodeGenOptionType.QUARKUS_REFLECTION,
+            "RegisterForReflection".toClassName("io.quarkus.runtime.annotations")
+        )
+
+    private fun TypeSpec.Builder.addMicronautIntrospectedAnnotation(): TypeSpec.Builder =
+        this.addOptionalAnnotation(
+            ModelCodeGenOptionType.MICRONAUT_INTROSPECTION,
+            "Introspected".toClassName("io.micronaut.core.annotation")
+        )
+
+    private fun TypeSpec.Builder.addMicronautReflectionAnnotation(): TypeSpec.Builder =
+        this.addOptionalAnnotation(
+            ModelCodeGenOptionType.MICRONAUT_REFLECTION,
+            "ReflectiveAccess".toClassName("io.micronaut.core.annotation")
+        )
 
-    private fun TypeSpec.Builder.addMicronautIntrospectedAnnotation(): TypeSpec.Builder {
-        if (options.any { it == ModelCodeGenOptionType.MICRONAUT_INTROSPECTION })
+    private fun TypeSpec.Builder.addOptionalAnnotation(optionType: ModelCodeGenOptionType, type: ClassName): TypeSpec.Builder {
+        if (options.any { it == optionType })
             this.addAnnotation(
-                AnnotationSpec.builder("Introspected".toClassName("io.micronaut.core.annotation")).build()
+                AnnotationSpec.builder(type).build()
             )
         return this
     }


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
index e9bd6f3..b541cfa 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -158,7 +158,7 @@ class ModelGeneratorTest {
 
     @Test
     fun `micronaut introspected models are generated from a full API definition when the micronaut-introspection option is set`() {
-        val basePackage = "examples.quarkusReflectionModels"
+        val basePackage = "examples.micronautIntrospectedModels"
         val spec = readTextResource("/examples/micronautIntrospectedModels/api.yaml")
         val expectedModels = readTextResource("/examples/micronautIntrospectedModels/models/Models.kt")
 
@@ -173,6 +173,23 @@ class ModelGeneratorTest {
         assertThat(models).isEqualTo(expectedModels)
     }
 
+    @Test
+    fun `micronaut reflection models are generated from a full API definition when the micronaut-reflection option is set`() {
+        val basePackage = "examples.micronautReflectionModels"
+        val spec = readTextResource("/examples/micronautReflectionModels/api.yaml")
+        val expectedModels = readTextResource("/examples/micronautReflectionModels/models/Models.kt")
+
+        val models = JacksonModelGenerator(
+            Packages(basePackage),
+            SourceApi(spec),
+            setOf(ModelCodeGenOptionType.MICRONAUT_REFLECTION)
+        )
+            .generate()
+            .toSingleFile()
+
+        assertThat(models).isEqualTo(expectedModels)
+    }
+
     private fun Models.toSingleFile(): String {
         val destPackage = if (models.isNotEmpty()) models.first().destinationPackage else ""
         val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
diff --git a/src/test/resources/examples/micronautIntrospectedModels/models/Models.kt b/src/test/resources/examples/micronautIntrospectedModels/models/Models.kt
index b8dde39..edeb18f 100644
--- a/src/test/resources/examples/micronautIntrospectedModels/models/Models.kt
+++ b/src/test/resources/examples/micronautIntrospectedModels/models/Models.kt
@@ -1,4 +1,4 @@
-package examples.quarkusReflectionModels.models
+package examples.micronautIntrospectedModels.models
 
 import com.fasterxml.jackson.annotation.JsonProperty
 import com.fasterxml.jackson.annotation.JsonSubTypes
diff --git a/src/test/resources/examples/micronautReflectionModels/api.yaml b/src/test/resources/examples/micronautReflectionModels/api.yaml
new file mode 100644
index 0000000..f732be4
--- /dev/null
+++ b/src/test/resources/examples/micronautReflectionModels/api.yaml
@@ -0,0 +1,110 @@
+openapi: "3.0.0"
+info:
+paths:
+components:
+  schemas:
+    QueryResult:
+      type: "object"
+      required:
+        - "items"
+      properties:
+        items:
+          type: "array"
+          minItems: 0
+          items:
+            oneOf:
+              - $ref: "#/components/schemas/FirstModel"
+              - $ref: "#/components/schemas/SecondModel"
+              - $ref: "#/components/schemas/ThirdModel"
+    Content:
+      type: "object"
+      required:
+        - "id"
+        - "attr_1"
+        - "attr_2"
+        - "attr_3"
+        - "etag"
+        - "model_type"
+      properties:
+        id:
+          description: "The unique resource id"
+          type: "string"
+          readOnly: true
+        first_attr:
+          description: "The attribute 1"
+          type: "string"
+          format: "date-time"
+          example: "2016-01-27T10:52:46.406Z"
+          readOnly: true
+        second_attr:
+          description: "The attribute 2"
+          type: "string"
+          readOnly: true
+        third_attr:
+          type: "string"
+          enum:
+            - "enum_type_1"
+            - "enum_type_2"
+          description: "Enum types for attribute 3"
+          example: "enum_type_2"
+        etag:
+          type: "string"
+          description: "Etag value to be used in conjunction with If-Match headers for optimistic locking purposes"
+          readOnly: true
+        model_type:
+          type: "string"
+          description: "The model discrimination type"
+          enum:
+            - "first_model"
+            - "second_model"
+            - "third_model"
+          example: "third_model"
+      discriminator:
+        propertyName: "model_type"
+        mapping:
+          first_model: "#/components/schemas/FirstModel"
+          second_model: "#/components/schemas/SecondModel"
+          third_model: "#/components/schemas/ThirdModel"
+
+    FirstModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 1"
+              type: array
+              items:
+                type: "string"
+                minItems: 1
+                maxItems: 10
+              readOnly: true
+    SecondModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 2"
+              type: "string"
+              readOnly: true
+            extra_second_attr:
+              description: "The attribute 2 for model 2"
+              type: boolean
+              readOnly: true
+
+    ThirdModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 3"
+              type: "string"
+              format: "date-time"
+              example: "2016-01-27T10:52:46.406Z"
+              readOnly: true
+            extra_second_attr:
+              description: "The attribute 2 for model 3"
+              type: integer
+              readOnly: true
diff --git a/src/test/resources/examples/micronautReflectionModels/models/Models.kt b/src/test/resources/examples/micronautReflectionModels/models/Models.kt
new file mode 100644
index 0000000..312bdbb
--- /dev/null
+++ b/src/test/resources/examples/micronautReflectionModels/models/Models.kt
@@ -0,0 +1,176 @@
+package examples.micronautReflectionModels.models
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonValue
+import io.micronaut.core.annotation.ReflectiveAccess
+import java.time.OffsetDateTime
+import javax.validation.Valid
+import javax.validation.constraints.NotNull
+import javax.validation.constraints.Size
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@ReflectiveAccess
+data class QueryResult(
+    @param:JsonProperty("items")
+    @get:JsonProperty("items")
+    @get:NotNull
+    @get:Size(min = 0)
+    @get:Valid
+    val items: List<Content>
+)
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "model_type",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(
+        value = FirstModel::class,
+        name =
+        "first_model"
+    ),
+    JsonSubTypes.Type(
+        value = SecondModel::class,
+        name =
+        "second_model"
+    ),
+    JsonSubTypes.Type(value = ThirdModel::class, name = "third_model")
+)
+@ReflectiveAccess
+sealed class Content(
+    open val id: String? = null,
+    open val firstAttr: OffsetDateTime? = null,
+    open val secondAttr: String? = null,
+    open val thirdAttr: ContentThirdAttr? = null,
+    open val etag: String? = null
+) {
+    abstract val modelType: ContentModelType
+}
+
+@ReflectiveAccess
+enum class ContentThirdAttr(
+    @JsonValue
+    val value: String
+) {
+    ENUM_TYPE_1("enum_type_1"),
+
+    ENUM_TYPE_2("enum_type_2");
+
+    companion object {
+        private val mapping: Map<String, ContentThirdAttr> =
+            values().associateBy(ContentThirdAttr::value)
+
+        fun fromValue(value: String): ContentThirdAttr? = mapping[value]
+    }
+}
+
+@ReflectiveAccess
+enum class ContentModelType(
+    @JsonValue
+    val value: String
+) {
+    FIRST_MODEL("first_model"),
+
+    SECOND_MODEL("second_model"),
+
+    THIRD_MODEL("third_model");
+
+    companion object {
+        private val mapping: Map<String, ContentModelType> =
+            values().associateBy(ContentModelType::value)
+
+        fun fromValue(value: String): ContentModelType? = mapping[value]
+    }
+}
+
+@ReflectiveAccess
+data class FirstModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ContentThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: List<String>? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag) {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ContentModelType = ContentModelType.FIRST_MODEL
+}
+
+@ReflectiveAccess
+data class SecondModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ContentThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: String? = null,
+    @param:JsonProperty("extra_second_attr")
+    @get:JsonProperty("extra_second_attr")
+    val extraSecondAttr: Boolean? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag) {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ContentModelType = ContentModelType.SECOND_MODEL
+}
+
+@ReflectiveAccess
+data class ThirdModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ContentThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("extra_second_attr")
+    @get:JsonProperty("extra_second_attr")
+    val extraSecondAttr: Int? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag) {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ContentModelType = ContentModelType.THIRD_MODEL
+}