Polymorphic and Generic Support (#59)
This commit is contained in:
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@ -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 }}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
configure<com.adarshr.gradle.testlogger.TestLoggerExtension> {
|
||||
setTheme("standard")
|
||||
configure<TestLoggerExtension> {
|
||||
theme = ThemeType.MOCHA
|
||||
setLogLevel("lifecycle")
|
||||
showExceptions = true
|
||||
showStackTraces = true
|
||||
@ -51,8 +55,8 @@ allprojects {
|
||||
showFailedStandardStreams = true
|
||||
}
|
||||
|
||||
configure<io.gitlab.arturbosch.detekt.extensions.DetektExtension> {
|
||||
toolVersion = "1.16.0-RC2"
|
||||
configure<DetektExtension> {
|
||||
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/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=1.0.1
|
||||
project.version=1.1.0
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
|
@ -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 <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
|
||||
block: (KType, KType, KType) -> 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 responseType = typeOf<TResp>()
|
||||
val paramType = typeOf<TParam>()
|
||||
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 <reified TErr: Throwable, reified TResp : Any> errorNotarizationPreFlight(
|
||||
inline fun <reified TErr : Throwable, reified TResp : Any> errorNotarizationPreFlight(
|
||||
block: (KType, KType) -> Unit
|
||||
) {
|
||||
Kompendium.cache = Kontent.generateKontent<TResp>(Kompendium.cache)
|
||||
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
|
||||
val errorType = typeOf<TErr>()
|
||||
val responseType = typeOf<TResp>()
|
||||
addToCache(typeOf<Unit>(), typeOf<Unit>(), 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<KType> {
|
||||
val classifier = type.classifier as KClass<*>
|
||||
return if (classifier.isSealed) {
|
||||
classifier.sealedSubclasses.map {
|
||||
it.createType(type.arguments)
|
||||
}
|
||||
} else {
|
||||
listOf(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 <F> KType.resolveContent(
|
||||
private fun <F> resolveContent(
|
||||
type: KType,
|
||||
mediaTypes: List<String>,
|
||||
examples: Map<String, 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 {
|
||||
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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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<ReferencedSchema>) : OpenApiSpecComponentSchema()
|
||||
|
||||
data class ObjectSchema(
|
||||
val properties: Map<String, OpenApiSpecComponentSchema>,
|
||||
|
@ -1,7 +1,7 @@
|
||||
package io.bkbn.kompendium.models.oas
|
||||
|
||||
data class OpenApiSpecMediaType<T>(
|
||||
val schema: OpenApiSpecReferenceObject,
|
||||
val schema: OpenApiSpecReferencable,
|
||||
val examples: Map<String, ExampleWrapper<T>>? = null
|
||||
)
|
||||
|
||||
|
@ -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<OpenApiSpecReferenceObject>) : OpenApiSpecReferencable
|
||||
data class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable
|
||||
|
||||
data class OpenApiSpecResponse<T>(
|
||||
val description: String? = null,
|
||||
val headers: Map<String, OpenApiSpecReferencable>? = null,
|
||||
val content: Map<String, OpenApiSpecMediaType<T>>? = null,
|
||||
val links: Map<String, OpenApiSpecReferencable>? = 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<T>(
|
||||
val description: String?,
|
||||
val content: Map<String, OpenApiSpecMediaType<T>>,
|
||||
val required: Boolean = false
|
||||
) : OpenApiSpecReferencable()
|
||||
) : OpenApiSpecReferencable
|
||||
|
@ -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 <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
|
||||
* 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}"
|
||||
|
@ -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(
|
||||
|
@ -37,6 +37,8 @@ data class TestRequest(
|
||||
|
||||
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 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<T>
|
||||
|
||||
data class Gibbity<T>(val a: T): Flibbity<T>
|
||||
data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>
|
||||
|
@ -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<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?"))
|
||||
}
|
||||
}
|
||||
@ -41,10 +46,17 @@ fun Application.statusPageModule() {
|
||||
|
||||
fun Application.statusPageMultiExceptions() {
|
||||
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)
|
||||
}
|
||||
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?"))
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<TestResponse>(HttpStatusCode.OK, "A Successful Endeavor")
|
||||
@ -16,12 +19,12 @@ object TestResponseInfo {
|
||||
private val testRequest = RequestInfo<TestRequest>("A Test request")
|
||||
private val testRequestAgain = RequestInfo<Int>("A Test request")
|
||||
private val complexRequest = RequestInfo<ComplexRequest>("A Complex request")
|
||||
val testGetInfo = MethodInfo.GetInfo<TestParams, TestResponse>(
|
||||
val testGetInfo = GetInfo<TestParams, TestResponse>(
|
||||
summary = "Another get test",
|
||||
description = "testing more",
|
||||
responseInfo = testGetResponse
|
||||
)
|
||||
val testGetInfoAgain = MethodInfo.GetInfo<TestParams, List<TestResponse>>(
|
||||
val testGetInfoAgain = GetInfo<TestParams, List<TestResponse>>(
|
||||
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<TestParams, TestRequest, TestCreatedResponse>(
|
||||
val testPostInfo = PostInfo<TestParams, TestRequest, TestCreatedResponse>(
|
||||
summary = "Test post endpoint",
|
||||
description = "Post your tests here!",
|
||||
responseInfo = testPostResponse,
|
||||
requestInfo = testRequest
|
||||
)
|
||||
val testPutInfo = MethodInfo.PutInfo<Unit, ComplexRequest, TestCreatedResponse>(
|
||||
val testPutInfo = PutInfo<Unit, ComplexRequest, TestCreatedResponse>(
|
||||
summary = "Test put endpoint",
|
||||
description = "Put your tests here!",
|
||||
responseInfo = testPostResponse,
|
||||
requestInfo = complexRequest
|
||||
)
|
||||
val testPutInfoAlso = MethodInfo.PutInfo<TestParams, TestRequest, TestCreatedResponse>(
|
||||
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(
|
||||
summary = "Test put endpoint",
|
||||
description = "Put your tests here!",
|
||||
responseInfo = testPostResponse,
|
||||
requestInfo = testRequest
|
||||
)
|
||||
val testPutInfoAgain = MethodInfo.PutInfo<Unit, Int, Boolean>(
|
||||
val testPutInfoAgain = PutInfo<Unit, Int, Boolean>(
|
||||
summary = "Test put endpoint",
|
||||
description = "Put your tests here!",
|
||||
responseInfo = testPostResponseAgain,
|
||||
requestInfo = testRequestAgain
|
||||
)
|
||||
val testDeleteInfo = MethodInfo.DeleteInfo<TestParams, Unit>(
|
||||
val testDeleteInfo = DeleteInfo<TestParams, Unit>(
|
||||
summary = "Test delete endpoint",
|
||||
description = "testing my deletes",
|
||||
responseInfo = testDeleteResponse
|
||||
)
|
||||
val emptyTestGetInfo =
|
||||
MethodInfo.GetInfo<OptionalParams, Unit>(
|
||||
GetInfo<OptionalParams, Unit>(
|
||||
summary = "No request params and response body",
|
||||
description = "testing more"
|
||||
)
|
||||
val trulyEmptyTestGetInfo =
|
||||
MethodInfo.GetInfo<Unit, Unit>(summary = "No request params and response body", description = "testing more")
|
||||
val trulyEmptyTestGetInfo = GetInfo<Unit, Unit>(
|
||||
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")
|
||||
}
|
||||
|
@ -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" : { }
|
||||
|
@ -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" : [ ]
|
||||
}
|
@ -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" : { }
|
||||
|
73
kompendium-core/src/test/resources/generic_response.json
Normal file
73
kompendium-core/src/test/resources/generic_response.json
Normal 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" : [ ]
|
||||
}
|
@ -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" : { }
|
||||
|
@ -58,12 +58,12 @@
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"Boolean" : {
|
||||
"type" : "boolean"
|
||||
},
|
||||
"Int" : {
|
||||
"format" : "int32",
|
||||
"type" : "integer"
|
||||
},
|
||||
"Boolean" : {
|
||||
"type" : "boolean"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
|
@ -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" : { }
|
||||
|
85
kompendium-core/src/test/resources/polymorphic_response.json
Normal file
85
kompendium-core/src/test/resources/polymorphic_response.json
Normal 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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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 😢
|
||||
}
|
||||
|
@ -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")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ data class JustQuery(
|
||||
|
||||
data class ExampleNested(val nesty: String)
|
||||
|
||||
data class ExampleGeneric<T>(val potato: T)
|
||||
|
||||
data class ExampleRequest(
|
||||
@KompendiumField(name = "field_name")
|
||||
val fieldName: ExampleNested,
|
||||
|
@ -37,7 +37,7 @@ object PlaygroundToC {
|
||||
canThrow = setOf(Exception::class)
|
||||
)
|
||||
|
||||
val testIdGetInfo = MethodInfo.GetInfo<ExampleParams, ExampleResponse>(
|
||||
val testIdGetInfo = MethodInfo.GetInfo<ExampleParams, ExampleGeneric<Int>>(
|
||||
summary = "Get Test",
|
||||
description = "Test for the getting",
|
||||
tags = setOf("test", "sample", "get"),
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.leafygreens.kompendium.swagger
|
||||
package io.bkbn.kompendium.swagger
|
||||
|
||||
import io.ktor.application.call
|
||||
import io.ktor.response.respondRedirect
|
||||
|
Reference in New Issue
Block a user