From 6ba3617e3216265de5df2d1f0d57c50118c4f731 Mon Sep 17 00:00:00 2001 From: Ryan Brink <5607577+rgbrizzlehizzle@users.noreply.github.com> Date: Sat, 14 Aug 2021 21:02:23 -0400 Subject: [PATCH] add super hack to support undeclared polymorphic adapter fields (#84) --- CHANGELOG.md | 8 +- README.md | 14 +++- gradle.properties | 2 +- .../main/kotlin/io/bkbn/kompendium/Kontent.kt | 12 ++- .../kompendium/annotations/UndeclaredField.kt | 8 ++ .../io/bkbn/kompendium/KompendiumTest.kt | 17 +++++ .../io/bkbn/kompendium/util/TestModels.kt | 9 +++ .../io/bkbn/kompendium/util/TestModules.kt | 10 +++ .../bkbn/kompendium/util/TestResponseInfo.kt | 5 ++ .../src/test/resources/undeclared_field.json | 73 +++++++++++++++++++ .../io/bkbn/kompendium/playground/Main.kt | 6 ++ .../io/bkbn/kompendium/playground/Models.kt | 9 +++ .../kompendium/playground/PlaygroundToC.kt | 9 +++ 13 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 kompendium-core/src/main/kotlin/io/bkbn/kompendium/annotations/UndeclaredField.kt create mode 100644 kompendium-core/src/test/resources/undeclared_field.json 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().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-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 data class Gibbity(val a: T): Flibbity data class Bibbity(val b: String, val f: T) : Flibbity + +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( + summary = "spooky class", + description = "break this glass in scenario of emergency", + responseInfo = simpleOkResponse() + ) val genericResponse = GetInfo>( 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" : [ ] +} 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( + summary = "Tests adding undeclared fields", + description = "vvv mysterious", + tags = setOf("mysterious"), + responseInfo = ResponseInfo( + status = HttpStatusCode.OK, + description = "good tings" + ) + ) }