Repository: bkbnio/kompendium
Pull request #84 (2021-08-15 00:59:55)

add super hack to support undeclared polymorphic adapter fields
# Description

Adds in a super hacky approach to allowing a singular undeclared field for a provided class.  Super niche, should only be used in case of emergency (like undeclared polymorphic adapter fields 😭 )

Closes # (issue)

## Type of change

- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

Added tests and confirmed in playground 

# Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated the CHANGELOG and bumped the version
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2324d199f..34394a741 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
 # Changelog
 
-## [1.6.0] - August 12, 2021
+## [1.7.0] - August 14th, 2021
+
+### Added
+
+- Added ability to inject an emergency `UndeclaredField` in the event of certain polymorphic serializers and such
+
+## [1.6.0] - August 12th, 2021
 
 ### Added
 
diff --git a/README.md b/README.md
index f3aa62f31..20329c3ee 100644
--- a/README.md
+++ b/README.md
@@ -96,9 +96,21 @@ The intended purpose of `KompendiumField` is to offer field level overrides such
 The purpose of `KompendiumParam` is to provide supplemental information needed to properly assign the type of parameter 
 (cookie, header, query, path) as well as other parameter-level metadata. 
 
+### Undeclared Field
+
+There is also a final `UndeclaredField` annotation.  This should be used only in an absolutely emergency.  This annotation
+will allow you to inject a _single_ undeclared field that will be included as part of the schema. 
+
+Due to limitations in using repeated annotations, this can only be used once per class
+
+This is a complete hack, and is included for odd scenarios like kotlinx serialization polymorphic adapters that expect a 
+`type` field in order to perform their analysis.  
+
+Use this _only_ when **all** else fails
+
 ### Polymorphism
 
-Out of the box, Kompendium has support for sealed classes. At runtime, it will build a mapping of all available sub-classes
+Speaking of polymorphism... out of the box, Kompendium has support for sealed classes and interfaces. At runtime, it will build a mapping of all available sub-classes
 and build a spec that takes `anyOf` the implementations.  This is currently a weak point of the entire library, and 
 suggestions on better implementations are welcome 🤠
 
diff --git a/gradle.properties b/gradle.properties
index 7d8ca2374..f62ad89b3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
 # Kompendium
-project.version=1.6.0
+project.version=1.7.0
 # Kotlin
 kotlin.code.style=official
 # Gradle
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt
index 9ea146da9..76059f24d 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt
@@ -1,5 +1,6 @@
 package io.bkbn.kompendium
 
+import io.bkbn.kompendium.annotations.UndeclaredField
 import io.bkbn.kompendium.models.meta.SchemaMap
 import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
 import io.bkbn.kompendium.models.oas.ArraySchema
@@ -7,6 +8,7 @@ import io.bkbn.kompendium.models.oas.DictionarySchema
 import io.bkbn.kompendium.models.oas.EnumSchema
 import io.bkbn.kompendium.models.oas.FormatSchema
 import io.bkbn.kompendium.models.oas.ObjectSchema
+import io.bkbn.kompendium.models.oas.OpenApiSpecComponentSchema
 import io.bkbn.kompendium.models.oas.ReferencedSchema
 import io.bkbn.kompendium.models.oas.SimpleSchema
 import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG
@@ -204,8 +206,14 @@ object Kontent {
           }
           Pair(prop.name, propSchema)
         }
+        logger.debug("Looking for undeclared fields")
+        val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
+          val undeclaredType = it.clazz.createType()
+          newCache = generateKontent(undeclaredType, newCache)
+          it.field to ReferencedSchema(undeclaredType.getReferenceSlug())
+        }
         logger.debug("$slug contains $fieldMap")
-        val schema = ObjectSchema(fieldMap)
+        val schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
         logger.debug("$slug schema: $schema")
         newCache.plus(slug to schema)
       }
