add super hack to support undeclared polymorphic adapter fields (#84)
This commit is contained in:
@ -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
|
||||
|
||||
|
14
README.md
14
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 🤠
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=1.6.0
|
||||
project.version=1.7.0
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
|
@ -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 {
|
||||
|
@ -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<*>)
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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") {
|
||||
|
@ -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",
|
||||
|
73
kompendium-core/src/test/resources/undeclared_field.json
Normal file
73
kompendium-core/src/test/resources/undeclared_field.json
Normal file
@ -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" : [ ]
|
||||
}
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user