diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d49e842d..8a5f1d2b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.5.1] - August 12th, 2021 + +### Changed + +- Fixed bug where polymorphic types were not being rendered correctly when part of collections and maps + ## [1.5.0] - July 25th, 2021 ### Changed diff --git a/gradle.properties b/gradle.properties index bd890282d..776e862e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=1.5.0 +project.version=1.5.1 # Kotlin kotlin.code.style=official # Gradle diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/KompendiumPreFlight.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/KompendiumPreFlight.kt index 4a50305f0..cf028c319 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/KompendiumPreFlight.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/KompendiumPreFlight.kt @@ -49,23 +49,8 @@ object KompendiumPreFlight { } fun addToCache(paramType: KType, requestType: KType, responseType: KType) { - gatherSubTypes(requestType).forEach { - Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache) - } - gatherSubTypes(responseType).forEach { - Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache) - } + Kompendium.cache = Kontent.generateKontent(requestType, Kompendium.cache) + Kompendium.cache = Kontent.generateKontent(responseType, Kompendium.cache) Kompendium.cache = Kontent.generateParameterKontent(paramType, Kompendium.cache) } - - private fun gatherSubTypes(type: KType): List { - val classifier = type.classifier as KClass<*> - return if (classifier.isSealed) { - classifier.sealedSubclasses.map { - it.createType(type.arguments) - } - } else { - listOf(type) - } - } } 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 7589f72ea..9ea146da9 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt @@ -57,7 +57,22 @@ object Kontent { type: KType, cache: SchemaMap = emptyMap() ): SchemaMap { - return generateKTypeKontent(type, cache) + var newCache = cache + gatherSubTypes(type).forEach { + newCache = generateKTypeKontent(it, newCache) + } + return newCache + } + + private fun gatherSubTypes(type: KType): List { + val classifier = type.classifier as KClass<*> + return if (classifier.isSealed) { + classifier.sealedSubclasses.map { + it.createType(type.arguments) + } + } else { + listOf(type) + } } /** @@ -220,11 +235,20 @@ object Kontent { if (keyType?.classifier != String::class) { error("Invalid Map $type: OpenAPI dictionaries must have keys of type String") } - val valClassName = (valType?.classifier as KClass<*>).simpleName + val valClass = valType?.classifier as KClass<*> + val valClassName = valClass.simpleName val referenceName = genericNameAdapter(type, clazz) - val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName") + val valueReference = when(valClass.isSealed) { + true -> { + val subTypes = gatherSubTypes(valType) + AnyOfReferencedSchema(subTypes.map { + ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}")) + }) + } + false -> ReferencedSchema("$COMPONENT_SLUG/$valClassName") + } val schema = DictionarySchema(additionalProperties = valueReference) - val updatedCache = generateKTypeKontent(valType, cache) + val updatedCache = generateKontent(valType, cache) return updatedCache.plus(referenceName to schema) } @@ -240,9 +264,17 @@ object Kontent { val collectionClass = collectionType.classifier as KClass<*> logger.debug("Obtained collection class: $collectionClass") val referenceName = genericNameAdapter(type, clazz) - val valueReference = ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}") + val valueReference = when (collectionClass.isSealed) { + true -> { + val subTypes = gatherSubTypes(collectionType) + AnyOfReferencedSchema(subTypes.map { + ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}")) + }) + } + false -> ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}") + } val schema = ArraySchema(items = valueReference) - val updatedCache = generateKTypeKontent(collectionType, cache) + val updatedCache = generateKontent(collectionType, cache) return updatedCache.plus(referenceName to schema) } } 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 af3f1b980..42f6d2e85 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt @@ -32,6 +32,9 @@ import io.bkbn.kompendium.util.notarizedGetWithNotarizedException import io.bkbn.kompendium.util.notarizedPostModule import io.bkbn.kompendium.util.notarizedPutModule import io.bkbn.kompendium.util.pathParsingTestModule +import io.bkbn.kompendium.util.polymorphicCollectionResponse +import io.bkbn.kompendium.util.polymorphicInterfaceResponse +import io.bkbn.kompendium.util.polymorphicMapResponse import io.bkbn.kompendium.util.polymorphicResponse import io.bkbn.kompendium.util.primitives import io.bkbn.kompendium.util.returnsList @@ -457,6 +460,54 @@ internal class KompendiumTest { } } + @Test + fun `Can generate a collection with polymorphic response type`() { + withTestApplication({ + jacksonConfigModule() + docs() + polymorphicCollectionResponse() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("polymorphic_list_response.json").trim() + assertEquals(expected, json, "The received json spec should match the expected content") + } + } + + @Test + fun `Can generate a map with a polymorphic response type`() { + withTestApplication({ + jacksonConfigModule() + docs() + polymorphicMapResponse() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("polymorphic_map_response.json").trim() + assertEquals(expected, json, "The received json spec should match the expected content") + } + } + + @Test + fun `Can generate a polymorphic response from a sealed interface`() { + withTestApplication({ + jacksonConfigModule() + docs() + polymorphicInterfaceResponse() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("sealed_interface_response.json").trim() + assertEquals(expected, json, "The received json spec should match the expected content") + } + } + @Test fun `Can generate a response type with a generic type`() { withTestApplication({ 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 7f14308db..de9c33f61 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 @@ -84,6 +84,12 @@ sealed class FlibbityGibbit data class SimpleGibbit(val a: String) : FlibbityGibbit() data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit() +sealed interface SlammaJamma + +data class OneJamma(val a: Int) : SlammaJamma +data class AnothaJamma(val b: Float) : SlammaJamma +//data class InsaneJamma(val c: SlammaJamma) : SlammaJamma // 👀 + sealed interface Flibbity data class Gibbity(val a: T): Flibbity 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 e6792797a..abf165191 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 @@ -285,6 +285,36 @@ fun Application.polymorphicResponse() { } } +fun Application.polymorphicCollectionResponse() { + routing { + route("/test/polymorphiclist") { + notarizedGet(TestResponseInfo.polymorphicListResponse) { + call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi"))) + } + } + } +} + +fun Application.polymorphicMapResponse() { + routing { + route("/test/polymorphicmap") { + notarizedGet(TestResponseInfo.polymorphicMapResponse) { + call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi"))) + } + } + } +} + +fun Application.polymorphicInterfaceResponse() { + routing { + route("/test/polymorphicmap") { + notarizedGet(TestResponseInfo.polymorphicInterfaceResponse) { + call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi"))) + } + } + } +} + fun Application.genericPolymorphicResponse() { 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 579c57d9c..7b46adbd3 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 @@ -78,6 +78,21 @@ object TestResponseInfo { description = "Polymorphic response", responseInfo = simpleOkResponse() ) + val polymorphicListResponse = GetInfo>( + summary = "Oh so many gibbits", + description = "Polymorphic list response", + responseInfo = simpleOkResponse() + ) + val polymorphicMapResponse = GetInfo>( + summary = "By gawd that's a lot of gibbits", + description = "Polymorphic list response", + responseInfo = simpleOkResponse() + ) + val polymorphicInterfaceResponse = GetInfo( + summary = "Come on and slam", + description = "and welcome to the jam", + responseInfo = simpleOkResponse() + ) val genericPolymorphicResponse = GetInfo>( summary = "More flibbity", description = "Polymorphic with generics", diff --git a/kompendium-core/src/test/resources/polymorphic_list_response.json b/kompendium-core/src/test/resources/polymorphic_list_response.json new file mode 100644 index 000000000..62feaafc5 --- /dev/null +++ b/kompendium-core/src/test/resources/polymorphic_list_response.json @@ -0,0 +1,91 @@ +{ + "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/polymorphiclist" : { + "get" : { + "tags" : [ ], + "summary" : "Oh so many gibbits", + "description" : "Polymorphic list response", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/List-FlibbityGibbit" + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "String" : { + "type" : "string" + }, + "SimpleGibbit" : { + "type" : "object", + "properties" : { + "a" : { + "$ref" : "#/components/schemas/String" + } + } + }, + "Int" : { + "type" : "integer", + "format" : "int32" + }, + "ComplexGibbit" : { + "type" : "object", + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "c" : { + "$ref" : "#/components/schemas/Int" + } + } + }, + "List-FlibbityGibbit" : { + "type" : "array", + "items" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/SimpleGibbit" + }, { + "$ref" : "#/components/schemas/ComplexGibbit" + } ] + } + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +} diff --git a/kompendium-core/src/test/resources/polymorphic_map_response.json b/kompendium-core/src/test/resources/polymorphic_map_response.json new file mode 100644 index 000000000..88c4dfb03 --- /dev/null +++ b/kompendium-core/src/test/resources/polymorphic_map_response.json @@ -0,0 +1,91 @@ +{ + "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/polymorphicmap" : { + "get" : { + "tags" : [ ], + "summary" : "By gawd that's a lot of gibbits", + "description" : "Polymorphic list response", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Map-String-FlibbityGibbit" + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "String" : { + "type" : "string" + }, + "SimpleGibbit" : { + "type" : "object", + "properties" : { + "a" : { + "$ref" : "#/components/schemas/String" + } + } + }, + "Int" : { + "type" : "integer", + "format" : "int32" + }, + "ComplexGibbit" : { + "type" : "object", + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "c" : { + "$ref" : "#/components/schemas/Int" + } + } + }, + "Map-String-FlibbityGibbit" : { + "type" : "object", + "additionalProperties" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/SimpleGibbit" + }, { + "$ref" : "#/components/schemas/ComplexGibbit" + } ] + } + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +} diff --git a/kompendium-core/src/test/resources/sealed_interface_response.json b/kompendium-core/src/test/resources/sealed_interface_response.json new file mode 100644 index 000000000..3a6f375b2 --- /dev/null +++ b/kompendium-core/src/test/resources/sealed_interface_response.json @@ -0,0 +1,83 @@ +{ + "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/polymorphicmap" : { + "get" : { + "tags" : [ ], + "summary" : "Come on and slam", + "description" : "and welcome to the jam", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/OneJamma" + }, { + "$ref" : "#/components/schemas/AnothaJamma" + } ] + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "Int" : { + "type" : "integer", + "format" : "int32" + }, + "OneJamma" : { + "type" : "object", + "properties" : { + "a" : { + "$ref" : "#/components/schemas/Int" + } + } + }, + "Float" : { + "type" : "number", + "format" : "float" + }, + "AnothaJamma" : { + "type" : "object", + "properties" : { + "b" : { + "$ref" : "#/components/schemas/Float" + } + } + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +}