Repository: cjbooms/fabrikt
Pull request #73 (2021-08-16 10:03:31)

ISSUE-58: Add support for Micronaut @Introspected models
ISSUE-58: Add support for adding the Micronaut @Introspected annotation to the models


Patch:
diff --git a/README.md b/README.md
index e9f04d4..2fe30e5 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,7 @@ This section documents the available CLI parameters for controlling what gets ge
  |                        |   `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:+"
  |   `--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 77e16cc..dabcd39 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -26,7 +26,8 @@ enum class ClientCodeGenOptionType(private val description: String) {
 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:+\"");
+    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:+\"");
 
     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 7325f88..46492dd 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -268,6 +268,7 @@ class JacksonModelGenerator(
                     .build()
             )
             .addQuarkusReflectionAnnotation()
+            .addMicronautIntrospectedAnnotation()
         enum.entries.forEach {
             classBuilder.addEnumConstant(
                 it.toUpperCase(),
@@ -304,7 +305,8 @@ class JacksonModelGenerator(
                 )
                 .addModifiers(KModifier.DATA)
                 .addSerializableInterface()
-                .addQuarkusReflectionAnnotation(),
+                .addQuarkusReflectionAnnotation()
+                .addMicronautIntrospectedAnnotation(),
             ClassType.VANILLA_MODEL
         )
 
@@ -334,7 +336,9 @@ class JacksonModelGenerator(
                 it to toModelType(packages.base, KotlinTypeInfo.from(schemaInfo.schema, schemaInfo.name))
             }
         }.toMap()
-        classBuilder.addAnnotation(polymorphicSubTypes(mappings)).addQuarkusReflectionAnnotation()
+        classBuilder.addAnnotation(polymorphicSubTypes(mappings))
+            .addQuarkusReflectionAnnotation()
+            .addMicronautIntrospectedAnnotation()
 
         return properties.addToClass(
             classBuilder,
@@ -358,6 +362,7 @@ class JacksonModelGenerator(
                 .addModifiers(KModifier.DATA)
                 .addSerializableInterface()
                 .addQuarkusReflectionAnnotation()
+                .addMicronautIntrospectedAnnotation()
                 .superclass(
                     toModelType(
                         packages.base,
@@ -401,4 +406,12 @@ class JacksonModelGenerator(
             )
         return this
     }
+
+    private fun TypeSpec.Builder.addMicronautIntrospectedAnnotation(): TypeSpec.Builder {
+        if (options.any { it == ModelCodeGenOptionType.MICRONAUT_INTROSPECTION })
+            this.addAnnotation(
+                AnnotationSpec.builder("Introspected".toClassName("io.micronaut.core.annotation")).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 889e80f..e9bd6f3 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -156,6 +156,23 @@ class ModelGeneratorTest {
         assertThat(models).isEqualTo(expectedModels)
     }
 
+    @Test
+    fun `micronaut introspected models are generated from a full API definition when the micronaut-introspection option is set`() {
+        val basePackage = "examples.quarkusReflectionModels"
+        val spec = readTextResource("/examples/micronautIntrospectedModels/api.yaml")
+        val expectedModels = readTextResource("/examples/micronautIntrospectedModels/models/Models.kt")
+
+        val models = JacksonModelGenerator(
+            Packages(basePackage),
+            SourceApi(spec),
+            setOf(ModelCodeGenOptionType.MICRONAUT_INTROSPECTION)
+        )
+            .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/api.yaml b/src/test/resources/examples/micronautIntrospectedModels/api.yaml
new file mode 100644
index 0000000..f732be4
--- /dev/null
+++ b/src/test/resources/examples/micronautIntrospectedModels/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/micronautIntrospectedModels/models/Models.kt b/src/test/resources/examples/micronautIntrospectedModels/models/Models.kt
new file mode 100644
index 0000000..8ade43e
--- /dev/null
+++ b/src/test/resources/examples/micronautIntrospectedModels/models/Models.kt
@@ -0,0 +1,161 @@
+package examples.quarkusReflectionModels.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.Introspected
+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
+
+@Introspected
+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")
+)
+@Introspected
+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
+}
+
+@Introspected
+enum class ContentThirdAttr(
+    @JsonValue
+    val value: String
+) {
+    ENUM_TYPE_1("enum_type_1"),
+
+    ENUM_TYPE_2("enum_type_2");
+}
+
+@Introspected
+enum class ContentModelType(
+    @JsonValue
+    val value: String
+) {
+    FIRST_MODEL("first_model"),
+
+    SECOND_MODEL("second_model"),
+
+    THIRD_MODEL("third_model");
+}
+
+@Introspected
+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
+}
+
+@Introspected
+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
+}
+
+@Introspected
+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
+}