@@ -238,7 +246,7 @@ object Kontent {
     val valClass = valType?.classifier as KClass<*>
     val valClassName = valClass.simpleName
     val referenceName = genericNameAdapter(type, clazz)
-    val valueReference = when(valClass.isSealed) {
+    val valueReference = when (valClass.isSealed) {
       true -> {
         val subTypes = gatherSubTypes(valType)
         AnyOfReferencedSchema(subTypes.map {
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/annotations/UndeclaredField.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/annotations/UndeclaredField.kt
new file mode 100644
index 000000000..692aa5b1b
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/annotations/UndeclaredField.kt
@@ -0,0 +1,8 @@
+package io.bkbn.kompendium.annotations
+
+import kotlin.reflect.KClass
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+@Repeatable
+annotation class UndeclaredField(val field: String, val clazz: KClass<*>)
diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Main.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Main.kt
index ce17269e0..96128a94e 100644
--- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Main.kt
+++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Main.kt
@@ -19,6 +19,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfo
 import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable
 import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo
 import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo
+import io.bkbn.kompendium.playground.PlaygroundToC.testUndeclaredFields
 import io.bkbn.kompendium.routes.openApi
 import io.bkbn.kompendium.routes.redoc
 import io.bkbn.kompendium.swagger.swaggerUI
@@ -138,5 +139,10 @@ fun Application.mainModule() {
         error("bad things just happened")
       }
     }
+    route("/undeclared") {
+      notarizedGet(testUndeclaredFields) {
+        call.respondText { "hi" }
+      }
+    }
   }
 }
diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Models.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Models.kt
index ae8ad6496..d677d5050 100644
--- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Models.kt
+++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/Models.kt
@@ -3,6 +3,7 @@ package io.bkbn.kompendium.playground
 import io.bkbn.kompendium.annotations.KompendiumField
 import io.bkbn.kompendium.annotations.KompendiumParam
 import io.bkbn.kompendium.annotations.ParamType
+import io.bkbn.kompendium.annotations.UndeclaredField
 import org.joda.time.DateTime
 
 data class ExampleParams(
@@ -33,3 +34,11 @@ data class ExceptionResponse(val message: String)
 data class ExampleCreatedResponse(val id: Int, val c: String)
 
 data class DateTimeWrapper(val dt: DateTime)
+
+enum class Testerino {
+  First,
+  Second
+}
+
+@UndeclaredField("type", Testerino::class)
+data class SimpleYetMysterious(val exists: Boolean)
diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PlaygroundToC.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PlaygroundToC.kt
index 745bcef5b..50bcb51af 100644
--- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PlaygroundToC.kt
+++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PlaygroundToC.kt
@@ -109,4 +109,13 @@ object PlaygroundToC {
     ),
     securitySchemes = setOf("basic")
   )
+  val testUndeclaredFields = MethodInfo.GetInfo<Unit, SimpleYetMysterious>(
+    summary = "Tests adding undeclared fields",
+    description = "vvv mysterious",
+    tags = setOf("mysterious"),
+    responseInfo = ResponseInfo(
+      status = HttpStatusCode.OK,
+      description = "good tings"
+    )
+  )
 }


Test patch:
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt
index 42f6d2e85..80b33f62b 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt
@@ -43,6 +43,7 @@ import io.bkbn.kompendium.util.simpleGenericResponse
 import io.bkbn.kompendium.util.statusPageModule
 import io.bkbn.kompendium.util.statusPageMultiExceptions
 import io.bkbn.kompendium.util.trailingSlash
+import io.bkbn.kompendium.util.undeclaredType
 import io.bkbn.kompendium.util.withDefaultParameter
 import io.bkbn.kompendium.util.withExamples
 
@@ -556,6 +557,22 @@ internal class KompendiumTest {
     }
   }
 
+  @Test
+  fun `Can add an undeclared field`() {
+    withTestApplication({
+      kotlinxConfigModule()
+      docs()
+      undeclaredType()
+    }) {
+      // do
+      val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
+
+      // expect
+      val expected = getFileSnapshot("undeclared_field.json").trim()
+      assertEquals(expected, json, "The received json spec should match the expected content")
+    }
+  }
+
   private val oas = Kompendium.openApiSpec.copy(
     info = OpenApiSpecInfo(
       title = "Test API",
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModels.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModels.kt
index de9c33f61..3eab02493 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModels.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModels.kt
@@ -4,6 +4,7 @@ import java.util.UUID
 import io.bkbn.kompendium.annotations.KompendiumField
 import io.bkbn.kompendium.annotations.KompendiumParam
 import io.bkbn.kompendium.annotations.ParamType
+import io.bkbn.kompendium.annotations.UndeclaredField
 import java.math.BigDecimal
 import java.math.BigInteger
 
@@ -94,3 +95,11 @@ sealed interface Flibbity<T>
 
 data class Gibbity<T>(val a: T): Flibbity<T>
 data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>
+
+enum class Hehe {
+  HAHA,
+  HOHO
+}
+
+@UndeclaredField("nowYouDont", Hehe::class)
+data class Mysterious(val nowYouSeeMe: String)
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt
index abf165191..89d6d59d1 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt
@@ -340,6 +340,16 @@ fun Application.genericPolymorphicResponseMultipleImpls() {
   }
 }
 
+fun Application.undeclaredType() {
+  routing {
+    route("/test/polymorphic") {
+      notarizedGet(TestResponseInfo.undeclaredResponseType) {
+        call.respond(HttpStatusCode.OK, Mysterious("hi"))
+      }
+    }
+  }
+}
+
 fun Application.simpleGenericResponse() {
   routing {
     route("/test/polymorphic") {
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestResponseInfo.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestResponseInfo.kt
index 7b46adbd3..3e1f1a838 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestResponseInfo.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestResponseInfo.kt
@@ -103,6 +103,11 @@ object TestResponseInfo {
     description = "Polymorphic with generics but like... crazier",
     responseInfo = simpleOkResponse()
   )
+  val undeclaredResponseType = GetInfo<Unit, Mysterious>(
+    summary = "spooky class",
+    description = "break this glass in scenario of emergency",
+    responseInfo = simpleOkResponse()
+  )
   val genericResponse = GetInfo<Unit, TestGeneric<Int>>(
     summary = "Single Generic",
     description = "Simple generic data class",
diff --git a/kompendium-core/src/test/resources/undeclared_field.json b/kompendium-core/src/test/resources/undeclared_field.json
new file mode 100644
index 000000000..65c24e1bf
--- /dev/null
+++ b/kompendium-core/src/test/resources/undeclared_field.json
@@ -0,0 +1,73 @@
+{
+  "openapi" : "3.0.3",
+  "info" : {
+    "title" : "Test API",
+    "version" : "1.33.7",
+    "description" : "An amazing, fully-ish 😉 generated API spec",
+    "termsOfService" : "https://example.com",
+    "contact" : {
+      "name" : "Homer Simpson",
+      "url" : "https://gph.is/1NPUDiM",
+      "email" : "chunkylover53@aol.com"
+    },
+    "license" : {
+      "name" : "MIT",
+      "url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
+    }
+  },
+  "servers" : [ {
+    "url" : "https://myawesomeapi.com",
+    "description" : "Production instance of my API"
+  }, {
+    "url" : "https://staging.myawesomeapi.com",
+    "description" : "Where the fun stuff happens"
+  } ],
+  "paths" : {
+    "/test/polymorphic" : {
+      "get" : {
+        "tags" : [ ],
+        "summary" : "spooky class",
+        "description" : "break this glass in scenario of emergency",
+        "parameters" : [ ],
+        "responses" : {
+          "200" : {
+            "description" : "A successful endeavor",
+            "content" : {
+              "application/json" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/Mysterious"
+                }
+              }
+            }
+          }
+        },
+        "deprecated" : false
+      }
+    }
+  },
+  "components" : {
+    "schemas" : {
+      "String" : {
+        "type" : "string"
+      },
+      "Hehe" : {
+        "type" : "string",
+        "enum" : [ "HAHA", "HOHO" ]
+      },
+      "Mysterious" : {
+        "type" : "object",
+        "properties" : {
+          "nowYouSeeMe" : {
+            "$ref" : "#/components/schemas/String"
+          },
+          "nowYouDont" : {
+            "$ref" : "#/components/schemas/Hehe"
+          }
+        }
+      }
+    },
+    "securitySchemes" : { }
+  },
+  "security" : [ ],
+  "tags" : [ ]
+}