Polymorphic and Generic Support (#59)

This commit is contained in:
Ryan Brink
2021-05-21 17:35:19 -04:00
committed by GitHub
parent c885ff1cfb
commit 59c0c3aabf
31 changed files with 902 additions and 228 deletions

View File

@ -20,11 +20,6 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }} key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle restore-keys: ${{ runner.os }}-gradle
- name: Publish packages to Github - name: Publish packages to Github
run: ./gradlew publishAllPublicationsToGithubPackagesRepository -Prelease=true run: ./gradlew publish -Prelease=true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 }}

View File

@ -1,5 +1,12 @@
# Changelog # Changelog
## [1.1.0] - May 19th, 2021
### Added
- Support for sealed classes 🔥
- Support for generic classes ☄️
## [1.0.1] - May 10th, 2021 ## [1.0.1] - May 10th, 2021
### Changed ### Changed

View File

@ -30,7 +30,7 @@ repositories {
// 3 Add the package like any normal dependency // 3 Add the package like any normal dependency
dependencies { 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 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. (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 ## Examples
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example 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 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 [V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes
- Polymorphic support
- AsyncAPI Integration - AsyncAPI Integration
- Field Validation - Field Validation
- MavenCentral Release - MavenCentral Release

View File

@ -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 { plugins {
id("org.jetbrains.kotlin.jvm") version "1.4.32" apply false id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC2" 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("com.adarshr.test-logger") version "3.0.0" apply false
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
} }
allprojects { allprojects {
@ -26,14 +30,14 @@ allprojects {
apply(plugin = "com.adarshr.test-logger") apply(plugin = "com.adarshr.test-logger")
apply(plugin = "idea") apply(plugin = "idea")
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
} }
configure<com.adarshr.gradle.testlogger.TestLoggerExtension> { configure<TestLoggerExtension> {
setTheme("standard") theme = ThemeType.MOCHA
setLogLevel("lifecycle") setLogLevel("lifecycle")
showExceptions = true showExceptions = true
showStackTraces = true showStackTraces = true
@ -51,8 +55,8 @@ allprojects {
showFailedStandardStreams = true showFailedStandardStreams = true
} }
configure<io.gitlab.arturbosch.detekt.extensions.DetektExtension> { configure<DetektExtension> {
toolVersion = "1.16.0-RC2" toolVersion = "1.17.0-RC3"
config = files("${rootProject.projectDir}/detekt.yml") config = files("${rootProject.projectDir}/detekt.yml")
buildUponDefaultConfig = true buildUponDefaultConfig = true
} }
@ -61,14 +65,3 @@ allprojects {
withSourcesJar() 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/"))
}
}
}

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=1.0.1 project.version=1.1.0
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle

View File

@ -1,7 +1,9 @@
package io.bkbn.kompendium package io.bkbn.kompendium
import io.ktor.routing.Route import io.ktor.routing.Route
import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
/** /**
@ -21,13 +23,11 @@ object KompendiumPreFlight {
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight( inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
block: (KType, KType, KType) -> Route block: (KType, KType, KType) -> Route
): Route { ): Route {
Kompendium.cache = Kontent.generateKontent<TResp>(Kompendium.cache)
Kompendium.cache = Kontent.generateKontent<TReq>(Kompendium.cache)
Kompendium.cache = Kontent.generateParameterKontent<TParam>(Kompendium.cache)
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
val requestType = typeOf<TReq>() val requestType = typeOf<TReq>()
val responseType = typeOf<TResp>() val responseType = typeOf<TResp>()
val paramType = typeOf<TParam>() val paramType = typeOf<TParam>()
addToCache(paramType, requestType, responseType)
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
return block.invoke(paramType, requestType, responseType) 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 * @param block The function to execute, provided type information of the parameters above
*/ */
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
inline fun <reified TErr: Throwable, reified TResp : Any> errorNotarizationPreFlight( inline fun <reified TErr : Throwable, reified TResp : Any> errorNotarizationPreFlight(
block: (KType, KType) -> Unit block: (KType, KType) -> Unit
) { ) {
Kompendium.cache = Kontent.generateKontent<TResp>(Kompendium.cache)
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
val errorType = typeOf<TErr>() val errorType = typeOf<TErr>()
val responseType = typeOf<TResp>() val responseType = typeOf<TResp>()
addToCache(typeOf<Unit>(), typeOf<Unit>(), responseType)
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
return block.invoke(errorType, responseType) 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<KType> {
val classifier = type.classifier as KClass<*>
return if (classifier.isSealed) {
classifier.sealedSubclasses.map {
it.createType(type.arguments)
}
} else {
listOf(type)
}
}
} }

View File

@ -1,13 +1,7 @@
package io.bkbn.kompendium 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.meta.SchemaMap
import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
import io.bkbn.kompendium.models.oas.ArraySchema import io.bkbn.kompendium.models.oas.ArraySchema
import io.bkbn.kompendium.models.oas.DictionarySchema import io.bkbn.kompendium.models.oas.DictionarySchema
import io.bkbn.kompendium.models.oas.EnumSchema 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.COMPONENT_SLUG
import io.bkbn.kompendium.util.Helpers.genericNameAdapter import io.bkbn.kompendium.util.Helpers.genericNameAdapter
import io.bkbn.kompendium.util.Helpers.getReferenceSlug import io.bkbn.kompendium.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.util.Helpers.logged 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 import org.slf4j.LoggerFactory
/** /**
@ -42,6 +45,19 @@ object Kontent {
return generateKTypeKontent(kontentType, cache) 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 * Analyze a type [T], but filters out the top-level type
* @param T type to analyze * @param T type to analyze
@ -57,6 +73,20 @@ object Kontent {
.filterNot { (slug, _) -> slug == (kontentType.classifier as KClass<*>).simpleName } .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 * Recursively fills schema map depending on [KType] classifier
* @param type [KType] to parse * @param type [KType] to parse
@ -80,7 +110,7 @@ object Kontent {
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache) clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache) clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
clazz.isSubclassOf(Map::class) -> handleMapType(type, 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 clazz Class of the object to analyze
* @param cache Existing schema map to append to * @param cache Existing schema map to append to
*/ */
private fun handleComplexType(clazz: KClass<*>, cache: SchemaMap): SchemaMap = private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
when (cache.containsKey(clazz.simpleName)) { // 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 -> { true -> {
logger.debug("Cache already contains ${clazz.simpleName}, returning cache untouched") logger.debug("Cache already contains $slug, returning cache untouched")
cache cache
} }
false -> { 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 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 -> val fieldMap = clazz.memberProperties.associate { prop ->
logger.debug("Analyzing $prop in class $clazz") 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") val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
logger.debug("Detected field $field") 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)) { if (!newCache.containsKey(field.simpleName)) {
logger.debug("Cache was missing ${field.simpleName}, adding now") 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) Pair(prop.name, propSchema)
} }
logger.debug("${clazz.simpleName} contains $fieldMap") logger.debug("$slug contains $fieldMap")
val schema = ObjectSchema(fieldMap) val schema = ObjectSchema(fieldMap)
logger.debug("${clazz.simpleName} schema: $schema") logger.debug("$slug schema: $schema")
newCache.plus(clazz.simpleName!! to schema) newCache.plus(slug to schema)
} }
} }
}
/** /**
* Handler for when an [Enum] is encountered * Handler for when an [Enum] is encountered

View File

@ -1,20 +1,11 @@
package io.bkbn.kompendium 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.annotations.KompendiumParam
import io.bkbn.kompendium.models.meta.MethodInfo import io.bkbn.kompendium.models.meta.MethodInfo
import io.bkbn.kompendium.models.meta.RequestInfo import io.bkbn.kompendium.models.meta.RequestInfo
import io.bkbn.kompendium.models.meta.ResponseInfo import io.bkbn.kompendium.models.meta.ResponseInfo
import io.bkbn.kompendium.models.oas.ExampleWrapper 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.OpenApiSpecMediaType
import io.bkbn.kompendium.models.oas.OpenApiSpecParameter import io.bkbn.kompendium.models.oas.OpenApiSpecParameter
import io.bkbn.kompendium.models.oas.OpenApiSpecPathItemOperation 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
import io.bkbn.kompendium.util.Helpers.getReferenceSlug import io.bkbn.kompendium.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.util.Helpers.getSimpleSlug 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. * The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
@ -106,7 +108,7 @@ object MethodParser {
else -> { else -> {
OpenApiSpecRequest( OpenApiSpecRequest(
description = requestInfo.description, 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 -> { else -> {
val specResponse = OpenApiSpecResponse( val specResponse = OpenApiSpecResponse(
description = responseInfo.description, description = responseInfo.description,
content = resolveContent(responseInfo.mediaTypes, responseInfo.examples) content = resolveContent(this, responseInfo.mediaTypes, responseInfo.examples)
) )
Pair(responseInfo.status.value, specResponse) Pair(responseInfo.status.value, specResponse)
} }
@ -131,20 +133,31 @@ object MethodParser {
/** /**
* Generates MediaTypes along with any examples provided * 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 mediaTypes list of acceptable http media types
* @param examples Mapping of named examples of valid bodies. * @param examples Mapping of named examples of valid bodies.
* @return Named mapping of media types. * @return Named mapping of media types.
*/ */
private fun <F> KType.resolveContent( private fun <F> resolveContent(
type: KType,
mediaTypes: List<String>, mediaTypes: List<String>,
examples: Map<String, F> examples: Map<String, F>
): Map<String, OpenApiSpecMediaType<F>>? { ): Map<String, OpenApiSpecMediaType<F>>? {
return if (this != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) { val classifier = type.classifier as KClass<*>
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith { 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( OpenApiSpecMediaType(
schema = OpenApiSpecReferenceObject(ref), schema = schema,
examples = examples.mapValues { (_, v) -> ExampleWrapper(v) }.ifEmpty { null } 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 * 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 * @receiver type
* @return list of valid parameter specs as detailed by the [KType] members * @return list of valid parameter specs as detailed by the [KType] members
* @throws [IllegalStateException] if the class could not be parsed properly * @throws [IllegalStateException] if the class could not be parsed properly
@ -170,7 +183,7 @@ object MethodParser {
val defaultValue = getDefaultParameterValue(clazz, prop) val defaultValue = getDefaultParameterValue(clazz, prop)
OpenApiSpecParameter( OpenApiSpecParameter(
name = prop.name, name = prop.name,
`in` = anny.type.name.toLowerCase(), `in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue), schema = schema.addDefault(defaultValue),
description = anny.description.ifBlank { null }, description = anny.description.ifBlank { null },
required = !prop.returnType.isMarkedNullable required = !prop.returnType.isMarkedNullable

View File

@ -27,7 +27,7 @@ object Notarized {
/** /**
* Notarization for an HTTP GET request * Notarization for an HTTP GET request
* @param TParam The class containing all parameter fields. * @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 TResp Class detailing the expected API response
* @param info Route metadata * @param info Route metadata
*/ */
@ -45,7 +45,7 @@ object Notarized {
/** /**
* Notarization for an HTTP POST request * Notarization for an HTTP POST request
* @param TParam The class containing all parameter fields. * @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 TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response * @param TResp Class detailing the expected API response
* @param info Route metadata * @param info Route metadata
@ -64,7 +64,7 @@ object Notarized {
/** /**
* Notarization for an HTTP Delete request * Notarization for an HTTP Delete request
* @param TParam The class containing all parameter fields. * @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 TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response * @param TResp Class detailing the expected API response
* @param info Route metadata * @param info Route metadata
@ -84,7 +84,7 @@ object Notarized {
/** /**
* Notarization for an HTTP POST request * Notarization for an HTTP POST request
* @param TParam The class containing all parameter fields. * @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 TResp Class detailing the expected API response
* @param info Route metadata * @param info Route metadata
*/ */

View File

@ -3,6 +3,7 @@ package io.bkbn.kompendium.models.oas
sealed class OpenApiSpecComponentSchema(open val default: Any? = null) { sealed class OpenApiSpecComponentSchema(open val default: Any? = null) {
fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) { fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) {
is AnyOfReferencedSchema -> error("Cannot add default to anyOf reference")
is ReferencedSchema -> this.copy(default = default) is ReferencedSchema -> this.copy(default = default)
is ObjectSchema -> this.copy(default = default) is ObjectSchema -> this.copy(default = default)
is DictionarySchema -> 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) 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 ReferencedSchema(val `$ref`: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default)
data class AnyOfReferencedSchema(val anyOf: List<ReferencedSchema>) : OpenApiSpecComponentSchema()
data class ObjectSchema( data class ObjectSchema(
val properties: Map<String, OpenApiSpecComponentSchema>, val properties: Map<String, OpenApiSpecComponentSchema>,

View File

@ -1,7 +1,7 @@
package io.bkbn.kompendium.models.oas package io.bkbn.kompendium.models.oas
data class OpenApiSpecMediaType<T>( data class OpenApiSpecMediaType<T>(
val schema: OpenApiSpecReferenceObject, val schema: OpenApiSpecReferencable,
val examples: Map<String, ExampleWrapper<T>>? = null val examples: Map<String, ExampleWrapper<T>>? = null
) )

View File

@ -1,15 +1,16 @@
package io.bkbn.kompendium.models.oas package io.bkbn.kompendium.models.oas
sealed class OpenApiSpecReferencable sealed interface OpenApiSpecReferencable
class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable() data class OpenApiAnyOf(val anyOf: List<OpenApiSpecReferenceObject>) : OpenApiSpecReferencable
data class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable
data class OpenApiSpecResponse<T>( data class OpenApiSpecResponse<T>(
val description: String? = null, val description: String? = null,
val headers: Map<String, OpenApiSpecReferencable>? = null, val headers: Map<String, OpenApiSpecReferencable>? = null,
val content: Map<String, OpenApiSpecMediaType<T>>? = null, val content: Map<String, OpenApiSpecMediaType<T>>? = null,
val links: Map<String, OpenApiSpecReferencable>? = null val links: Map<String, OpenApiSpecReferencable>? = null
) : OpenApiSpecReferencable() ) : OpenApiSpecReferencable
data class OpenApiSpecParameter( data class OpenApiSpecParameter(
val name: String, val name: String,
@ -21,10 +22,10 @@ data class OpenApiSpecParameter(
val allowEmptyValue: Boolean? = null, val allowEmptyValue: Boolean? = null,
val style: String? = null, val style: String? = null,
val explode: Boolean? = null val explode: Boolean? = null
) : OpenApiSpecReferencable() ) : OpenApiSpecReferencable
data class OpenApiSpecRequest<T>( data class OpenApiSpecRequest<T>(
val description: String?, val description: String?,
val content: Map<String, OpenApiSpecMediaType<T>>, val content: Map<String, OpenApiSpecMediaType<T>>,
val required: Boolean = false val required: Boolean = false
) : OpenApiSpecReferencable() ) : OpenApiSpecReferencable

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.util package io.bkbn.kompendium.util
import io.bkbn.kompendium.util.Helpers.getReferenceSlug
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -16,27 +17,6 @@ object Helpers {
val UNIT_TYPE by lazy { Unit::class.createType() } 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 <K, V> MutableMap<K, V>.putPairIfAbsent(pair: Pair<K, V>) = 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 <T> List<T>.toPair(): Pair<T, T> {
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 * 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 * along with the result of the function invocation
@ -53,6 +33,11 @@ object Helpers {
else -> simpleName ?: error("Could not determine simple name for $this") 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 { fun KType.getReferenceSlug(): String = when {
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}" arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}" else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}"

View File

@ -20,6 +20,8 @@ import io.bkbn.kompendium.util.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.util.complexType import io.bkbn.kompendium.util.complexType
import io.bkbn.kompendium.util.configModule import io.bkbn.kompendium.util.configModule
import io.bkbn.kompendium.util.emptyGet 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.nestedUnderRootModule
import io.bkbn.kompendium.util.nonRequiredParamsGet import io.bkbn.kompendium.util.nonRequiredParamsGet
import io.bkbn.kompendium.util.notarizedDeleteModule 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.notarizedPostModule
import io.bkbn.kompendium.util.notarizedPutModule import io.bkbn.kompendium.util.notarizedPutModule
import io.bkbn.kompendium.util.pathParsingTestModule import io.bkbn.kompendium.util.pathParsingTestModule
import io.bkbn.kompendium.util.polymorphicResponse
import io.bkbn.kompendium.util.primitives import io.bkbn.kompendium.util.primitives
import io.bkbn.kompendium.util.returnsList import io.bkbn.kompendium.util.returnsList
import io.bkbn.kompendium.util.rootModule import io.bkbn.kompendium.util.rootModule
import io.bkbn.kompendium.util.simpleGenericResponse
import io.bkbn.kompendium.util.statusPageModule import io.bkbn.kompendium.util.statusPageModule
import io.bkbn.kompendium.util.statusPageMultiExceptions import io.bkbn.kompendium.util.statusPageMultiExceptions
import io.bkbn.kompendium.util.trailingSlash 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( private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo( info = OpenApiSpecInfo(

View File

@ -37,6 +37,8 @@ data class TestRequest(
data class TestResponse(val c: String) data class TestResponse(val c: String)
data class TestGeneric<T>(val messy: String, val potato: T)
data class TestCreatedResponse(val id: Int, val c: String) data class TestCreatedResponse(val id: Int, val c: String)
data class ComplexRequest( data class ComplexRequest(
@ -72,3 +74,13 @@ data class OptionalParams(
@KompendiumParam(ParamType.QUERY) val required: String, @KompendiumParam(ParamType.QUERY) val required: String,
@KompendiumParam(ParamType.QUERY) val notRequired: 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<T>
data class Gibbity<T>(val a: T): Flibbity<T>
data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>

View File

@ -2,6 +2,14 @@ package io.bkbn.kompendium.util
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature 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.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -13,14 +21,6 @@ import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.route import io.ktor.routing.route
import io.ktor.routing.routing 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() { fun Application.configModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
@ -33,7 +33,12 @@ fun Application.configModule() {
fun Application.statusPageModule() { fun Application.statusPageModule() {
install(StatusPages) { install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(HttpStatusCode.BadRequest, "Bad Things Happened")) { notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo(
HttpStatusCode.BadRequest,
"Bad Things Happened"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?")) call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
} }
} }
@ -41,10 +46,17 @@ fun Application.statusPageModule() {
fun Application.statusPageMultiExceptions() { fun Application.statusPageMultiExceptions() {
install(StatusPages) { install(StatusPages) {
notarizedException<AccessDeniedException, Unit>(info = ResponseInfo(HttpStatusCode.Forbidden, "New API who dis?")) { notarizedException<AccessDeniedException, Unit>(
info = ResponseInfo(HttpStatusCode.Forbidden, "New API who dis?")
) {
call.respond(HttpStatusCode.Forbidden) call.respond(HttpStatusCode.Forbidden)
} }
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(HttpStatusCode.BadRequest, "Bad Things Happened")) { notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo(
HttpStatusCode.BadRequest,
"Bad Things Happened"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?")) 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"))
}
}
}
}

View File

@ -1,9 +1,12 @@
package io.bkbn.kompendium.util package io.bkbn.kompendium.util
import io.ktor.http.HttpStatusCode import io.bkbn.kompendium.models.meta.MethodInfo.DeleteInfo
import io.bkbn.kompendium.models.meta.MethodInfo 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.RequestInfo
import io.bkbn.kompendium.models.meta.ResponseInfo import io.bkbn.kompendium.models.meta.ResponseInfo
import io.ktor.http.HttpStatusCode
object TestResponseInfo { object TestResponseInfo {
private val testGetResponse = ResponseInfo<TestResponse>(HttpStatusCode.OK, "A Successful Endeavor") private val testGetResponse = ResponseInfo<TestResponse>(HttpStatusCode.OK, "A Successful Endeavor")
@ -16,12 +19,12 @@ object TestResponseInfo {
private val testRequest = RequestInfo<TestRequest>("A Test request") private val testRequest = RequestInfo<TestRequest>("A Test request")
private val testRequestAgain = RequestInfo<Int>("A Test request") private val testRequestAgain = RequestInfo<Int>("A Test request")
private val complexRequest = RequestInfo<ComplexRequest>("A Complex request") private val complexRequest = RequestInfo<ComplexRequest>("A Complex request")
val testGetInfo = MethodInfo.GetInfo<TestParams, TestResponse>( val testGetInfo = GetInfo<TestParams, TestResponse>(
summary = "Another get test", summary = "Another get test",
description = "testing more", description = "testing more",
responseInfo = testGetResponse responseInfo = testGetResponse
) )
val testGetInfoAgain = MethodInfo.GetInfo<TestParams, List<TestResponse>>( val testGetInfoAgain = GetInfo<TestParams, List<TestResponse>>(
summary = "Another get test", summary = "Another get test",
description = "testing more", description = "testing more",
responseInfo = testGetListResponse responseInfo = testGetListResponse
@ -32,40 +35,64 @@ object TestResponseInfo {
val testGetWithMultipleExceptions = testGetInfo.copy( val testGetWithMultipleExceptions = testGetInfo.copy(
canThrow = setOf(AccessDeniedException::class, Exception::class) canThrow = setOf(AccessDeniedException::class, Exception::class)
) )
val testPostInfo = MethodInfo.PostInfo<TestParams, TestRequest, TestCreatedResponse>( val testPostInfo = PostInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test post endpoint", summary = "Test post endpoint",
description = "Post your tests here!", description = "Post your tests here!",
responseInfo = testPostResponse, responseInfo = testPostResponse,
requestInfo = testRequest requestInfo = testRequest
) )
val testPutInfo = MethodInfo.PutInfo<Unit, ComplexRequest, TestCreatedResponse>( val testPutInfo = PutInfo<Unit, ComplexRequest, TestCreatedResponse>(
summary = "Test put endpoint", summary = "Test put endpoint",
description = "Put your tests here!", description = "Put your tests here!",
responseInfo = testPostResponse, responseInfo = testPostResponse,
requestInfo = complexRequest requestInfo = complexRequest
) )
val testPutInfoAlso = MethodInfo.PutInfo<TestParams, TestRequest, TestCreatedResponse>( val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test put endpoint", summary = "Test put endpoint",
description = "Put your tests here!", description = "Put your tests here!",
responseInfo = testPostResponse, responseInfo = testPostResponse,
requestInfo = testRequest requestInfo = testRequest
) )
val testPutInfoAgain = MethodInfo.PutInfo<Unit, Int, Boolean>( val testPutInfoAgain = PutInfo<Unit, Int, Boolean>(
summary = "Test put endpoint", summary = "Test put endpoint",
description = "Put your tests here!", description = "Put your tests here!",
responseInfo = testPostResponseAgain, responseInfo = testPostResponseAgain,
requestInfo = testRequestAgain requestInfo = testRequestAgain
) )
val testDeleteInfo = MethodInfo.DeleteInfo<TestParams, Unit>( val testDeleteInfo = DeleteInfo<TestParams, Unit>(
summary = "Test delete endpoint", summary = "Test delete endpoint",
description = "testing my deletes", description = "testing my deletes",
responseInfo = testDeleteResponse responseInfo = testDeleteResponse
) )
val emptyTestGetInfo = val emptyTestGetInfo =
MethodInfo.GetInfo<OptionalParams, Unit>( GetInfo<OptionalParams, Unit>(
summary = "No request params and response body", summary = "No request params and response body",
description = "testing more" description = "testing more"
) )
val trulyEmptyTestGetInfo = val trulyEmptyTestGetInfo = GetInfo<Unit, Unit>(
MethodInfo.GetInfo<Unit, Unit>(summary = "No request params and response body", description = "testing more") summary = "No request params and response body",
description = "testing more"
)
val polymorphicResponse = GetInfo<Unit, FlibbityGibbit>(
summary = "All the gibbits",
description = "Polymorphic response",
responseInfo = simpleOkResponse()
)
val genericPolymorphicResponse = GetInfo<Unit, Flibbity<TestNested>>(
summary = "More flibbity",
description = "Polymorphic with generics",
responseInfo = simpleOkResponse()
)
val anotherGenericPolymorphicResponse = GetInfo<Unit, Flibbity<FlibbityGibbit>>(
summary = "The Most Flibbity",
description = "Polymorphic with generics but like... crazier",
responseInfo = simpleOkResponse()
)
val genericResponse = GetInfo<Unit, TestGeneric<Int>>(
summary = "Single Generic",
description = "Simple generic data class",
responseInfo = simpleOkResponse()
)
private fun <T> simpleOkResponse() = ResponseInfo<T>(HttpStatusCode.OK, "A successful endeavor")
} }

View File

@ -61,21 +61,6 @@
"String" : { "String" : {
"type" : "string" "type" : "string"
}, },
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"
},
"SimpleEnum" : { "SimpleEnum" : {
"enum" : [ "ONE", "TWO" ], "enum" : [ "ONE", "TWO" ],
"type" : "string" "type" : "string"
@ -124,6 +109,21 @@
} }
}, },
"type" : "object" "type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"
} }
}, },
"securitySchemes" : { } "securitySchemes" : { }

View File

@ -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" : [ ]
}

View File

@ -85,17 +85,6 @@
}, },
"components" : { "components" : {
"schemas" : { "schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Long" : { "Long" : {
"format" : "int64", "format" : "int64",
"type" : "integer" "type" : "integer"
@ -110,6 +99,9 @@
"format" : "double", "format" : "double",
"type" : "number" "type" : "number"
}, },
"String" : {
"type" : "string"
},
"TestNested" : { "TestNested" : {
"properties" : { "properties" : {
"nesty" : { "nesty" : {
@ -131,6 +123,14 @@
} }
}, },
"type" : "object" "type" : "object"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
} }
}, },
"securitySchemes" : { } "securitySchemes" : { }

View File

@ -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" : [ ]
}

View File

@ -75,24 +75,6 @@
}, },
"components" : { "components" : {
"schemas" : { "schemas" : {
"String" : {
"type" : "string"
},
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"
},
"Long" : { "Long" : {
"format" : "int64", "format" : "int64",
"type" : "integer" "type" : "integer"
@ -107,6 +89,9 @@
"format" : "double", "format" : "double",
"type" : "number" "type" : "number"
}, },
"String" : {
"type" : "string"
},
"TestNested" : { "TestNested" : {
"properties" : { "properties" : {
"nesty" : { "nesty" : {
@ -128,6 +113,21 @@
} }
}, },
"type" : "object" "type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"
} }
}, },
"securitySchemes" : { } "securitySchemes" : { }

View File

@ -58,12 +58,12 @@
}, },
"components" : { "components" : {
"schemas" : { "schemas" : {
"Boolean" : {
"type" : "boolean"
},
"Int" : { "Int" : {
"format" : "int32", "format" : "int32",
"type" : "integer" "type" : "integer"
},
"Boolean" : {
"type" : "boolean"
} }
}, },
"securitySchemes" : { } "securitySchemes" : { }

View File

@ -75,24 +75,6 @@
}, },
"components" : { "components" : {
"schemas" : { "schemas" : {
"String" : {
"type" : "string"
},
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"
},
"Long" : { "Long" : {
"format" : "int64", "format" : "int64",
"type" : "integer" "type" : "integer"
@ -107,6 +89,9 @@
"format" : "double", "format" : "double",
"type" : "number" "type" : "number"
}, },
"String" : {
"type" : "string"
},
"TestNested" : { "TestNested" : {
"properties" : { "properties" : {
"nesty" : { "nesty" : {
@ -128,6 +113,21 @@
} }
}, },
"type" : "object" "type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"
} }
}, },
"securitySchemes" : { } "securitySchemes" : { }

View File

@ -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" : [ ]
}

View File

@ -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" : [ ]
}

View File

@ -20,6 +20,6 @@ dependencies {
application { application {
@Suppress("DEPRECATION") @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 😢 applicationDefaultJvmArgs = listOf("-Dio.ktor.development=true") // TODO I don't think this is working 😢
} }

View File

@ -37,7 +37,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo
import io.bkbn.kompendium.routes.openApi import io.bkbn.kompendium.routes.openApi
import io.bkbn.kompendium.routes.redoc import io.bkbn.kompendium.routes.redoc
import org.leafygreens.kompendium.swagger.swaggerUI import io.bkbn.kompendium.swagger.swaggerUI
fun main() { fun main() {
embeddedServer( embeddedServer(
@ -91,46 +91,46 @@ fun Application.mainModule() {
openApi(oas) openApi(oas)
redoc(oas) redoc(oas)
swaggerUI() swaggerUI()
route("/potato/spud") { // route("/potato/spud") {
notarizedGet(testGetWithExamples) { // notarizedGet(testGetWithExamples) {
call.respond(HttpStatusCode.OK) // call.respond(HttpStatusCode.OK)
} // }
notarizedPost(testPostWithExamples) { // notarizedPost(testPostWithExamples) {
call.respond(HttpStatusCode.Created, ExampleResponse("hey")) // call.respond(HttpStatusCode.Created, ExampleResponse("hey"))
} // }
} // }
route("/test") { route("/test") {
route("/{id}") { route("/{id}") {
notarizedGet(testIdGetInfo) { notarizedGet(testIdGetInfo) {
call.respondText("get by id") call.respondText("get by id")
} }
} }
route("/single") { // route("/single") {
notarizedGet(testSingleGetInfo) { // notarizedGet(testSingleGetInfo) {
call.respondText("get single") // call.respondText("get single")
} // }
notarizedPost(testSinglePostInfo) { // notarizedPost(testSinglePostInfo) {
call.respondText("test post") // call.respondText("test post")
} // }
notarizedPut(testSinglePutInfo) { // notarizedPut(testSinglePutInfo) {
call.respondText { "hey" } // call.respondText { "hey" }
} // }
notarizedDelete(testSingleDeleteInfo) { // notarizedDelete(testSingleDeleteInfo) {
call.respondText { "heya" } // call.respondText { "heya" }
} // }
} // }
authenticate("basic") { // authenticate("basic") {
route("/authenticated/single") { // route("/authenticated/single") {
notarizedGet(testAuthenticatedSingleGetInfo) { // notarizedGet(testAuthenticatedSingleGetInfo) {
call.respond(HttpStatusCode.OK) // call.respond(HttpStatusCode.OK)
} // }
} // }
} // }
}
route("/error") {
notarizedGet(testSingleGetInfoWithThrowable) {
error("bad things just happened")
}
} }
// route("/error") {
// notarizedGet(testSingleGetInfoWithThrowable) {
// error("bad things just happened")
// }
// }
} }
} }

View File

@ -16,6 +16,8 @@ data class JustQuery(
data class ExampleNested(val nesty: String) data class ExampleNested(val nesty: String)
data class ExampleGeneric<T>(val potato: T)
data class ExampleRequest( data class ExampleRequest(
@KompendiumField(name = "field_name") @KompendiumField(name = "field_name")
val fieldName: ExampleNested, val fieldName: ExampleNested,

View File

@ -37,7 +37,7 @@ object PlaygroundToC {
canThrow = setOf(Exception::class) canThrow = setOf(Exception::class)
) )
val testIdGetInfo = MethodInfo.GetInfo<ExampleParams, ExampleResponse>( val testIdGetInfo = MethodInfo.GetInfo<ExampleParams, ExampleGeneric<Int>>(
summary = "Get Test", summary = "Get Test",
description = "Test for the getting", description = "Test for the getting",
tags = setOf("test", "sample", "get"), tags = setOf("test", "sample", "get"),

View File

@ -1,4 +1,4 @@
package org.leafygreens.kompendium.swagger package io.bkbn.kompendium.swagger
import io.ktor.application.call import io.ktor.application.call
import io.ktor.response.respondRedirect import io.ktor.response.respondRedirect