fix: type erasure breaking nested generics (#285)

This commit is contained in:
Ryan Brink
2022-08-13 11:15:49 -07:00
committed by GitHub
parent 2e29b46f0f
commit 5beeade430
7 changed files with 302 additions and 10 deletions

View File

@ -6,6 +6,7 @@
### Changed ### Changed
- Can now put redoc route behind authentication - Can now put redoc route behind authentication
- Fixed issue where type erasure was breaking nested generics
### Remove ### Remove

View File

@ -16,6 +16,8 @@ import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.TestModules.headerParameter import io.bkbn.kompendium.core.util.TestModules.headerParameter
import io.bkbn.kompendium.core.util.TestModules.multipleExceptions import io.bkbn.kompendium.core.util.TestModules.multipleExceptions
import io.bkbn.kompendium.core.util.TestModules.nestedGenericCollection
import io.bkbn.kompendium.core.util.TestModules.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam
import io.bkbn.kompendium.core.util.TestModules.polymorphicException import io.bkbn.kompendium.core.util.TestModules.polymorphicException
@ -157,6 +159,12 @@ class KompendiumTest : DescribeSpec({
it("Can handle an absolutely psycho inheritance test") { it("Can handle an absolutely psycho inheritance test") {
openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() } openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
} }
it("Can support nested generic collections") {
openApiTestAllSerializers("T0039__nested_generic_collection.json") { nestedGenericCollection() }
}
it("Can support nested generics with multiple type parameters") {
openApiTestAllSerializers("T0040__nested_generic_multiple_type_params.json") { nestedGenericMultipleParamsCollection() }
}
} }
describe("Miscellaneous") { describe("Miscellaneous") {
xit("Can generate the necessary ReDoc home page") { xit("Can generate the necessary ReDoc home page") {

View File

@ -8,8 +8,10 @@ import io.bkbn.kompendium.core.fixtures.ExceptionResponse
import io.bkbn.kompendium.core.fixtures.Flibbity import io.bkbn.kompendium.core.fixtures.Flibbity
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.Gibbity import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.NullableEnum import io.bkbn.kompendium.core.fixtures.NullableEnum
import io.bkbn.kompendium.core.fixtures.NullableField import io.bkbn.kompendium.core.fixtures.NullableField
import io.bkbn.kompendium.core.fixtures.Page
import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestNested import io.bkbn.kompendium.core.fixtures.TestNested
@ -567,6 +569,10 @@ object TestModules {
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>() fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude") fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>() fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
@ -575,14 +581,16 @@ object TestModules {
fun Routing.dateTimeString() = basicGetGenerator<DateTimeString>() fun Routing.dateTimeString() = basicGetGenerator<DateTimeString>()
fun Routing.headerParameter() = basicGetGenerator<TestResponse>( params = listOf( fun Routing.headerParameter() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter( Parameter(
name = "X-User-Email", name = "X-User-Email",
`in` = Parameter.Location.header, `in` = Parameter.Location.header,
schema = TypeDefinition.STRING, schema = TypeDefinition.STRING,
required = true required = true
) )
)) )
)
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>() fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()

View File

@ -0,0 +1,101 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"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": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Page-Int"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Page-Int": {
"type": "object",
"properties": {
"content": {
"items": {
"type": "number",
"format": "int32"
},
"type": "array"
},
"number": {
"type": "number",
"format": "int32"
},
"numberOfElements": {
"type": "number",
"format": "int32"
},
"size": {
"type": "number",
"format": "int32"
},
"totalElements": {
"type": "number",
"format": "int64"
},
"totalPages": {
"type": "number",
"format": "int32"
}
},
"required": [
"content",
"number",
"numberOfElements",
"size",
"totalElements",
"totalPages"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,129 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"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": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiNestedGenerics-String-ComplexRequest"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"enum": [
"ONE",
"TWO"
]
}
},
"required": [
"enumeration"
]
},
"NestedComplexItem": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string"
}
},
"required": [
"alias",
"name"
]
},
"ComplexRequest": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem"
},
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
},
"MultiNestedGenerics-String-ComplexRequest": {
"type": "object",
"properties": {
"content": {
"additionalProperties": {
"$ref": "#/components/schemas/ComplexRequest"
},
"type": "object"
}
},
"required": [
"content"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -118,3 +118,16 @@ public data class ProfileMetadataUpdateRequest(
public val isPrivate: Boolean?, public val isPrivate: Boolean?,
public val otherThing: String? public val otherThing: String?
) )
data class Page<T>(
val content: List<T>,
val totalElements: Long,
val totalPages: Int,
val numberOfElements: Int,
val number: Int,
val size: Int
)
data class MultiNestedGenerics<T, E>(
val content: Map<T, E>
)

View File

@ -13,6 +13,7 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.KTypeParameter import kotlin.reflect.KTypeParameter
import kotlin.reflect.KTypeProjection import kotlin.reflect.KTypeProjection
import kotlin.reflect.full.createType
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.primaryConstructor
@ -22,10 +23,13 @@ object SimpleObjectHandler {
// cache[type.getSimpleSlug()] = ReferenceDefinition("RECURSION_PLACEHOLDER") // cache[type.getSimpleSlug()] = ReferenceDefinition("RECURSION_PLACEHOLDER")
val typeMap = clazz.typeParameters.zip(type.arguments).toMap() val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
val props = clazz.memberProperties.associate { prop -> val props = clazz.memberProperties.associate { prop ->
val schema = when (typeMap.containsKey(prop.returnType.classifier)) { val schema = when (prop.needsToInjectGenerics(typeMap)) {
true -> handleNestedGenerics(typeMap, prop, cache)
false -> when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(prop, typeMap, cache) true -> handleGenericProperty(prop, typeMap, cache)
false -> handleProperty(prop, cache) false -> handleProperty(prop, cache)
} }
}
prop.name to schema prop.name to schema
} }
@ -48,6 +52,34 @@ object SimpleObjectHandler {
} }
} }
private fun KProperty<*>.needsToInjectGenerics(
typeMap: Map<KTypeParameter, KTypeProjection>
): Boolean {
val typeSymbols = returnType.arguments.map { it.type.toString() }
return typeMap.any { (k, _) -> typeSymbols.contains(k.name) }
}
private fun handleNestedGenerics(
typeMap: Map<KTypeParameter, KTypeProjection>,
prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>
): JsonSchema {
val propClass = prop.returnType.classifier as KClass<*>
val types = prop.returnType.arguments.map {
val typeSymbol = it.type.toString()
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
}
val constructedType = propClass.createType(types)
return SchemaGenerator.fromTypeToSchema(constructedType, cache).let {
if (it is TypeDefinition && it.type == "object") {
cache[constructedType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug())
} else {
it
}
}
}
private fun handleGenericProperty( private fun handleGenericProperty(
prop: KProperty<*>, prop: KProperty<*>,
typeMap: Map<KTypeParameter, KTypeProjection>, typeMap: Map<KTypeParameter, KTypeProjection>,