fix: type erasure breaking nested generics (#285)
This commit is contained in:
@ -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
|
||||||
|
|
||||||
|
@ -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") {
|
||||||
|
@ -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>()
|
||||||
|
|
||||||
|
101
core/src/test/resources/T0039__nested_generic_collection.json
Normal file
101
core/src/test/resources/T0039__nested_generic_collection.json
Normal 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": []
|
||||||
|
}
|
@ -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": []
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
@ -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>,
|
||||||
|
Reference in New Issue
Block a user