Repository: cjbooms/fabrikt (kotlin)

Issue:
Add a configuration option to treat x-extensible-enum as an enum
Zalando's string format `x-extensible-enum` could be generated as an enum if requested by users.

The default should be not to treat these strings as enums as it could cause clients to break as new enum values are added. But for server-side development, it may be nicer to generate these definitions as enums

Pull request description:
Closes #23

Patch:
diff --git a/README.md b/README.md
index d0183d6c..e763bc67 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ This section documents the available CLI parameters for controlling what gets ge
  |                        |   `RESILIENCE4J` - Generates a fault tolerance service for the client using the following library "io.github.resilience4j:resilience4j-all:+"
  |   `--http-model-opts`  | Select the options for the http models that you want to be generated.
  |                        | CHOOSE ANY OF:
+ |                        |   `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:+"
  |   `--output-directory` | Allows the generation dir to be overridden. Defaults to current dir
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
index 00b23bc7..d178cfb3 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
@@ -2,6 +2,7 @@ package com.cjbooms.fabrikt.cli
 
 import com.beust.jcommander.ParameterException
 import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.MutableSettings
 import com.cjbooms.fabrikt.model.SourceApi
 import java.nio.file.Files
 import java.nio.file.Path
@@ -46,7 +47,7 @@ object CodeGen {
         val packages = Packages(basePackage)
         val sourceApi = SourceApi.create(suppliedApi, apiFragments, baseDir)
         val generator = CodeGenerator(packages, sourceApi, codeGenTypes, modelOptions, clientOptions)
-
+        MutableSettings.updateSettings(codeGenTypes, modelOptions, clientOptions)
         generator.generate().forEach { it.writeFileTo(outputDir.toFile()) }
     }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
index dc927c8a..366f4d94 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -46,6 +46,7 @@ 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:+\"");
 
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
new file mode 100644
index 00000000..87640019
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
@@ -0,0 +1,20 @@
+package com.cjbooms.fabrikt.generators
+
+import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
+import com.cjbooms.fabrikt.cli.CodeGenerationType
+import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
+
+object MutableSettings {
+    var generationTypes: MutableSet<CodeGenerationType> = mutableSetOf()
+    var modelOptions: MutableSet<ModelCodeGenOptionType> = mutableSetOf()
+    var clientOptions: MutableSet<ClientCodeGenOptionType> = mutableSetOf()
+    fun updateSettings(
+        genTypes: Set<CodeGenerationType>,
+        modelOptions: Set<ModelCodeGenOptionType>,
+        clientOptions: Set<ClientCodeGenOptionType>
+    ) {
+        this.generationTypes = genTypes.toMutableSet()
+        this.modelOptions = modelOptions.toMutableSet()
+        this.clientOptions = clientOptions.toMutableSet()
+    }
+}
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 59d23594..bdc3c5e5 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -1,5 +1,7 @@
 package com.cjbooms.fabrikt.util
 
+import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
+import com.cjbooms.fabrikt.generators.MutableSettings
 import com.cjbooms.fabrikt.model.OasType
 import com.cjbooms.fabrikt.model.PropertyInfo
 import com.cjbooms.fabrikt.util.NormalisedString.toMapValueClassName
@@ -54,12 +56,16 @@ object KaizenParserExtensions {
     fun Schema.isInlineableMapDefinition() = hasAdditionalProperties() && properties?.isEmpty() == true
 
     fun Schema.isEnumDefinition(): Boolean =
-        this.type == OasType.Text.type && (this.hasEnums() || extensions.containsKey(EXTENSIBLE_ENUM_KEY))
+        this.type == OasType.Text.type && (this.hasEnums() ||
+            (MutableSettings.modelOptions.contains(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS) &&
+                extensions.containsKey(EXTENSIBLE_ENUM_KEY)))
 
     @Suppress("UNCHECKED_CAST")
-    fun Schema.getEnumValues(): List<String> =
-        if (this.hasEnums()) this.enums.map { it.toString() }
-        else extensions[EXTENSIBLE_ENUM_KEY]?.let { it as List<String> } ?: emptyList()
+    fun Schema.getEnumValues(): List<String> = when {
+        this.hasEnums() -> this.enums.map { it.toString() }
+        !MutableSettings.modelOptions.contains(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS) -> emptyList()
+        else -> extensions[EXTENSIBLE_ENUM_KEY]?.let { it as List<String> } ?: emptyList()
+    }
 
     fun Schema.hasAdditionalProperties(): Boolean = isObjectType() && Overlay.of(additionalPropertiesSchema).isPresent
 


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 edace888..5270c91f 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -19,7 +19,7 @@ import org.junit.jupiter.params.provider.MethodSource
 class ModelGeneratorTest {
 
     private fun testCases(): Stream<String> = Stream.of(
-/*        "arrays",
+        "arrays",
         "anyOfOneOfAllOf",
         "deepNestedSharingReferences",
         "defaultValues",
@@ -30,19 +30,20 @@ class ModelGeneratorTest {
         "githubApi",
         "inLinedObject",
         "mapExamples",
-        "mixingCamelSnakeLispCase",*/
-        "oneOfPolymorphicModels"
-/*        "optionalVsRequired",
+        "mixingCamelSnakeLispCase",
+        "oneOfPolymorphicModels",
+        "optionalVsRequired",
         "polymorphicModels",
         "requiredReadOnly",
         "validationAnnotations",
-        "wildCardTypes"*/
+        "wildCardTypes"
     )
 
     @ParameterizedTest
     @MethodSource("testCases")
     fun `correct models are generated for different OpenApi Specifications`(testCaseName: String) {
         print("Testcase: $testCaseName")
+        MutableSettings.modelOptions.add(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS)
         val basePackage = "examples.$testCaseName"
         val apiLocation = javaClass.getResource("/examples/$testCaseName/api.yaml")
         val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI()))
diff --git a/src/test/resources/examples/enumPolymorphicDiscriminator/api.yaml b/src/test/resources/examples/enumPolymorphicDiscriminator/api.yaml
index 02dd5ebb..9cefc42d 100644
--- a/src/test/resources/examples/enumPolymorphicDiscriminator/api.yaml
+++ b/src/test/resources/examples/enumPolymorphicDiscriminator/api.yaml
@@ -34,7 +34,7 @@ components:
 
     EnumDiscriminator:
       type: string
-      x-extensible-enum:
+      enum:
         - obj_one
         - obj_two
 
diff --git a/src/test/resources/examples/githubApi/api.yaml b/src/test/resources/examples/githubApi/api.yaml
index 82775d07..630ef0db 100644
--- a/src/test/resources/examples/githubApi/api.yaml
+++ b/src/test/resources/examples/githubApi/api.yaml
@@ -751,7 +751,7 @@ components:
       in: query
       schema:
         type: string
-        x-extensible-enum:
+        enum:
         - active
         - inactive
         - all
@@ -1022,7 +1022,7 @@ components:
           readOnly: true
         status:
           type: string
-          x-extensible-enum:
+          enum:
           - active
           - inactive
           description: States whether the entity is currently active or inactive
@@ -1092,7 +1092,7 @@ components:
           readOnly: true
         status:
           type: string
-          x-extensible-enum:
+          enum:
           - active
           - inactive
           description: States whether the entity is currently active or inactive
@@ -1167,7 +1167,7 @@ components:
           readOnly: true
         status:
           type: string
-          x-extensible-enum:
+          enum:
           - active
           - inactive
           description: States whether the entity is currently active or inactive
@@ -1182,7 +1182,7 @@ components:
           type: string
         visibility:
           type: string
-          x-extensible-enum:
+          enum:
           - Private
           - Public
         tags:
@@ -1246,7 +1246,7 @@ components:
           readOnly: true
         status:
           type: string
-          x-extensible-enum:
+          enum:
           - active
           - inactive
           description: States whether the entity is currently active or inactive
diff --git a/src/test/resources/examples/javaSerializableModels/api.yaml b/src/test/resources/examples/javaSerializableModels/api.yaml
index 2fa8b07a..5e2f3799 100644
--- a/src/test/resources/examples/javaSerializableModels/api.yaml
+++ b/src/test/resources/examples/javaSerializableModels/api.yaml
@@ -219,7 +219,7 @@ components:
           readOnly: true
         third_attr:
           type: "string"
-          x-extensible-enum:
+          enum:
             - "enum_type_1"
             - "enum_type_2"
           description: "Enum types for attribute 3"
@@ -231,7 +231,7 @@ components:
         model_type:
           type: "string"
           description: "The model discrimination type"
-          x-extensible-enum:
+          enum:
             - "first_model"
             - "second_model"
             - "third_model"
diff --git a/src/test/resources/examples/okHttpClient/api.yaml b/src/test/resources/examples/okHttpClient/api.yaml
index 2fa8b07a..5e2f3799 100644
--- a/src/test/resources/examples/okHttpClient/api.yaml
+++ b/src/test/resources/examples/okHttpClient/api.yaml
@@ -219,7 +219,7 @@ components:
           readOnly: true
         third_attr:
           type: "string"
-          x-extensible-enum:
+          enum:
             - "enum_type_1"
             - "enum_type_2"
           description: "Enum types for attribute 3"
@@ -231,7 +231,7 @@ components:
         model_type:
           type: "string"
           description: "The model discrimination type"
-          x-extensible-enum:
+          enum:
             - "first_model"
             - "second_model"
             - "third_model"
diff --git a/src/test/resources/examples/quarkusReflectionModels/api.yaml b/src/test/resources/examples/quarkusReflectionModels/api.yaml
index 2fa8b07a..f732be40 100644
--- a/src/test/resources/examples/quarkusReflectionModels/api.yaml
+++ b/src/test/resources/examples/quarkusReflectionModels/api.yaml
@@ -1,184 +1,7 @@
 openapi: "3.0.0"
 info:
-  title: "API Example"
-  version: "1.0"
 paths:
-  /example-path-1:
-    get:
-      summary: "GET example path 1"
-      parameters:
-        - $ref: "#/components/parameters/QueryParam1"
-        - $ref: "#/components/parameters/QueryParam2"
-      responses:
-        200:
-          description: "successful operation"
-          headers:
-            Cache-Control:
-              $ref: "#/components/headers/CacheControl"
-          content:
-            application/json:
-              schema:
-                $ref: "#/components/schemas/QueryResult"
-
-    post:
-      summary: "POST example path 1"
-      parameters:
-        - $ref: "#/components/parameters/QueryParam1"
-      requestBody:
-        $ref: "#/components/requestBodies/PostBody"
-      responses:
-        201:
-          headers:
-            Location:
-              $ref: "#/components/headers/Location"
-          description: "Successful operation"
-
-  /example-path-2/{path_param}:
-    get:
-      summary: "GET example path 2"
-      parameters:
-        - $ref: "#/components/parameters/PathParam"
-        - $ref: "#/components/parameters/QueryParam2"
-        - $ref: "#/components/parameters/IfNoneMatch"
-      responses:
-        200:
-          description: "successful operation"
-          headers:
-            Cache-Control:
-              $ref: "#/components/headers/CacheControl"
-          content:
-            application/json:
-              schema:
-                discriminator:
-                  propertyName: "model_type"
-                oneOf:
-                  - $ref: "#/components/schemas/FirstModel"
-                  - $ref: "#/components/schemas/SecondModel"
-                  - $ref: "#/components/schemas/ThirdModel"
-        304:
-          description: "Returned in conjunction with use of the If-None-Match header.\
-            \ Indicates that the resource as described by the passed etag value has\
-            \ not changed"
-    head:
-      summary: "HEAD example path 2"
-      parameters:
-        - $ref: "#/components/parameters/PathParam"
-        - $ref: "#/components/parameters/QueryParam3"
-        - $ref: "#/components/parameters/IfNoneMatch"
-      responses:
-        200:
-          description: "Record Exists"
-        304:
-          description: "Returned in conjunction with use of the If-None-Match header.\
-            \ Indicates that the resource as described by the passed etag value has\
-            \ not changed"
-
-    put:
-      summary: "PUT example path 2"
-      parameters:
-        - $ref: "#/components/parameters/PathParam"
-        - $ref: "#/components/parameters/IfMatch"
-      requestBody:
-        $ref: "#/components/requestBodies/PutBody"
-      responses:
-        204:
-          description: "Operation successful"
-
-  /example-path-3/{path_param}/subresource:
-    put:
-      summary: "PUT example path 3"
-      parameters:
-        - $ref: "#/components/parameters/QueryParam1"
-        - $ref: "#/components/parameters/PathParam"
-        - $ref: "#/components/parameters/IfMatch"
-      requestBody:
-        $ref: "#/components/requestBodies/PutBody"
-      responses:
-        204:
-          description: "Operation successful"
-
 components:
-  parameters:
-    PathParam:
-      name: "path_param"
-      description: "The resource id"
-      in: "path"
-      required: true
-      schema:
-        type: "string"
-
-    IfMatch:
-      name: "If-Match"
-      description: "The RFC7232 If-Match header field"
-      in: "header"
-      schema:
-        type: "string"
-      required: true
-
-    IfNoneMatch:
-      name: "If-None-Match"
-      description: "The RFC7232 If-None-Match header field"
-      in: "header"
-      schema:
-        type: "string"
-      required: false
-
-    QueryParam1:
-      name: "query_param1"
-      in: "query"
-      schema:
-        type: "array"
-        items:
-          type: "string"
-      required: false
-      allowEmptyValue: false
-
-    QueryParam2:
-      name: "query_param2"
-      in: "query"
-      schema:
-        type: integer
-      required: false
-
-    QueryParam3:
-      name: "query_param3"
-      in: "query"
-      schema:
-        type: boolean
-      required: false
-
-  headers:
-    Location:
-      description: "The Location header indicates the URL of a newly created resource"
-      schema:
-        type: "string"
-
-    CacheControl:
-      description: "The RFC7234 Cache Control header"
-      schema:
-        type: "string"
-      example: "must-revalidate, max-age=5"
-
-  requestBodies:
-    PostBody:
-      required: true
-      content:
-        application/json:
-          schema:
-            discriminator:
-              propertyName: "model_type"
-            oneOf:
-              - $ref: "#/components/schemas/FirstModel"
-              - $ref: "#/components/schemas/SecondModel"
-              - $ref: "#/components/schemas/ThirdModel"
-    PutBody:
-      required: true
-      content:
-        application/json:
-          schema:
-            $ref: "#/components/schemas/FirstModel"
-
-
   schemas:
     QueryResult:
       type: "object"
@@ -219,7 +42,7 @@ components:
           readOnly: true
         third_attr:
           type: "string"
-          x-extensible-enum:
+          enum:
             - "enum_type_1"
             - "enum_type_2"
           description: "Enum types for attribute 3"
@@ -231,7 +54,7 @@ components:
         model_type:
           type: "string"
           description: "The model discrimination type"
-          x-extensible-enum:
+          enum:
             - "first_model"
             - "second_model"
             - "third_model"