From 59c0c3aabfef3e6ed35e62e23158b83993f63b00 Mon Sep 17 00:00:00 2001 From: Ryan Brink <5607577+rgbrizzlehizzle@users.noreply.github.com> Date: Fri, 21 May 2021 17:35:19 -0400 Subject: [PATCH] Polymorphic and Generic Support (#59) --- .github/workflows/release.yml | 7 +- CHANGELOG.md | 7 + README.md | 9 +- build.gradle.kts | 31 ++-- gradle.properties | 2 +- .../io/bkbn/kompendium/KompendiumPreFlight.kt | 35 +++- .../main/kotlin/io/bkbn/kompendium/Kontent.kt | 110 ++++++++++-- .../kotlin/io/bkbn/kompendium/MethodParser.kt | 51 ++++-- .../kotlin/io/bkbn/kompendium/Notarized.kt | 8 +- .../models/oas/OpenApiSpecComponentSchema.kt | 2 + .../models/oas/OpenApiSpecMediaType.kt | 2 +- .../models/oas/OpenApiSpecReferencable.kt | 11 +- .../kotlin/io/bkbn/kompendium/util/Helpers.kt | 27 +-- .../io/bkbn/kompendium/KompendiumTest.kt | 67 +++++++ .../io/bkbn/kompendium/util/TestModels.kt | 12 ++ .../io/bkbn/kompendium/util/TestModules.kt | 79 +++++++-- .../bkbn/kompendium/util/TestResponseInfo.kt | 51 ++++-- .../src/test/resources/complex_type.json | 30 ++-- .../resources/crazy_polymorphic_example.json | 164 ++++++++++++++++++ .../test/resources/example_req_and_resp.json | 22 +-- .../src/test/resources/generic_response.json | 73 ++++++++ .../src/test/resources/notarized_post.json | 36 ++-- .../test/resources/notarized_primitives.json | 6 +- .../src/test/resources/notarized_put.json | 36 ++-- .../test/resources/polymorphic_response.json | 85 +++++++++ .../polymorphic_response_with_generics.json | 89 ++++++++++ kompendium-playground/build.gradle.kts | 2 +- .../io/bkbn/kompendium/playground/Main.kt | 70 ++++---- .../io/bkbn/kompendium/playground/Models.kt | 2 + .../kompendium/playground/PlaygroundToC.kt | 2 +- .../io/bkbn/kompendium/swagger/SwaggerUI.kt | 2 +- 31 files changed, 902 insertions(+), 228 deletions(-) create mode 100644 kompendium-core/src/test/resources/crazy_polymorphic_example.json create mode 100644 kompendium-core/src/test/resources/generic_response.json create mode 100644 kompendium-core/src/test/resources/polymorphic_response.json create mode 100644 kompendium-core/src/test/resources/polymorphic_response_with_generics.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3bb7db03..394f33e3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,11 +20,6 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }} restore-keys: ${{ runner.os }}-gradle - name: Publish packages to Github - run: ./gradlew publishAllPublicationsToGithubPackagesRepository -Prelease=true + run: ./gradlew publish -Prelease=true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# - name: Publish packages to Nexus -# run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -Prelease=true -# env: -# SONATYPE_USER: ${{ secrets.SONATYPE_USER }} -# SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fcac567a8..a635dd9a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.1.0] - May 19th, 2021 + +### Added + +- Support for sealed classes 🔥 +- Support for generic classes ☄️ + ## [1.0.1] - May 10th, 2021 ### Changed diff --git a/README.md b/README.md index 7096df4da..e982c203a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ repositories { // 3 Add the package like any normal dependency dependencies { - implementation("org.leafygreens:kompendium-core:1.0.0") + implementation("io.bkbn:kompendium-core:1.0.0") } ``` @@ -76,6 +76,12 @@ 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. +### Polymorphism + +Out of the box, Kompendium has support for sealed classes. 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 🤠 + ## Examples The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example @@ -201,7 +207,6 @@ parity with the OpenAPI feature spec, nor does it have all-of-the nice to have f should have. There are several outstanding features that have been added to the [V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes -- Polymorphic support - AsyncAPI Integration - Field Validation - MavenCentral Release diff --git a/build.gradle.kts b/build.gradle.kts index a7e9ea7a3..677f7374e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,12 @@ +import com.adarshr.gradle.testlogger.theme.ThemeType +import com.adarshr.gradle.testlogger.TestLoggerExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import io.gitlab.arturbosch.detekt.extensions.DetektExtension + plugins { - id("org.jetbrains.kotlin.jvm") version "1.4.32" apply false - id("io.gitlab.arturbosch.detekt") version "1.16.0-RC2" apply false + id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false + id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false id("com.adarshr.test-logger") version "3.0.0" apply false - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true } allprojects { @@ -26,14 +30,14 @@ allprojects { apply(plugin = "com.adarshr.test-logger") apply(plugin = "idea") - tasks.withType().configureEach { + tasks.withType().configureEach { kotlinOptions { jvmTarget = "11" } } - configure { - setTheme("standard") + configure { + theme = ThemeType.MOCHA setLogLevel("lifecycle") showExceptions = true showStackTraces = true @@ -51,8 +55,8 @@ allprojects { showFailedStandardStreams = true } - configure { - toolVersion = "1.16.0-RC2" + configure { + toolVersion = "1.17.0-RC3" config = files("${rootProject.projectDir}/detekt.yml") buildUponDefaultConfig = true } @@ -61,14 +65,3 @@ allprojects { withSourcesJar() } } - -nexusPublishing { - repositories { - sonatype { - username.set(System.getenv("SONATYPE_USER")) - password.set(System.getenv("SONATYPE_PASSWORD")) - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - } - } -} diff --git a/gradle.properties b/gradle.properties index 71832147a..b31c74d36 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=1.0.1 +project.version=1.1.0 # 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 7e729671d..4a50305f0 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/KompendiumPreFlight.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/KompendiumPreFlight.kt @@ -1,7 +1,9 @@ package io.bkbn.kompendium import io.ktor.routing.Route +import kotlin.reflect.KClass import kotlin.reflect.KType +import kotlin.reflect.full.createType import kotlin.reflect.typeOf /** @@ -21,13 +23,11 @@ object KompendiumPreFlight { inline fun methodNotarizationPreFlight( block: (KType, KType, KType) -> Route ): Route { - Kompendium.cache = Kontent.generateKontent(Kompendium.cache) - Kompendium.cache = Kontent.generateKontent(Kompendium.cache) - Kompendium.cache = Kontent.generateParameterKontent(Kompendium.cache) - Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache) val requestType = typeOf() val responseType = typeOf() val paramType = typeOf() + addToCache(paramType, requestType, responseType) + Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache) return block.invoke(paramType, requestType, responseType) } @@ -38,13 +38,34 @@ object KompendiumPreFlight { * @param block The function to execute, provided type information of the parameters above */ @OptIn(ExperimentalStdlibApi::class) - inline fun errorNotarizationPreFlight( + inline fun errorNotarizationPreFlight( block: (KType, KType) -> Unit ) { - Kompendium.cache = Kontent.generateKontent(Kompendium.cache) - Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache) val errorType = typeOf() val responseType = typeOf() + addToCache(typeOf(), typeOf(), responseType) + Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache) return block.invoke(errorType, responseType) } + + 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.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 0ac0045b7..40d3528ec 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kontent.kt @@ -1,13 +1,7 @@ package io.bkbn.kompendium -import java.util.UUID -import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.javaField -import kotlin.reflect.typeOf import io.bkbn.kompendium.models.meta.SchemaMap +import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema import io.bkbn.kompendium.models.oas.ArraySchema import io.bkbn.kompendium.models.oas.DictionarySchema import io.bkbn.kompendium.models.oas.EnumSchema @@ -18,7 +12,16 @@ import io.bkbn.kompendium.models.oas.SimpleSchema import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG import io.bkbn.kompendium.util.Helpers.genericNameAdapter import io.bkbn.kompendium.util.Helpers.getReferenceSlug +import io.bkbn.kompendium.util.Helpers.getSimpleSlug import io.bkbn.kompendium.util.Helpers.logged +import java.util.UUID +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.createType +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaField +import kotlin.reflect.typeOf import org.slf4j.LoggerFactory /** @@ -42,6 +45,19 @@ object Kontent { return generateKTypeKontent(kontentType, cache) } + /** + * Analyzes a [KType] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided + * @param type [KType] to analyze + * @param cache Existing schema map to append to + * @return an updated schema map containing all type information for [KType] type + */ + fun generateKontent( + type: KType, + cache: SchemaMap = emptyMap() + ): SchemaMap { + return generateKTypeKontent(type, cache) + } + /** * Analyze a type [T], but filters out the top-level type * @param T type to analyze @@ -57,6 +73,20 @@ object Kontent { .filterNot { (slug, _) -> slug == (kontentType.classifier as KClass<*>).simpleName } } + /** + * Analyze a type but filters out the top-level type + * @param type to analyze + * @param cache Existing schema map to append to + * @return an updated schema map containing all type information for [T] + */ + fun generateParameterKontent( + type: KType, + cache: SchemaMap = emptyMap() + ): SchemaMap { + return generateKTypeKontent(type, cache) + .filterNot { (slug, _) -> slug == (type.classifier as KClass<*>).simpleName } + } + /** * Recursively fills schema map depending on [KType] classifier * @param type [KType] to parse @@ -80,7 +110,7 @@ object Kontent { clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache) clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache) clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache) - else -> handleComplexType(clazz, cache) + else -> handleComplexType(type, clazz, cache) } } } @@ -90,32 +120,78 @@ object Kontent { * @param clazz Class of the object to analyze * @param cache Existing schema map to append to */ - private fun handleComplexType(clazz: KClass<*>, cache: SchemaMap): SchemaMap = - when (cache.containsKey(clazz.simpleName)) { + private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap { + // This needs to be simple because it will be stored under it's appropriate reference component implicitly + val slug = type.getSimpleSlug() + // Only analyze if component has not already been stored in the cache + return when (cache.containsKey(slug)) { true -> { - logger.debug("Cache already contains ${clazz.simpleName}, returning cache untouched") + logger.debug("Cache already contains $slug, returning cache untouched") cache } false -> { - logger.debug("${clazz.simpleName} was not found in cache, generating now") + logger.debug("$slug was not found in cache, generating now") var newCache = cache + // Grabs any type parameters as a zip with the corresponding type argument + val typeMap = clazz.typeParameters.zip(type.arguments).toMap() + // associates each member with a Pair of prop name to property schema val fieldMap = clazz.memberProperties.associate { prop -> logger.debug("Analyzing $prop in class $clazz") + // Grab the field of the current property val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop") logger.debug("Detected field $field") + // Yoinks any generic types from the type map should the field be a generic + val yoinkBaseType = if (typeMap.containsKey(prop.returnType.classifier)) { + logger.debug("Generic type detected") + typeMap[prop.returnType.classifier]?.type!! + } else { + prop.returnType + } + // converts the base type to a class + val yoinkedClassifier = yoinkBaseType.classifier as KClass<*> + // in the event of a sealed class, grab all sealed subclasses and create a type from the base args + val yoinkedTypes = if (yoinkedClassifier.isSealed) { + yoinkedClassifier.sealedSubclasses.map { it.createType(yoinkBaseType.arguments) } + } else { + listOf(yoinkBaseType) + } + // if the most up-to-date cache does not contain the content for this field, generate it and add to cache if (!newCache.containsKey(field.simpleName)) { logger.debug("Cache was missing ${field.simpleName}, adding now") - newCache = generateKTypeKontent(prop.returnType, newCache) + yoinkedTypes.forEach { + newCache = generateKTypeKontent(it, newCache) + } + } + // TODO This in particular is worthy of a refactor... just not very well written + // builds the appropriate property schema based on the property return type + val propSchema = if (typeMap.containsKey(prop.returnType.classifier)) { + if (yoinkedClassifier.isSealed) { + val refs = yoinkedClassifier.sealedSubclasses + .map { it.createType(yoinkBaseType.arguments) } + .map { ReferencedSchema(it.getReferenceSlug()) } + AnyOfReferencedSchema(refs) + } else { + ReferencedSchema(typeMap[prop.returnType.classifier]?.type!!.getReferenceSlug()) + } + } else { + if (yoinkedClassifier.isSealed) { + val refs = yoinkedClassifier.sealedSubclasses + .map { it.createType(yoinkBaseType.arguments) } + .map { ReferencedSchema(it.getReferenceSlug()) } + AnyOfReferencedSchema(refs) + } else { + ReferencedSchema(field.getReferenceSlug(prop)) + } } - val propSchema = ReferencedSchema(field.getReferenceSlug(prop)) Pair(prop.name, propSchema) } - logger.debug("${clazz.simpleName} contains $fieldMap") + logger.debug("$slug contains $fieldMap") val schema = ObjectSchema(fieldMap) - logger.debug("${clazz.simpleName} schema: $schema") - newCache.plus(clazz.simpleName!! to schema) + logger.debug("$slug schema: $schema") + newCache.plus(slug to schema) } } + } /** * Handler for when an [Enum] is encountered diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt index c8bc4b12d..58a4a6a44 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt @@ -1,20 +1,11 @@ package io.bkbn.kompendium -import java.util.UUID -import kotlin.reflect.KClass -import kotlin.reflect.KParameter -import kotlin.reflect.KProperty -import kotlin.reflect.KType -import kotlin.reflect.full.createType -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.javaField import io.bkbn.kompendium.annotations.KompendiumParam import io.bkbn.kompendium.models.meta.MethodInfo import io.bkbn.kompendium.models.meta.RequestInfo import io.bkbn.kompendium.models.meta.ResponseInfo import io.bkbn.kompendium.models.oas.ExampleWrapper +import io.bkbn.kompendium.models.oas.OpenApiAnyOf import io.bkbn.kompendium.models.oas.OpenApiSpecMediaType import io.bkbn.kompendium.models.oas.OpenApiSpecParameter import io.bkbn.kompendium.models.oas.OpenApiSpecPathItemOperation @@ -25,6 +16,17 @@ import io.bkbn.kompendium.models.oas.OpenApiSpecResponse import io.bkbn.kompendium.util.Helpers import io.bkbn.kompendium.util.Helpers.getReferenceSlug import io.bkbn.kompendium.util.Helpers.getSimpleSlug +import java.util.Locale +import java.util.UUID +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.KType +import kotlin.reflect.full.createType +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.javaField /** * The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class. @@ -106,7 +108,7 @@ object MethodParser { else -> { OpenApiSpecRequest( description = requestInfo.description, - content = resolveContent(requestInfo.mediaTypes, requestInfo.examples) ?: mapOf() + content = resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf() ) } } @@ -123,7 +125,7 @@ object MethodParser { else -> { val specResponse = OpenApiSpecResponse( description = responseInfo.description, - content = resolveContent(responseInfo.mediaTypes, responseInfo.examples) + content = resolveContent(this, responseInfo.mediaTypes, responseInfo.examples) ) Pair(responseInfo.status.value, specResponse) } @@ -131,20 +133,31 @@ object MethodParser { /** * Generates MediaTypes along with any examples provided - * @receiver [KType] Type of the object + * @param type [KType] Type of the object * @param mediaTypes list of acceptable http media types * @param examples Mapping of named examples of valid bodies. * @return Named mapping of media types. */ - private fun KType.resolveContent( + private fun resolveContent( + type: KType, mediaTypes: List, examples: Map ): Map>? { - return if (this != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) { + val classifier = type.classifier as KClass<*> + return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) { mediaTypes.associateWith { - val ref = getReferenceSlug() + val schema = if (classifier.isSealed) { + val refs = classifier.sealedSubclasses + .map { it.createType(type.arguments) } + .map { it.getReferenceSlug() } + .map { OpenApiSpecReferenceObject(it) } + OpenApiAnyOf(refs) + } else { + val ref = type.getReferenceSlug() + OpenApiSpecReferenceObject(ref) + } OpenApiSpecMediaType( - schema = OpenApiSpecReferenceObject(ref), + schema = schema, examples = examples.mapValues { (_, v) -> ExampleWrapper(v) }.ifEmpty { null } ) } @@ -153,7 +166,7 @@ object MethodParser { /** * Parses a type for all parameter information. All fields in the receiver - * must be annotated with [org.leafygreens.kompendium.annotations.KompendiumParam]. + * must be annotated with [io.bkbn.kompendium.annotations.KompendiumParam]. * @receiver type * @return list of valid parameter specs as detailed by the [KType] members * @throws [IllegalStateException] if the class could not be parsed properly @@ -170,7 +183,7 @@ object MethodParser { val defaultValue = getDefaultParameterValue(clazz, prop) OpenApiSpecParameter( name = prop.name, - `in` = anny.type.name.toLowerCase(), + `in` = anny.type.name.lowercase(Locale.getDefault()), schema = schema.addDefault(defaultValue), description = anny.description.ifBlank { null }, required = !prop.returnType.isMarkedNullable diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt index f6023fd7c..e0c1fdd65 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt @@ -27,7 +27,7 @@ object Notarized { /** * Notarization for an HTTP GET request * @param TParam The class containing all parameter fields. - * Each field must be annotated with @[org.leafygreens.kompendium.annotations.KompendiumField] + * Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField] * @param TResp Class detailing the expected API response * @param info Route metadata */ @@ -45,7 +45,7 @@ object Notarized { /** * Notarization for an HTTP POST request * @param TParam The class containing all parameter fields. - * Each field must be annotated with @[org.leafygreens.kompendium.annotations.KompendiumField] + * Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField] * @param TReq Class detailing the expected API request body * @param TResp Class detailing the expected API response * @param info Route metadata @@ -64,7 +64,7 @@ object Notarized { /** * Notarization for an HTTP Delete request * @param TParam The class containing all parameter fields. - * Each field must be annotated with @[org.leafygreens.kompendium.annotations.KompendiumField] + * Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField] * @param TReq Class detailing the expected API request body * @param TResp Class detailing the expected API response * @param info Route metadata @@ -84,7 +84,7 @@ object Notarized { /** * Notarization for an HTTP POST request * @param TParam The class containing all parameter fields. - * Each field must be annotated with @[org.leafygreens.kompendium.annotations.KompendiumField] + * Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField] * @param TResp Class detailing the expected API response * @param info Route metadata */ diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecComponentSchema.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecComponentSchema.kt index bba9cb9c0..89069198e 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecComponentSchema.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecComponentSchema.kt @@ -3,6 +3,7 @@ package io.bkbn.kompendium.models.oas sealed class OpenApiSpecComponentSchema(open val default: Any? = null) { fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) { + is AnyOfReferencedSchema -> error("Cannot add default to anyOf reference") is ReferencedSchema -> this.copy(default = default) is ObjectSchema -> this.copy(default = default) is DictionarySchema -> this.copy(default = default) @@ -17,6 +18,7 @@ sealed class OpenApiSpecComponentSchema(open val default: Any? = null) { sealed class TypedSchema(open val type: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default) data class ReferencedSchema(val `$ref`: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default) +data class AnyOfReferencedSchema(val anyOf: List) : OpenApiSpecComponentSchema() data class ObjectSchema( val properties: Map, diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecMediaType.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecMediaType.kt index 0693e225d..fb7ea6e0f 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecMediaType.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecMediaType.kt @@ -1,7 +1,7 @@ package io.bkbn.kompendium.models.oas data class OpenApiSpecMediaType( - val schema: OpenApiSpecReferenceObject, + val schema: OpenApiSpecReferencable, val examples: Map>? = null ) diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecReferencable.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecReferencable.kt index ad066ebfa..2a9f6c353 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecReferencable.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/oas/OpenApiSpecReferencable.kt @@ -1,15 +1,16 @@ package io.bkbn.kompendium.models.oas -sealed class OpenApiSpecReferencable +sealed interface OpenApiSpecReferencable -class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable() +data class OpenApiAnyOf(val anyOf: List) : OpenApiSpecReferencable +data class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable data class OpenApiSpecResponse( val description: String? = null, val headers: Map? = null, val content: Map>? = null, val links: Map? = null -) : OpenApiSpecReferencable() +) : OpenApiSpecReferencable data class OpenApiSpecParameter( val name: String, @@ -21,10 +22,10 @@ data class OpenApiSpecParameter( val allowEmptyValue: Boolean? = null, val style: String? = null, val explode: Boolean? = null -) : OpenApiSpecReferencable() +) : OpenApiSpecReferencable data class OpenApiSpecRequest( val description: String?, val content: Map>, val required: Boolean = false -) : OpenApiSpecReferencable() +) : OpenApiSpecReferencable diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/util/Helpers.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/util/Helpers.kt index 93b6e2a3a..19e2582de 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/util/Helpers.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/util/Helpers.kt @@ -1,5 +1,6 @@ package io.bkbn.kompendium.util +import io.bkbn.kompendium.util.Helpers.getReferenceSlug import java.lang.reflect.ParameterizedType import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -16,27 +17,6 @@ object Helpers { val UNIT_TYPE by lazy { Unit::class.createType() } - /** - * Simple extension function that will take a [Pair] and place it (if absent) into a [MutableMap]. - * - * @receiver [MutableMap] - * @param pair to add to map - */ - fun MutableMap.putPairIfAbsent(pair: Pair) = putIfAbsent(pair.first, pair.second) - - /** - * Simple extension function that will convert a list with two items into a [Pair] - * @receiver [List] - * @return [Pair] - * @throws [IllegalArgumentException] when the list size is not exactly two - */ - fun List.toPair(): Pair { - if (this.size != 2) { - throw IllegalArgumentException("List is not of length 2!") - } - return Pair(this[0], this[1]) - } - /** * Higher order function that takes a map of names to objects and will log their state ahead of function invocation * along with the result of the function invocation @@ -53,6 +33,11 @@ object Helpers { else -> simpleName ?: error("Could not determine simple name for $this") } + fun KType.getSimpleSlug(): String = when { + this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>) + else -> (classifier as KClass<*>).simpleName ?: error("Could not determine simple name for $this") + } + fun KType.getReferenceSlug(): String = when { arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}" else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}" 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 f4c11783d..b6fc7caed 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt @@ -20,6 +20,8 @@ import io.bkbn.kompendium.util.TestHelpers.getFileSnapshot import io.bkbn.kompendium.util.complexType import io.bkbn.kompendium.util.configModule import io.bkbn.kompendium.util.emptyGet +import io.bkbn.kompendium.util.genericPolymorphicResponse +import io.bkbn.kompendium.util.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.util.nestedUnderRootModule import io.bkbn.kompendium.util.nonRequiredParamsGet import io.bkbn.kompendium.util.notarizedDeleteModule @@ -29,9 +31,11 @@ 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.polymorphicResponse import io.bkbn.kompendium.util.primitives import io.bkbn.kompendium.util.returnsList import io.bkbn.kompendium.util.rootModule +import io.bkbn.kompendium.util.simpleGenericResponse import io.bkbn.kompendium.util.statusPageModule import io.bkbn.kompendium.util.statusPageMultiExceptions import io.bkbn.kompendium.util.trailingSlash @@ -436,6 +440,69 @@ internal class KompendiumTest { } } + @Test + fun `Can generate a polymorphic response type`() { + withTestApplication({ + configModule() + docs() + polymorphicResponse() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("polymorphic_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({ + configModule() + docs() + simpleGenericResponse() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("generic_response.json").trim() + assertEquals(expected, json, "The received json spec should match the expected content") + } + } + + @Test + fun `Can generate a polymorphic response type with generics`() { + withTestApplication({ + configModule() + docs() + genericPolymorphicResponse() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("polymorphic_response_with_generics.json").trim() + assertEquals(expected, json, "The received json spec should match the expected content") + } + } + + @Test + fun `Absolute Psycho Inheritance Test`() { + withTestApplication({ + configModule() + docs() + genericPolymorphicResponseMultipleImpls() + }) { + // do + val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content + + // expect + val expected = getFileSnapshot("crazy_polymorphic_example.json").trim() + assertEquals(expected, json, "The received json spec should match the expected content") + } + } private val oas = Kompendium.openApiSpec.copy( info = OpenApiSpecInfo( 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 039b69a0a..58dd45641 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 @@ -37,6 +37,8 @@ data class TestRequest( data class TestResponse(val c: String) +data class TestGeneric(val messy: String, val potato: T) + data class TestCreatedResponse(val id: Int, val c: String) data class ComplexRequest( @@ -72,3 +74,13 @@ data class OptionalParams( @KompendiumParam(ParamType.QUERY) val required: String, @KompendiumParam(ParamType.QUERY) val notRequired: String? ) + +sealed class FlibbityGibbit + +data class SimpleGibbit(val a: String) : FlibbityGibbit() +data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit() + +sealed interface Flibbity + +data class Gibbity(val a: T): Flibbity +data class Bibbity(val b: String, val f: 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 d699df992..8b2422a6f 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 @@ -2,6 +2,14 @@ package io.bkbn.kompendium.util import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.SerializationFeature +import io.bkbn.kompendium.Notarized.notarizedDelete +import io.bkbn.kompendium.Notarized.notarizedException +import io.bkbn.kompendium.Notarized.notarizedGet +import io.bkbn.kompendium.Notarized.notarizedPost +import io.bkbn.kompendium.Notarized.notarizedPut +import io.bkbn.kompendium.models.meta.MethodInfo +import io.bkbn.kompendium.models.meta.RequestInfo +import io.bkbn.kompendium.models.meta.ResponseInfo import io.ktor.application.Application import io.ktor.application.call import io.ktor.application.install @@ -13,14 +21,6 @@ import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.route import io.ktor.routing.routing -import io.bkbn.kompendium.Notarized.notarizedDelete -import io.bkbn.kompendium.Notarized.notarizedException -import io.bkbn.kompendium.Notarized.notarizedGet -import io.bkbn.kompendium.Notarized.notarizedPost -import io.bkbn.kompendium.Notarized.notarizedPut -import io.bkbn.kompendium.models.meta.MethodInfo -import io.bkbn.kompendium.models.meta.RequestInfo -import io.bkbn.kompendium.models.meta.ResponseInfo fun Application.configModule() { install(ContentNegotiation) { @@ -33,7 +33,12 @@ fun Application.configModule() { fun Application.statusPageModule() { install(StatusPages) { - notarizedException(info = ResponseInfo(HttpStatusCode.BadRequest, "Bad Things Happened")) { + notarizedException( + info = ResponseInfo( + HttpStatusCode.BadRequest, + "Bad Things Happened" + ) + ) { call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?")) } } @@ -41,10 +46,17 @@ fun Application.statusPageModule() { fun Application.statusPageMultiExceptions() { install(StatusPages) { - notarizedException(info = ResponseInfo(HttpStatusCode.Forbidden, "New API who dis?")) { + notarizedException( + info = ResponseInfo(HttpStatusCode.Forbidden, "New API who dis?") + ) { call.respond(HttpStatusCode.Forbidden) } - notarizedException(info = ResponseInfo(HttpStatusCode.BadRequest, "Bad Things Happened")) { + notarizedException( + info = ResponseInfo( + HttpStatusCode.BadRequest, + "Bad Things Happened" + ) + ) { call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?")) } } @@ -255,3 +267,48 @@ fun Application.nonRequiredParamsGet() { } } } + +fun Application.polymorphicResponse() { + routing { + route("/test/polymorphic") { + notarizedGet(TestResponseInfo.polymorphicResponse) { + call.respond(HttpStatusCode.OK, SimpleGibbit("hey")) + } + } + } +} + +fun Application.genericPolymorphicResponse() { + routing { + route("/test/polymorphic") { + notarizedGet(TestResponseInfo.genericPolymorphicResponse) { + call.respond(HttpStatusCode.OK, Gibbity("hey")) + } + } + } +} + +fun Application.genericPolymorphicResponseMultipleImpls() { + routing { + route("/test/polymorphic") { + notarizedGet(TestResponseInfo.genericPolymorphicResponse) { + call.respond(HttpStatusCode.OK, Gibbity("hey")) + } + } + route("/test/also/poly") { + notarizedGet(TestResponseInfo.anotherGenericPolymorphicResponse) { + call.respond(HttpStatusCode.OK, Bibbity("test", ComplexGibbit("nice", 1))) + } + } + } +} + +fun Application.simpleGenericResponse() { + routing { + route("/test/polymorphic") { + notarizedGet(TestResponseInfo.genericResponse) { + call.respond(HttpStatusCode.OK, Gibbity("hey")) + } + } + } +} 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 1a653caf4..579c57d9c 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 @@ -1,9 +1,12 @@ package io.bkbn.kompendium.util -import io.ktor.http.HttpStatusCode -import io.bkbn.kompendium.models.meta.MethodInfo +import io.bkbn.kompendium.models.meta.MethodInfo.DeleteInfo +import io.bkbn.kompendium.models.meta.MethodInfo.GetInfo +import io.bkbn.kompendium.models.meta.MethodInfo.PostInfo +import io.bkbn.kompendium.models.meta.MethodInfo.PutInfo import io.bkbn.kompendium.models.meta.RequestInfo import io.bkbn.kompendium.models.meta.ResponseInfo +import io.ktor.http.HttpStatusCode object TestResponseInfo { private val testGetResponse = ResponseInfo(HttpStatusCode.OK, "A Successful Endeavor") @@ -16,12 +19,12 @@ object TestResponseInfo { private val testRequest = RequestInfo("A Test request") private val testRequestAgain = RequestInfo("A Test request") private val complexRequest = RequestInfo("A Complex request") - val testGetInfo = MethodInfo.GetInfo( + val testGetInfo = GetInfo( summary = "Another get test", description = "testing more", responseInfo = testGetResponse ) - val testGetInfoAgain = MethodInfo.GetInfo>( + val testGetInfoAgain = GetInfo>( summary = "Another get test", description = "testing more", responseInfo = testGetListResponse @@ -32,40 +35,64 @@ object TestResponseInfo { val testGetWithMultipleExceptions = testGetInfo.copy( canThrow = setOf(AccessDeniedException::class, Exception::class) ) - val testPostInfo = MethodInfo.PostInfo( + val testPostInfo = PostInfo( summary = "Test post endpoint", description = "Post your tests here!", responseInfo = testPostResponse, requestInfo = testRequest ) - val testPutInfo = MethodInfo.PutInfo( + val testPutInfo = PutInfo( summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponse, requestInfo = complexRequest ) - val testPutInfoAlso = MethodInfo.PutInfo( + val testPutInfoAlso = PutInfo( summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponse, requestInfo = testRequest ) - val testPutInfoAgain = MethodInfo.PutInfo( + val testPutInfoAgain = PutInfo( summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponseAgain, requestInfo = testRequestAgain ) - val testDeleteInfo = MethodInfo.DeleteInfo( + val testDeleteInfo = DeleteInfo( summary = "Test delete endpoint", description = "testing my deletes", responseInfo = testDeleteResponse ) val emptyTestGetInfo = - MethodInfo.GetInfo( + GetInfo( summary = "No request params and response body", description = "testing more" ) - val trulyEmptyTestGetInfo = - MethodInfo.GetInfo(summary = "No request params and response body", description = "testing more") + val trulyEmptyTestGetInfo = GetInfo( + summary = "No request params and response body", + description = "testing more" + ) + val polymorphicResponse = GetInfo( + summary = "All the gibbits", + description = "Polymorphic response", + responseInfo = simpleOkResponse() + ) + val genericPolymorphicResponse = GetInfo>( + summary = "More flibbity", + description = "Polymorphic with generics", + responseInfo = simpleOkResponse() + ) + val anotherGenericPolymorphicResponse = GetInfo>( + summary = "The Most Flibbity", + description = "Polymorphic with generics but like... crazier", + responseInfo = simpleOkResponse() + ) + val genericResponse = GetInfo>( + summary = "Single Generic", + description = "Simple generic data class", + responseInfo = simpleOkResponse() + ) + + private fun simpleOkResponse() = ResponseInfo(HttpStatusCode.OK, "A successful endeavor") } diff --git a/kompendium-core/src/test/resources/complex_type.json b/kompendium-core/src/test/resources/complex_type.json index 69293fc96..c4b97bd19 100644 --- a/kompendium-core/src/test/resources/complex_type.json +++ b/kompendium-core/src/test/resources/complex_type.json @@ -61,21 +61,6 @@ "String" : { "type" : "string" }, - "Int" : { - "format" : "int32", - "type" : "integer" - }, - "TestCreatedResponse" : { - "properties" : { - "c" : { - "$ref" : "#/components/schemas/String" - }, - "id" : { - "$ref" : "#/components/schemas/Int" - } - }, - "type" : "object" - }, "SimpleEnum" : { "enum" : [ "ONE", "TWO" ], "type" : "string" @@ -124,6 +109,21 @@ } }, "type" : "object" + }, + "Int" : { + "format" : "int32", + "type" : "integer" + }, + "TestCreatedResponse" : { + "properties" : { + "c" : { + "$ref" : "#/components/schemas/String" + }, + "id" : { + "$ref" : "#/components/schemas/Int" + } + }, + "type" : "object" } }, "securitySchemes" : { } diff --git a/kompendium-core/src/test/resources/crazy_polymorphic_example.json b/kompendium-core/src/test/resources/crazy_polymorphic_example.json new file mode 100644 index 000000000..50205f777 --- /dev/null +++ b/kompendium-core/src/test/resources/crazy_polymorphic_example.json @@ -0,0 +1,164 @@ +{ + "openapi" : "3.0.3", + "info" : { + "title" : "Test API", + "version" : "1.33.7", + "description" : "An amazing, fully-ish \uD83D\uDE09 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" : "More flibbity", + "description" : "Polymorphic with generics", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/Gibbity-TestNested" + }, { + "$ref" : "#/components/schemas/Bibbity-TestNested" + } ] + } + } + } + } + }, + "deprecated" : false + } + }, + "/test/also/poly" : { + "get" : { + "tags" : [ ], + "summary" : "The Most Flibbity", + "description" : "Polymorphic with generics but like... crazier", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/Gibbity-FlibbityGibbit" + }, { + "$ref" : "#/components/schemas/Bibbity-FlibbityGibbit" + } ] + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "String" : { + "type" : "string" + }, + "TestNested" : { + "properties" : { + "nesty" : { + "$ref" : "#/components/schemas/String" + } + }, + "type" : "object" + }, + "Gibbity-TestNested" : { + "properties" : { + "a" : { + "$ref" : "#/components/schemas/TestNested" + } + }, + "type" : "object" + }, + "Bibbity-TestNested" : { + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "f" : { + "$ref" : "#/components/schemas/TestNested" + } + }, + "type" : "object" + }, + "SimpleGibbit" : { + "properties" : { + "a" : { + "$ref" : "#/components/schemas/String" + } + }, + "type" : "object" + }, + "Int" : { + "format" : "int32", + "type" : "integer" + }, + "ComplexGibbit" : { + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "c" : { + "$ref" : "#/components/schemas/Int" + } + }, + "type" : "object" + }, + "Gibbity-FlibbityGibbit" : { + "properties" : { + "a" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/SimpleGibbit" + }, { + "$ref" : "#/components/schemas/ComplexGibbit" + } ] + } + }, + "type" : "object" + }, + "Bibbity-FlibbityGibbit" : { + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "f" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/SimpleGibbit" + }, { + "$ref" : "#/components/schemas/ComplexGibbit" + } ] + } + }, + "type" : "object" + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +} diff --git a/kompendium-core/src/test/resources/example_req_and_resp.json b/kompendium-core/src/test/resources/example_req_and_resp.json index 88981f70d..80673270f 100644 --- a/kompendium-core/src/test/resources/example_req_and_resp.json +++ b/kompendium-core/src/test/resources/example_req_and_resp.json @@ -85,17 +85,6 @@ }, "components" : { "schemas" : { - "String" : { - "type" : "string" - }, - "TestResponse" : { - "properties" : { - "c" : { - "$ref" : "#/components/schemas/String" - } - }, - "type" : "object" - }, "Long" : { "format" : "int64", "type" : "integer" @@ -110,6 +99,9 @@ "format" : "double", "type" : "number" }, + "String" : { + "type" : "string" + }, "TestNested" : { "properties" : { "nesty" : { @@ -131,6 +123,14 @@ } }, "type" : "object" + }, + "TestResponse" : { + "properties" : { + "c" : { + "$ref" : "#/components/schemas/String" + } + }, + "type" : "object" } }, "securitySchemes" : { } diff --git a/kompendium-core/src/test/resources/generic_response.json b/kompendium-core/src/test/resources/generic_response.json new file mode 100644 index 000000000..e2df6d622 --- /dev/null +++ b/kompendium-core/src/test/resources/generic_response.json @@ -0,0 +1,73 @@ +{ + "openapi" : "3.0.3", + "info" : { + "title" : "Test API", + "version" : "1.33.7", + "description" : "An amazing, fully-ish \uD83D\uDE09 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" : "Single Generic", + "description" : "Simple generic data class", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TestGeneric-Int" + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "String" : { + "type" : "string" + }, + "Int" : { + "format" : "int32", + "type" : "integer" + }, + "TestGeneric-Int" : { + "properties" : { + "messy" : { + "$ref" : "#/components/schemas/String" + }, + "potato" : { + "$ref" : "#/components/schemas/Int" + } + }, + "type" : "object" + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +} diff --git a/kompendium-core/src/test/resources/notarized_post.json b/kompendium-core/src/test/resources/notarized_post.json index 87097f530..1d3c16da7 100644 --- a/kompendium-core/src/test/resources/notarized_post.json +++ b/kompendium-core/src/test/resources/notarized_post.json @@ -75,24 +75,6 @@ }, "components" : { "schemas" : { - "String" : { - "type" : "string" - }, - "Int" : { - "format" : "int32", - "type" : "integer" - }, - "TestCreatedResponse" : { - "properties" : { - "c" : { - "$ref" : "#/components/schemas/String" - }, - "id" : { - "$ref" : "#/components/schemas/Int" - } - }, - "type" : "object" - }, "Long" : { "format" : "int64", "type" : "integer" @@ -107,6 +89,9 @@ "format" : "double", "type" : "number" }, + "String" : { + "type" : "string" + }, "TestNested" : { "properties" : { "nesty" : { @@ -128,6 +113,21 @@ } }, "type" : "object" + }, + "Int" : { + "format" : "int32", + "type" : "integer" + }, + "TestCreatedResponse" : { + "properties" : { + "c" : { + "$ref" : "#/components/schemas/String" + }, + "id" : { + "$ref" : "#/components/schemas/Int" + } + }, + "type" : "object" } }, "securitySchemes" : { } diff --git a/kompendium-core/src/test/resources/notarized_primitives.json b/kompendium-core/src/test/resources/notarized_primitives.json index c9fc82a2e..42fa8c7ad 100644 --- a/kompendium-core/src/test/resources/notarized_primitives.json +++ b/kompendium-core/src/test/resources/notarized_primitives.json @@ -58,12 +58,12 @@ }, "components" : { "schemas" : { - "Boolean" : { - "type" : "boolean" - }, "Int" : { "format" : "int32", "type" : "integer" + }, + "Boolean" : { + "type" : "boolean" } }, "securitySchemes" : { } diff --git a/kompendium-core/src/test/resources/notarized_put.json b/kompendium-core/src/test/resources/notarized_put.json index 07eb76e05..467716305 100644 --- a/kompendium-core/src/test/resources/notarized_put.json +++ b/kompendium-core/src/test/resources/notarized_put.json @@ -75,24 +75,6 @@ }, "components" : { "schemas" : { - "String" : { - "type" : "string" - }, - "Int" : { - "format" : "int32", - "type" : "integer" - }, - "TestCreatedResponse" : { - "properties" : { - "c" : { - "$ref" : "#/components/schemas/String" - }, - "id" : { - "$ref" : "#/components/schemas/Int" - } - }, - "type" : "object" - }, "Long" : { "format" : "int64", "type" : "integer" @@ -107,6 +89,9 @@ "format" : "double", "type" : "number" }, + "String" : { + "type" : "string" + }, "TestNested" : { "properties" : { "nesty" : { @@ -128,6 +113,21 @@ } }, "type" : "object" + }, + "Int" : { + "format" : "int32", + "type" : "integer" + }, + "TestCreatedResponse" : { + "properties" : { + "c" : { + "$ref" : "#/components/schemas/String" + }, + "id" : { + "$ref" : "#/components/schemas/Int" + } + }, + "type" : "object" } }, "securitySchemes" : { } diff --git a/kompendium-core/src/test/resources/polymorphic_response.json b/kompendium-core/src/test/resources/polymorphic_response.json new file mode 100644 index 000000000..90f42c381 --- /dev/null +++ b/kompendium-core/src/test/resources/polymorphic_response.json @@ -0,0 +1,85 @@ +{ + "openapi" : "3.0.3", + "info" : { + "title" : "Test API", + "version" : "1.33.7", + "description" : "An amazing, fully-ish \uD83D\uDE09 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" : "All the gibbits", + "description" : "Polymorphic response", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/SimpleGibbit" + }, { + "$ref" : "#/components/schemas/ComplexGibbit" + } ] + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "String" : { + "type" : "string" + }, + "SimpleGibbit" : { + "properties" : { + "a" : { + "$ref" : "#/components/schemas/String" + } + }, + "type" : "object" + }, + "Int" : { + "format" : "int32", + "type" : "integer" + }, + "ComplexGibbit" : { + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "c" : { + "$ref" : "#/components/schemas/Int" + } + }, + "type" : "object" + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +} diff --git a/kompendium-core/src/test/resources/polymorphic_response_with_generics.json b/kompendium-core/src/test/resources/polymorphic_response_with_generics.json new file mode 100644 index 000000000..7bdc281d8 --- /dev/null +++ b/kompendium-core/src/test/resources/polymorphic_response_with_generics.json @@ -0,0 +1,89 @@ +{ + "openapi" : "3.0.3", + "info" : { + "title" : "Test API", + "version" : "1.33.7", + "description" : "An amazing, fully-ish \uD83D\uDE09 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" : "More flibbity", + "description" : "Polymorphic with generics", + "parameters" : [ ], + "responses" : { + "200" : { + "description" : "A successful endeavor", + "content" : { + "application/json" : { + "schema" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/Gibbity-TestNested" + }, { + "$ref" : "#/components/schemas/Bibbity-TestNested" + } ] + } + } + } + } + }, + "deprecated" : false + } + } + }, + "components" : { + "schemas" : { + "String" : { + "type" : "string" + }, + "TestNested" : { + "properties" : { + "nesty" : { + "$ref" : "#/components/schemas/String" + } + }, + "type" : "object" + }, + "Gibbity-TestNested" : { + "properties" : { + "a" : { + "$ref" : "#/components/schemas/TestNested" + } + }, + "type" : "object" + }, + "Bibbity-TestNested" : { + "properties" : { + "b" : { + "$ref" : "#/components/schemas/String" + }, + "f" : { + "$ref" : "#/components/schemas/TestNested" + } + }, + "type" : "object" + } + }, + "securitySchemes" : { } + }, + "security" : [ ], + "tags" : [ ] +} diff --git a/kompendium-playground/build.gradle.kts b/kompendium-playground/build.gradle.kts index 96c3ba691..90a010c3a 100644 --- a/kompendium-playground/build.gradle.kts +++ b/kompendium-playground/build.gradle.kts @@ -20,6 +20,6 @@ dependencies { application { @Suppress("DEPRECATION") - mainClassName = "org.leafygreens.kompendium.playground.MainKt" + mainClassName = "io.bkbn.kompendium.playground.MainKt" applicationDefaultJvmArgs = listOf("-Dio.ktor.development=true") // TODO I don't think this is working 😢 } 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 0e522d7bb..e9c819cf8 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 @@ -37,7 +37,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo import io.bkbn.kompendium.routes.openApi import io.bkbn.kompendium.routes.redoc -import org.leafygreens.kompendium.swagger.swaggerUI +import io.bkbn.kompendium.swagger.swaggerUI fun main() { embeddedServer( @@ -91,46 +91,46 @@ fun Application.mainModule() { openApi(oas) redoc(oas) swaggerUI() - route("/potato/spud") { - notarizedGet(testGetWithExamples) { - call.respond(HttpStatusCode.OK) - } - notarizedPost(testPostWithExamples) { - call.respond(HttpStatusCode.Created, ExampleResponse("hey")) - } - } +// route("/potato/spud") { +// notarizedGet(testGetWithExamples) { +// call.respond(HttpStatusCode.OK) +// } +// notarizedPost(testPostWithExamples) { +// call.respond(HttpStatusCode.Created, ExampleResponse("hey")) +// } +// } route("/test") { route("/{id}") { notarizedGet(testIdGetInfo) { call.respondText("get by id") } } - route("/single") { - notarizedGet(testSingleGetInfo) { - call.respondText("get single") - } - notarizedPost(testSinglePostInfo) { - call.respondText("test post") - } - notarizedPut(testSinglePutInfo) { - call.respondText { "hey" } - } - notarizedDelete(testSingleDeleteInfo) { - call.respondText { "heya" } - } - } - authenticate("basic") { - route("/authenticated/single") { - notarizedGet(testAuthenticatedSingleGetInfo) { - call.respond(HttpStatusCode.OK) - } - } - } - } - route("/error") { - notarizedGet(testSingleGetInfoWithThrowable) { - error("bad things just happened") - } +// route("/single") { +// notarizedGet(testSingleGetInfo) { +// call.respondText("get single") +// } +// notarizedPost(testSinglePostInfo) { +// call.respondText("test post") +// } +// notarizedPut(testSinglePutInfo) { +// call.respondText { "hey" } +// } +// notarizedDelete(testSingleDeleteInfo) { +// call.respondText { "heya" } +// } +// } +// authenticate("basic") { +// route("/authenticated/single") { +// notarizedGet(testAuthenticatedSingleGetInfo) { +// call.respond(HttpStatusCode.OK) +// } +// } +// } } +// route("/error") { +// notarizedGet(testSingleGetInfoWithThrowable) { +// error("bad things just happened") +// } +// } } } 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 6954143ee..4e6d96b64 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 @@ -16,6 +16,8 @@ data class JustQuery( data class ExampleNested(val nesty: String) +data class ExampleGeneric(val potato: T) + data class ExampleRequest( @KompendiumField(name = "field_name") val fieldName: ExampleNested, 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 3cdf3d045..5c822f2cf 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 @@ -37,7 +37,7 @@ object PlaygroundToC { canThrow = setOf(Exception::class) ) - val testIdGetInfo = MethodInfo.GetInfo( + val testIdGetInfo = MethodInfo.GetInfo>( summary = "Get Test", description = "Test for the getting", tags = setOf("test", "sample", "get"), diff --git a/kompendium-swagger-ui/src/main/kotlin/io/bkbn/kompendium/swagger/SwaggerUI.kt b/kompendium-swagger-ui/src/main/kotlin/io/bkbn/kompendium/swagger/SwaggerUI.kt index 19fc722b4..03490cbf0 100644 --- a/kompendium-swagger-ui/src/main/kotlin/io/bkbn/kompendium/swagger/SwaggerUI.kt +++ b/kompendium-swagger-ui/src/main/kotlin/io/bkbn/kompendium/swagger/SwaggerUI.kt @@ -1,4 +1,4 @@ -package org.leafygreens.kompendium.swagger +package io.bkbn.kompendium.swagger import io.ktor.application.call import io.ktor.response.respondRedirect