Compare commits

..

4 Commits

19 changed files with 644 additions and 27 deletions

View File

@ -1,5 +1,23 @@
# Changelog # Changelog
## [1.7.0] - August 14th, 2021
### Added
- Added ability to inject an emergency `UndeclaredField` in the event of certain polymorphic serializers and such
## [1.6.0] - August 12th, 2021
### Added
- Ability to add custom type schema overrides for edge case types.
## [1.5.1] - August 12th, 2021
### Changed
- Fixed bug where polymorphic types were not being rendered correctly when part of collections and maps
## [1.5.0] - July 25th, 2021 ## [1.5.0] - July 25th, 2021
### Changed ### Changed

View File

@ -96,9 +96,21 @@ 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.
### Undeclared Field
There is also a final `UndeclaredField` annotation. This should be used only in an absolutely emergency. This annotation
will allow you to inject a _single_ undeclared field that will be included as part of the schema.
Due to limitations in using repeated annotations, this can only be used once per class
This is a complete hack, and is included for odd scenarios like kotlinx serialization polymorphic adapters that expect a
`type` field in order to perform their analysis.
Use this _only_ when **all** else fails
### Polymorphism ### Polymorphism
Out of the box, Kompendium has support for sealed classes. At runtime, it will build a mapping of all available sub-classes Speaking of polymorphism... out of the box, Kompendium has support for sealed classes and interfaces. 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 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 🤠 suggestions on better implementations are welcome 🤠
@ -216,6 +228,25 @@ routing {
} }
``` ```
## Custom Type Overrides
Kompendium does its best to analyze types and to generate an OpenAPI format accordingly. However, there are certain
classes that just don't play nice with the standard reflection analysis that Kompendium performs.
Should you encounter a data type that Kompendium cannot comprehend, you will need to
add it explicitly. For example, adding the Joda Time `DateTime` object would be as simple as the following
```kotlin
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
```
Since `Kompendium` is an object, this needs to be declared once, ahead of the actual API instantiation. This way, this
type override can be cached ahead of reflection. Kompendium will then match all instances of this type and return the
specified schema.
So how do you know a type can and cannot be inferred? The safe bet is that it can be. So go ahead and give it a shot.
However, in the very odd scenario (almost always having to do with date/time libraries 😤) where it can't, you can rest
safely knowing that you have the option to inject a custom override should you need to.
## Limitations ## Limitations
### Kompendium as a singleton ### Kompendium as a singleton
@ -234,7 +265,6 @@ should have. There are several outstanding features that have been added to the
- AsyncAPI Integration - AsyncAPI Integration
- Field Validation - Field Validation
- MavenCentral Release
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open
an issue [here](https://github.com/bkbnio/kompendium/issues/new) an issue [here](https://github.com/bkbnio/kompendium/issues/new)

View File

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

View File

@ -4,8 +4,10 @@ import io.bkbn.kompendium.models.meta.ErrorMap
import io.bkbn.kompendium.models.meta.SchemaMap import io.bkbn.kompendium.models.meta.SchemaMap
import io.bkbn.kompendium.models.oas.OpenApiSpec import io.bkbn.kompendium.models.oas.OpenApiSpec
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
import io.bkbn.kompendium.models.oas.TypedSchema
import io.bkbn.kompendium.path.CorePathCalculator import io.bkbn.kompendium.path.CorePathCalculator
import io.bkbn.kompendium.path.PathCalculator import io.bkbn.kompendium.path.PathCalculator
import kotlin.reflect.KClass
/** /**
* Maintains all state for the Kompendium library * Maintains all state for the Kompendium library
@ -31,4 +33,8 @@ object Kompendium {
) )
cache = emptyMap() cache = emptyMap()
} }
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema)
}
} }

View File

@ -49,23 +49,8 @@ object KompendiumPreFlight {
} }
fun addToCache(paramType: KType, requestType: KType, responseType: KType) { fun addToCache(paramType: KType, requestType: KType, responseType: KType) {
gatherSubTypes(requestType).forEach { Kompendium.cache = Kontent.generateKontent(requestType, Kompendium.cache)
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache) Kompendium.cache = Kontent.generateKontent(responseType, Kompendium.cache)
}
gatherSubTypes(responseType).forEach {
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache)
}
Kompendium.cache = Kontent.generateParameterKontent(paramType, 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,5 +1,6 @@
package io.bkbn.kompendium package io.bkbn.kompendium
import io.bkbn.kompendium.annotations.UndeclaredField
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.AnyOfReferencedSchema
import io.bkbn.kompendium.models.oas.ArraySchema import io.bkbn.kompendium.models.oas.ArraySchema
@ -7,6 +8,7 @@ import io.bkbn.kompendium.models.oas.DictionarySchema
import io.bkbn.kompendium.models.oas.EnumSchema import io.bkbn.kompendium.models.oas.EnumSchema
import io.bkbn.kompendium.models.oas.FormatSchema import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.models.oas.ObjectSchema import io.bkbn.kompendium.models.oas.ObjectSchema
import io.bkbn.kompendium.models.oas.OpenApiSpecComponentSchema
import io.bkbn.kompendium.models.oas.ReferencedSchema import io.bkbn.kompendium.models.oas.ReferencedSchema
import io.bkbn.kompendium.models.oas.SimpleSchema import io.bkbn.kompendium.models.oas.SimpleSchema
import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG
@ -57,7 +59,22 @@ object Kontent {
type: KType, type: KType,
cache: SchemaMap = emptyMap() cache: SchemaMap = emptyMap()
): SchemaMap { ): SchemaMap {
return generateKTypeKontent(type, cache) var newCache = cache
gatherSubTypes(type).forEach {
newCache = generateKTypeKontent(it, newCache)
}
return newCache
}
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)
}
} }
/** /**
@ -189,8 +206,14 @@ object Kontent {
} }
Pair(prop.name, propSchema) Pair(prop.name, propSchema)
} }
logger.debug("Looking for undeclared fields")
val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
val undeclaredType = it.clazz.createType()
newCache = generateKontent(undeclaredType, newCache)
it.field to ReferencedSchema(undeclaredType.getReferenceSlug())
}
logger.debug("$slug contains $fieldMap") logger.debug("$slug contains $fieldMap")
val schema = ObjectSchema(fieldMap) val schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
logger.debug("$slug schema: $schema") logger.debug("$slug schema: $schema")
newCache.plus(slug to schema) newCache.plus(slug to schema)
} }
@ -220,11 +243,20 @@ object Kontent {
if (keyType?.classifier != String::class) { if (keyType?.classifier != String::class) {
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String") error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
} }
val valClassName = (valType?.classifier as KClass<*>).simpleName val valClass = valType?.classifier as KClass<*>
val valClassName = valClass.simpleName
val referenceName = genericNameAdapter(type, clazz) val referenceName = genericNameAdapter(type, clazz)
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName") val valueReference = when (valClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(valType)
AnyOfReferencedSchema(subTypes.map {
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
})
}
false -> ReferencedSchema("$COMPONENT_SLUG/$valClassName")
}
val schema = DictionarySchema(additionalProperties = valueReference) val schema = DictionarySchema(additionalProperties = valueReference)
val updatedCache = generateKTypeKontent(valType, cache) val updatedCache = generateKontent(valType, cache)
return updatedCache.plus(referenceName to schema) return updatedCache.plus(referenceName to schema)
} }
@ -240,9 +272,17 @@ object Kontent {
val collectionClass = collectionType.classifier as KClass<*> val collectionClass = collectionType.classifier as KClass<*>
logger.debug("Obtained collection class: $collectionClass") logger.debug("Obtained collection class: $collectionClass")
val referenceName = genericNameAdapter(type, clazz) val referenceName = genericNameAdapter(type, clazz)
val valueReference = ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}") val valueReference = when (collectionClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(collectionType)
AnyOfReferencedSchema(subTypes.map {
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
})
}
false -> ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}")
}
val schema = ArraySchema(items = valueReference) val schema = ArraySchema(items = valueReference)
val updatedCache = generateKTypeKontent(collectionType, cache) val updatedCache = generateKontent(collectionType, cache)
return updatedCache.plus(referenceName to schema) return updatedCache.plus(referenceName to schema)
} }
} }

View File

@ -0,0 +1,8 @@
package io.bkbn.kompendium.annotations
import kotlin.reflect.KClass
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Repeatable
annotation class UndeclaredField(val field: String, val clazz: KClass<*>)

View File

@ -32,6 +32,9 @@ 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.polymorphicCollectionResponse
import io.bkbn.kompendium.util.polymorphicInterfaceResponse
import io.bkbn.kompendium.util.polymorphicMapResponse
import io.bkbn.kompendium.util.polymorphicResponse 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
@ -40,6 +43,7 @@ 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
import io.bkbn.kompendium.util.undeclaredType
import io.bkbn.kompendium.util.withDefaultParameter import io.bkbn.kompendium.util.withDefaultParameter
import io.bkbn.kompendium.util.withExamples import io.bkbn.kompendium.util.withExamples
@ -457,6 +461,54 @@ internal class KompendiumTest {
} }
} }
@Test
fun `Can generate a collection with polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicCollectionResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("polymorphic_list_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a map with a polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicMapResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("polymorphic_map_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a polymorphic response from a sealed interface`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicInterfaceResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("sealed_interface_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test @Test
fun `Can generate a response type with a generic type`() { fun `Can generate a response type with a generic type`() {
withTestApplication({ withTestApplication({
@ -505,6 +557,22 @@ internal class KompendiumTest {
} }
} }
@Test
fun `Can add an undeclared field`() {
withTestApplication({
kotlinxConfigModule()
docs()
undeclaredType()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("undeclared_field.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(
title = "Test API", title = "Test API",

View File

@ -4,6 +4,7 @@ import java.util.UUID
import io.bkbn.kompendium.annotations.KompendiumField import io.bkbn.kompendium.annotations.KompendiumField
import io.bkbn.kompendium.annotations.KompendiumParam import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.annotations.UndeclaredField
import java.math.BigDecimal import java.math.BigDecimal
import java.math.BigInteger import java.math.BigInteger
@ -84,7 +85,21 @@ sealed class FlibbityGibbit
data class SimpleGibbit(val a: String) : FlibbityGibbit() data class SimpleGibbit(val a: String) : FlibbityGibbit()
data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit() data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit()
sealed interface SlammaJamma
data class OneJamma(val a: Int) : SlammaJamma
data class AnothaJamma(val b: Float) : SlammaJamma
//data class InsaneJamma(val c: SlammaJamma) : SlammaJamma // 👀
sealed interface Flibbity<T> sealed interface Flibbity<T>
data class Gibbity<T>(val a: T): Flibbity<T> data class Gibbity<T>(val a: T): Flibbity<T>
data class Bibbity<T>(val b: String, val f: T) : Flibbity<T> data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>
enum class Hehe {
HAHA,
HOHO
}
@UndeclaredField("nowYouDont", Hehe::class)
data class Mysterious(val nowYouSeeMe: String)

View File

@ -285,6 +285,36 @@ fun Application.polymorphicResponse() {
} }
} }
fun Application.polymorphicCollectionResponse() {
routing {
route("/test/polymorphiclist") {
notarizedGet(TestResponseInfo.polymorphicListResponse) {
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
}
}
}
}
fun Application.polymorphicMapResponse() {
routing {
route("/test/polymorphicmap") {
notarizedGet(TestResponseInfo.polymorphicMapResponse) {
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
}
}
}
}
fun Application.polymorphicInterfaceResponse() {
routing {
route("/test/polymorphicmap") {
notarizedGet(TestResponseInfo.polymorphicInterfaceResponse) {
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
}
}
}
}
fun Application.genericPolymorphicResponse() { fun Application.genericPolymorphicResponse() {
routing { routing {
route("/test/polymorphic") { route("/test/polymorphic") {
@ -310,6 +340,16 @@ fun Application.genericPolymorphicResponseMultipleImpls() {
} }
} }
fun Application.undeclaredType() {
routing {
route("/test/polymorphic") {
notarizedGet(TestResponseInfo.undeclaredResponseType) {
call.respond(HttpStatusCode.OK, Mysterious("hi"))
}
}
}
}
fun Application.simpleGenericResponse() { fun Application.simpleGenericResponse() {
routing { routing {
route("/test/polymorphic") { route("/test/polymorphic") {

View File

@ -78,6 +78,21 @@ object TestResponseInfo {
description = "Polymorphic response", description = "Polymorphic response",
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()
) )
val polymorphicListResponse = GetInfo<Unit, List<FlibbityGibbit>>(
summary = "Oh so many gibbits",
description = "Polymorphic list response",
responseInfo = simpleOkResponse()
)
val polymorphicMapResponse = GetInfo<Unit, Map<String, FlibbityGibbit>>(
summary = "By gawd that's a lot of gibbits",
description = "Polymorphic list response",
responseInfo = simpleOkResponse()
)
val polymorphicInterfaceResponse = GetInfo<Unit, SlammaJamma>(
summary = "Come on and slam",
description = "and welcome to the jam",
responseInfo = simpleOkResponse()
)
val genericPolymorphicResponse = GetInfo<Unit, Flibbity<TestNested>>( val genericPolymorphicResponse = GetInfo<Unit, Flibbity<TestNested>>(
summary = "More flibbity", summary = "More flibbity",
description = "Polymorphic with generics", description = "Polymorphic with generics",
@ -88,6 +103,11 @@ object TestResponseInfo {
description = "Polymorphic with generics but like... crazier", description = "Polymorphic with generics but like... crazier",
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()
) )
val undeclaredResponseType = GetInfo<Unit, Mysterious>(
summary = "spooky class",
description = "break this glass in scenario of emergency",
responseInfo = simpleOkResponse()
)
val genericResponse = GetInfo<Unit, TestGeneric<Int>>( val genericResponse = GetInfo<Unit, TestGeneric<Int>>(
summary = "Single Generic", summary = "Single Generic",
description = "Simple generic data class", description = "Simple generic data class",

View File

@ -0,0 +1,91 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphiclist" : {
"get" : {
"tags" : [ ],
"summary" : "Oh so many gibbits",
"description" : "Polymorphic list response",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/List-FlibbityGibbit"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleGibbit" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"ComplexGibbit" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"c" : {
"$ref" : "#/components/schemas/Int"
}
}
},
"List-FlibbityGibbit" : {
"type" : "array",
"items" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/SimpleGibbit"
}, {
"$ref" : "#/components/schemas/ComplexGibbit"
} ]
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,91 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphicmap" : {
"get" : {
"tags" : [ ],
"summary" : "By gawd that's a lot of gibbits",
"description" : "Polymorphic list response",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Map-String-FlibbityGibbit"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleGibbit" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"ComplexGibbit" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"c" : {
"$ref" : "#/components/schemas/Int"
}
}
},
"Map-String-FlibbityGibbit" : {
"type" : "object",
"additionalProperties" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/SimpleGibbit"
}, {
"$ref" : "#/components/schemas/ComplexGibbit"
} ]
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,83 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphicmap" : {
"get" : {
"tags" : [ ],
"summary" : "Come on and slam",
"description" : "and welcome to the jam",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/OneJamma"
}, {
"$ref" : "#/components/schemas/AnothaJamma"
} ]
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"Int" : {
"type" : "integer",
"format" : "int32"
},
"OneJamma" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/Int"
}
}
},
"Float" : {
"type" : "number",
"format" : "float"
},
"AnothaJamma" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/Float"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,73 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphic" : {
"get" : {
"tags" : [ ],
"summary" : "spooky class",
"description" : "break this glass in scenario of emergency",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Mysterious"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"Hehe" : {
"type" : "string",
"enum" : [ "HAHA", "HOHO" ]
},
"Mysterious" : {
"type" : "object",
"properties" : {
"nowYouSeeMe" : {
"$ref" : "#/components/schemas/String"
},
"nowYouDont" : {
"$ref" : "#/components/schemas/Hehe"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -18,6 +18,8 @@ dependencies {
implementation(libs.bundles.ktorAuth) implementation(libs.bundles.ktorAuth)
implementation(libs.bundles.logging) implementation(libs.bundles.logging)
implementation("joda-time:joda-time:2.10.10")
testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
} }

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.playground package io.bkbn.kompendium.playground
import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.Notarized.notarizedDelete import io.bkbn.kompendium.Notarized.notarizedDelete
import io.bkbn.kompendium.Notarized.notarizedException import io.bkbn.kompendium.Notarized.notarizedException
import io.bkbn.kompendium.Notarized.notarizedGet import io.bkbn.kompendium.Notarized.notarizedGet
@ -7,7 +8,9 @@ import io.bkbn.kompendium.Notarized.notarizedPost
import io.bkbn.kompendium.Notarized.notarizedPut import io.bkbn.kompendium.Notarized.notarizedPut
import io.bkbn.kompendium.auth.KompendiumAuth.notarizedBasic import io.bkbn.kompendium.auth.KompendiumAuth.notarizedBasic
import io.bkbn.kompendium.models.meta.ResponseInfo import io.bkbn.kompendium.models.meta.ResponseInfo
import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.playground.PlaygroundToC.testAuthenticatedSingleGetInfo import io.bkbn.kompendium.playground.PlaygroundToC.testAuthenticatedSingleGetInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testCustomOverride
import io.bkbn.kompendium.playground.PlaygroundToC.testGetWithExamples import io.bkbn.kompendium.playground.PlaygroundToC.testGetWithExamples
import io.bkbn.kompendium.playground.PlaygroundToC.testIdGetInfo import io.bkbn.kompendium.playground.PlaygroundToC.testIdGetInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testPostWithExamples import io.bkbn.kompendium.playground.PlaygroundToC.testPostWithExamples
@ -16,6 +19,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo 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.playground.PlaygroundToC.testUndeclaredFields
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 io.bkbn.kompendium.swagger.swaggerUI import io.bkbn.kompendium.swagger.swaggerUI
@ -36,8 +40,11 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.webjars.Webjars import io.ktor.webjars.Webjars
import org.joda.time.DateTime
fun main() { fun main() {
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
embeddedServer( embeddedServer(
Netty, Netty,
port = 8081, port = 8081,
@ -114,6 +121,11 @@ fun Application.mainModule() {
call.respondText { "heya" } call.respondText { "heya" }
} }
} }
route("custom_override") {
notarizedGet(testCustomOverride) {
call.respondText { DateTime.now().toString() }
}
}
authenticate("basic") { authenticate("basic") {
route("/authenticated/single") { route("/authenticated/single") {
notarizedGet(testAuthenticatedSingleGetInfo) { notarizedGet(testAuthenticatedSingleGetInfo) {
@ -127,5 +139,10 @@ fun Application.mainModule() {
error("bad things just happened") error("bad things just happened")
} }
} }
route("/undeclared") {
notarizedGet(testUndeclaredFields) {
call.respondText { "hi" }
}
}
} }
} }

View File

@ -3,6 +3,8 @@ package io.bkbn.kompendium.playground
import io.bkbn.kompendium.annotations.KompendiumField import io.bkbn.kompendium.annotations.KompendiumField
import io.bkbn.kompendium.annotations.KompendiumParam import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.annotations.UndeclaredField
import org.joda.time.DateTime
data class ExampleParams( data class ExampleParams(
@KompendiumParam(ParamType.PATH) val id: Int, @KompendiumParam(ParamType.PATH) val id: Int,
@ -30,3 +32,13 @@ data class ExampleResponse(val c: String)
data class ExceptionResponse(val message: String) data class ExceptionResponse(val message: String)
data class ExampleCreatedResponse(val id: Int, val c: String) data class ExampleCreatedResponse(val id: Int, val c: String)
data class DateTimeWrapper(val dt: DateTime)
enum class Testerino {
First,
Second
}
@UndeclaredField("type", Testerino::class)
data class SimpleYetMysterious(val exists: Boolean)

View File

@ -55,6 +55,15 @@ object PlaygroundToC {
description = "Returns a different sample" description = "Returns a different sample"
) )
) )
val testCustomOverride = MethodInfo.GetInfo<Unit, DateTimeWrapper>(
summary = "custom schema test",
description = "testing",
tags = setOf("custom"),
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "good tings"
)
)
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy( val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
summary = "Show me the error baby 🙏", summary = "Show me the error baby 🙏",
canThrow = setOf(Exception::class) canThrow = setOf(Exception::class)
@ -100,4 +109,13 @@ object PlaygroundToC {
), ),
securitySchemes = setOf("basic") securitySchemes = setOf("basic")
) )
val testUndeclaredFields = MethodInfo.GetInfo<Unit, SimpleYetMysterious>(
summary = "Tests adding undeclared fields",
description = "vvv mysterious",
tags = setOf("mysterious"),
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "good tings"
)
)
} }