Repository: cjbooms/fabrikt
Pull request #84 (2021-11-24 16:26:08)

Improving the Normalised String utilities.
Improved generation to support ENUM values containing special characters and classes containing numbers

This schmea:
```yaml
    ContentType:
      type: string
      x-extensible-enum:
        - application/json
        - application/x.some-type+json    
        - application/x.some-other-type+json;version=2  
```
Will now produce this enum class:
```kotlin
enum class ContentType(
    @JsonValue
    val value: String
) {
    APPLICATION_JSON("application/json"),

    APPLICATION_X_SOME_TYPE_JSON("application/x.some-type+json"),

    APPLICATION_X_SOME_OTHER_TYPE_JSON_VERSION_2("application/x.some-other-type+json;version=2");
}
```


Patch:
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 7146efd..419bb56 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -38,6 +38,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.mappingKeys
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toMapValueClassName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
+import com.cjbooms.fabrikt.util.NormalisedString.toEnumName
 import com.cjbooms.fabrikt.util.NormalisedString.toModelClassName
 import com.reprezen.jsonoverlay.Overlay
 import com.reprezen.kaizen.oasparser.OpenApi3Parser
@@ -289,7 +290,7 @@ class JacksonModelGenerator(
             .addMicronautIntrospectedAnnotation()
         enum.entries.forEach {
             classBuilder.addEnumConstant(
-                it.toUpperCase(),
+                it.toEnumName(),
                 TypeSpec.anonymousClassBuilder()
                     .addSuperclassConstructorParameter(CodeBlock.of("\"$it\""))
                     .build()
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index d94c400..abc9257 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -75,9 +75,9 @@ object KaizenParserExtensions {
 
     @Suppress("UNCHECKED_CAST")
     fun Schema.getEnumValues(): List<String> = when {
-        this.hasEnums() -> this.enums.map { it.toString() }
+        this.hasEnums() -> this.enums.map { it.toString() }.filterNot { it.isBlank() }
         !MutableSettings.modelOptions().contains(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS) -> emptyList()
-        else -> extensions[EXTENSIBLE_ENUM_KEY]?.let { it as List<String> } ?: emptyList()
+        else -> extensions[EXTENSIBLE_ENUM_KEY]?.let { it as List<String?> }?.filterNotNull()?.filterNot { it.isBlank() } ?: emptyList()
     }
 
     fun Schema.hasAdditionalProperties(): Boolean = isObjectType() && Overlay.of(additionalPropertiesSchema).isPresent
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt
index 3b63623..78dfdcf 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt
@@ -5,26 +5,32 @@ import javax.lang.model.SourceVersion
 object NormalisedString {
 
     fun String.pascalCase(): String =
-        Regex("[^A-Za-z]")
-            .replace(this, "_")
+        replaceSpecialCharacters()
             .split("_")
             .joinToString("") { it.capitalize() }
 
+    private fun String.replaceSpecialCharacters() =
+        Regex("[^A-Za-z0-9]").replace(this, "_")
+
+    private fun String.camelToSnake() =
+        Regex("[a-z][A-Z]")
+            .replace(this) { pair ->
+                val characters = pair.value.toCharArray()
+                "${characters[0]}_${characters[1]}"
+            }
+
     fun String.camelCase(): String = this.pascalCase().decapitalize()
 
     fun String.toModelClassName(parentModelName: String = ""): String = parentModelName + this.pascalCase()
 
     fun String.toMapValueClassName(): String = "${this.pascalCase()}Value"
 
-    fun String.toKotlinParameterName(): String = this.camelCase()
+    fun String.toEnumName(): String =
+        replaceSpecialCharacters()
+            .camelToSnake()
+            .toUpperCase()
 
-    fun String.camelToSnakeCase(): String =
-        Regex("[A-Z]").replace(this.camelCase()) {
-            "_${it.value.toLowerCase()}"
-        }
-
-    fun String.abbreviate() =
-        this.camelToSnakeCase().split("_").joinToString("_") { it.take(2) }.pascalCase()
+    fun String.toKotlinParameterName(): String = this.camelCase()
 
     fun String.isValidJavaPackage(): Boolean {
         val pieces = split(".")


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/util/NormalisedStringTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/util/NormalisedStringTest.kt
index d20425b..069cd6b 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/util/NormalisedStringTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/util/NormalisedStringTest.kt
@@ -1,10 +1,9 @@
 package com.cjbooms.fabrikt.util
 
-import com.cjbooms.fabrikt.util.NormalisedString.abbreviate
 import com.cjbooms.fabrikt.util.NormalisedString.camelCase
-import com.cjbooms.fabrikt.util.NormalisedString.camelToSnakeCase
 import com.cjbooms.fabrikt.util.NormalisedString.isValidJavaPackage
 import com.cjbooms.fabrikt.util.NormalisedString.pascalCase
+import com.cjbooms.fabrikt.util.NormalisedString.toEnumName
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
 
@@ -22,36 +21,32 @@ class NormalisedStringTest {
 
     @Test
     fun `should transform string with other random characters to pascal case`() {
-        assertThat("abc%def3g".camelCase()).isEqualTo("abcDefG")
-    }
-
-    @Test
-    fun `should convert from camel to snake case`() {
-        assertThat("camelCase".camelToSnakeCase()).isEqualTo("camel_case")
+        assertThat("abc%def3g".camelCase()).isEqualTo("abcDef3g")
     }
 
     @Test
     fun `isValidJavaPackage should return false on a hyphenated-name`() {
-        assertThat("hyphenated-name".isValidJavaPackage()).isFalse()
+        assertThat("hyphenated-name".isValidJavaPackage()).isFalse
     }
 
     @Test
     fun `isValidJavaPackage should return false when a section begins with a digit`() {
-        assertThat("123name".isValidJavaPackage()).isFalse()
+        assertThat("123name".isValidJavaPackage()).isFalse
     }
 
     @Test
     fun `isValidJavaPackage should return false when there is an upper-case letter present`() {
-        assertThat("myPackage".isValidJavaPackage()).isFalse()
+        assertThat("myPackage".isValidJavaPackage()).isFalse
     }
 
     @Test
     fun `isValidJavaPackage should return true for a simple valid package`() {
-        assertThat("com.cjbooms.fabrikt".isValidJavaPackage()).isTrue()
+        assertThat("com.cjbooms.fabrikt".isValidJavaPackage()).isTrue
     }
 
     @Test
-    fun `should abbreviate pascal cased string `() {
-        assertThat("GrandParentObjectParentObject".abbreviate()).isEqualTo("GrPaObPaOb")
+    fun `toEnumName should return an upper snake case enum with no special characters`() {
+        assertThat("PascalCase_enumWith-special/characters.json".toEnumName())
+            .isEqualTo("PASCAL_CASE_ENUM_WITH_SPECIAL_CHARACTERS_JSON")
     }
 }
diff --git a/src/test/resources/examples/enumExamples/api.yaml b/src/test/resources/examples/enumExamples/api.yaml
index a16e1af..341039e 100644
--- a/src/test/resources/examples/enumExamples/api.yaml
+++ b/src/test/resources/examples/enumExamples/api.yaml
@@ -44,5 +44,12 @@ components:
       x-extensible-enum:
         - active
         - inactive
+        - 
+    ContentType:
+      type: string
+      x-extensible-enum:
+        - application/json
+        - application/x.some-type+json    
+        - application/x.some-other-type+json;version=2    
 
 
diff --git a/src/test/resources/examples/enumExamples/models/Models.kt b/src/test/resources/examples/enumExamples/models/Models.kt
index 673aa9b..8be593b 100644
--- a/src/test/resources/examples/enumExamples/models/Models.kt
+++ b/src/test/resources/examples/enumExamples/models/Models.kt
@@ -73,3 +73,14 @@ enum class ExtensibleEnumObject(
 
     INACTIVE("inactive");
 }
+
+enum class ContentType(
+    @JsonValue
+    val value: String
+) {
+    APPLICATION_JSON("application/json"),
+
+    APPLICATION_X_SOME_TYPE_JSON("application/x.some-type+json"),
+
+    APPLICATION_X_SOME_OTHER_TYPE_JSON_VERSION_2("application/x.some-other-type+json;version=2");
+}
diff --git a/src/test/resources/examples/okHttpClient/client/ApiClient.kt b/src/test/resources/examples/okHttpClient/client/ApiClient.kt
index 9c2ff48..067f05f 100644
--- a/src/test/resources/examples/okHttpClient/client/ApiClient.kt
+++ b/src/test/resources/examples/okHttpClient/client/ApiClient.kt
@@ -22,7 +22,7 @@ import kotlin.collections.Map
 import kotlin.jvm.Throws
 
 @Suppress("unused")
-class ExamplePathClient(
+class ExamplePath1Client(
     private val objectMapper: ObjectMapper,
     private val baseUrl: String,
     private val client: OkHttpClient
@@ -89,7 +89,14 @@ class ExamplePathClient(
 
         return request.execute(client, objectMapper, jacksonTypeRef())
     }
+}
 
+@Suppress("unused")
+class ExamplePath2Client(
+    private val objectMapper: ObjectMapper,
+    private val baseUrl: String,
+    private val client: OkHttpClient
+) {
     /**
      * GET example path 2
      *
@@ -196,7 +203,7 @@ class ExamplePathClient(
 }
 
 @Suppress("unused")
-class ExamplePathSubresourceClient(
+class ExamplePath3SubresourceClient(
     private val objectMapper: ObjectMapper,
     private val baseUrl: String,
     private val client: OkHttpClient
diff --git a/src/test/resources/examples/okHttpClient/client/ApiService.kt b/src/test/resources/examples/okHttpClient/client/ApiService.kt
index a7597b3..a5149b9 100644
--- a/src/test/resources/examples/okHttpClient/client/ApiService.kt
+++ b/src/test/resources/examples/okHttpClient/client/ApiService.kt
@@ -24,15 +24,15 @@ import kotlin.jvm.Throws
  * @see ApiServerException
  */
 @Suppress("unused")
-class ExamplePathService(
+class ExamplePath1Service(
     private val circuitBreakerRegistry: CircuitBreakerRegistry,
     objectMapper: ObjectMapper,
     baseUrl: String,
     client: OkHttpClient
 ) {
-    var circuitBreakerName: String = "examplePathClient"
+    var circuitBreakerName: String = "examplePath1Client"
 
-    private val apiClient: ExamplePathClient = ExamplePathClient(objectMapper, baseUrl, client)
+    private val apiClient: ExamplePath1Client = ExamplePath1Client(objectMapper, baseUrl, client)
 
     @Throws(ApiException::class)
     fun getExamplePath1(
@@ -53,6 +53,26 @@ class ExamplePathService(
         withCircuitBreaker(circuitBreakerRegistry, circuitBreakerName) {
             apiClient.postExamplePath1(generatedType, explodeListQueryParam, additionalHeaders)
         }
+}
+
+/**
+ * The circuit breaker registry should have the proper configuration to correctly action on circuit
+ * breaker transitions based on the client exceptions [ApiClientException], [ApiServerException] and
+ * [IOException].
+ *
+ * @see ApiClientException
+ * @see ApiServerException
+ */
+@Suppress("unused")
+class ExamplePath2Service(
+    private val circuitBreakerRegistry: CircuitBreakerRegistry,
+    objectMapper: ObjectMapper,
+    baseUrl: String,
+    client: OkHttpClient
+) {
+    var circuitBreakerName: String = "examplePath2Client"
+
+    private val apiClient: ExamplePath2Client = ExamplePath2Client(objectMapper, baseUrl, client)
 
     @Throws(ApiException::class)
     fun getExamplePath2PathParam(
@@ -97,15 +117,15 @@ class ExamplePathService(
  * @see ApiServerException
  */
 @Suppress("unused")
-class ExamplePathSubresourceService(
+class ExamplePath3SubresourceService(
     private val circuitBreakerRegistry: CircuitBreakerRegistry,
     objectMapper: ObjectMapper,
     baseUrl: String,
     client: OkHttpClient
 ) {
-    var circuitBreakerName: String = "examplePathSubresourceClient"
+    var circuitBreakerName: String = "examplePath3SubresourceClient"
 
-    private val apiClient: ExamplePathSubresourceClient = ExamplePathSubresourceClient(
+    private val apiClient: ExamplePath3SubresourceClient = ExamplePath3SubresourceClient(
         objectMapper,
         baseUrl,
         client
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
index f9ccf28..47251d1 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
@@ -16,7 +16,7 @@ import kotlin.collections.Map
 import kotlin.jvm.Throws
 
 @Suppress("unused")
-class ExamplePathClient(
+class ExamplePath1Client(
     private val objectMapper: ObjectMapper,
     private val baseUrl: String,
     private val client: OkHttpClient
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
index f2ba9c9..8e7739f 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
@@ -20,15 +20,15 @@ import kotlin.jvm.Throws
  * @see ApiServerException
  */
 @Suppress("unused")
-class ExamplePathService(
+class ExamplePath1Service(
     private val circuitBreakerRegistry: CircuitBreakerRegistry,
     objectMapper: ObjectMapper,
     baseUrl: String,
     client: OkHttpClient
 ) {
-    var circuitBreakerName: String = "examplePathClient"
+    var circuitBreakerName: String = "examplePath1Client"
 
-    private val apiClient: ExamplePathClient = ExamplePathClient(objectMapper, baseUrl, client)
+    private val apiClient: ExamplePath1Client = ExamplePath1Client(objectMapper, baseUrl, client)
 
     @Throws(ApiException::class)
     fun getExamplePath1(