feat: v2-alpha (#112)

There are still some bugs, still some outstanding features, but I don't want to hold this back any longer, that way I can keep the future PRs much more focused
This commit is contained in:
Ryan Brink
2022-01-02 23:15:15 -05:00
committed by GitHub
parent d66880f9b2
commit c29567114d
229 changed files with 9172 additions and 7233 deletions

View File

@ -0,0 +1,9 @@
# Module kompendium-core
This is where the magic happens. This module houses all the reflective goodness that powers Kompendium.
It is also the only mandatory client-facing module for a basic setup.
# Package io.bkbn.kompendium.core
The root package contains several objects that power Kompendium

View File

@ -1,77 +1,33 @@
plugins {
`java-library`
`maven-publish`
signing
id("io.bkbn.sourdough.library")
`java-test-fixtures`
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(libs.jackson.module.kotlin)
implementation(libs.bundles.ktor)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation(libs.ktor.serialization)
testImplementation(libs.kotlinx.serialization.json)
testImplementation(libs.ktor.jackson)
testImplementation(libs.ktor.server.test.host)
}
// IMPLEMENTATION
java {
withSourcesJar()
withJavadocJar()
}
api(projects.kompendiumOas)
api(projects.kompendiumAnnotations)
publishing {
repositories {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("kompendium") {
from(components["kotlin"])
artifact(tasks.sourcesJar)
artifact(tasks.javadocJar)
groupId = project.group.toString()
artifactId = project.name.toLowerCase()
version = project.version.toString()
val ktorVersion: String by project
val kotestVersion: String by project
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion)
pom {
name.set("Kompendium")
description.set("A minimally invasive OpenAPI spec generator for Ktor")
url.set("https://github.com/bkbnio/Kompendium")
licenses {
license {
name.set("MIT License")
url.set("https://mit-license.org/")
}
}
developers {
developer {
id.set("bkbnio")
name.set("Ryan Brink")
email.set("admin@bkbn.io")
}
}
scm {
connection.set("scm:git:git://github.com/bkbnio/Kompendium.git")
developerConnection.set("scm:git:ssh://github.com/bkbnio/Kompendium.git")
url.set("https://github.com/bkbnio/Kompendium.git")
}
}
}
}
}
implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = "2.13.0")
signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
sign(publishing.publications)
// TEST FIXTURES
testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion)
testFixturesApi(group = "io.kotest", name = "kotest-assertions-core-jvm", version = kotestVersion)
testFixturesApi(group = "io.kotest", name = "kotest-property-jvm", version = kotestVersion)
testFixturesApi(group = "io.kotest", name = "kotest-assertions-json-jvm", version = kotestVersion)
testFixturesApi(group = "io.kotest", name = "kotest-assertions-ktor-jvm", version = "4.4.3")
testFixturesApi(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
testFixturesApi(group = "io.ktor", name = "ktor-server-test-host", version = ktorVersion)
testFixturesApi(group = "io.ktor", name = "ktor-jackson", version = ktorVersion)
testFixturesApi(group = "io.ktor", name = "ktor-serialization", version = ktorVersion)
testFixturesApi(group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.3.1")
}

View File

@ -1,49 +0,0 @@
package io.bkbn.kompendium
import io.bkbn.kompendium.models.meta.ErrorMap
import io.bkbn.kompendium.models.meta.SchemaMap
import io.bkbn.kompendium.models.oas.OpenApiSpec
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
import io.bkbn.kompendium.models.oas.TypedSchema
import io.bkbn.kompendium.path.IPathCalculator
import io.bkbn.kompendium.path.PathCalculator
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import kotlin.reflect.KClass
/**
* Maintains all state for the Kompendium library
*/
object Kompendium {
var errorMap: ErrorMap = emptyMap()
var cache: SchemaMap = emptyMap()
var openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),
paths = mutableMapOf()
)
fun calculatePath(route: Route) = PathCalculator.calculate(route)
fun resetSchema() {
openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),
paths = mutableMapOf()
)
cache = emptyMap()
}
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema)
}
fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: IPathCalculator.(Route, String) -> String
) {
PathCalculator.addCustomRouteHandler(selector, handler)
}
}

View File

@ -1,54 +0,0 @@
package io.bkbn.kompendium
import io.ktor.routing.Route
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* Functions are considered preflight when they are used to intercept a method ahead of running.
*/
object KompendiumPreFlight {
/**
* Performs all content analysis on the types provided to a notarized route and adds it to the top level spec
* @param TParam
* @param TReq
* @param TResp
* @param block The function to execute, provided type information of the parameters above
* @return [Route]
*/
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
block: (KType, KType, KType) -> Route
): Route {
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)
}
/**
* Performs all content analysis on the types provided to a notarized error and adds them to the top level spec.
* @param TErr
* @param TResp
* @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(
block: (KType, KType) -> Unit
) {
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) {
Kompendium.cache = Kontent.generateKontent(requestType, Kompendium.cache)
Kompendium.cache = Kontent.generateKontent(responseType, Kompendium.cache)
Kompendium.cache = Kontent.generateParameterKontent(paramType, Kompendium.cache)
}
}

View File

@ -1,290 +0,0 @@
package io.bkbn.kompendium
import io.bkbn.kompendium.annotations.UndeclaredField
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
import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.models.oas.ObjectSchema
import io.bkbn.kompendium.models.oas.ReferencedSchema
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 org.slf4j.LoggerFactory
import java.math.BigDecimal
import java.math.BigInteger
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
/**
* Responsible for generating the schema map that is used to power all object references across the API Spec.
*/
object Kontent {
private val logger = LoggerFactory.getLogger(javaClass)
/**
* Analyzes a type [T] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
* @param T type to analyze
* @param cache Existing schema map to append to
* @return an updated schema map containing all type information for [T]
*/
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> generateKontent(
cache: SchemaMap = emptyMap()
): SchemaMap {
val kontentType = typeOf<T>()
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 {
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)
}
}
/**
* Analyze a type [T], but filters out the top-level type
* @param T type to analyze
* @param cache Existing schema map to append to
* @return an updated schema map containing all type information for [T]
*/
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> generateParameterKontent(
cache: SchemaMap = emptyMap()
): SchemaMap {
val kontentType = typeOf<T>()
return generateKTypeKontent(kontentType, cache)
.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
* @param cache Existing schema map to append to
*/
fun generateKTypeKontent(
type: KType,
cache: SchemaMap = emptyMap()
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
logger.debug("Parsing Kontent of $type")
when (val clazz = type.classifier as KClass<*>) {
Unit::class -> cache
Int::class -> cache.plus(clazz.simpleName!! to FormatSchema("int32", "integer"))
Long::class -> cache.plus(clazz.simpleName!! to FormatSchema("int64", "integer"))
Double::class -> cache.plus(clazz.simpleName!! to FormatSchema("double", "number"))
Float::class -> cache.plus(clazz.simpleName!! to FormatSchema("float", "number"))
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
UUID::class -> cache.plus(clazz.simpleName!! to FormatSchema("uuid", "string"))
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormatSchema("double", "number"))
BigInteger::class -> cache.plus(clazz.simpleName!! to FormatSchema("int64", "integer"))
ByteArray::class -> cache.plus(clazz.simpleName!! to FormatSchema("byte", "string"))
else -> when {
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(type, clazz, cache)
}
}
}
/**
* In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
* @param clazz Class of the object to analyze
* @param cache Existing schema map to append to
*/
// TODO Fix as part of this issue https://github.com/bkbnio/kompendium/issues/80
@Suppress("LongMethod", "ComplexMethod")
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 $slug, returning cache untouched")
cache
}
false -> {
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")
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))
}
}
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")
val schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
logger.debug("$slug schema: $schema")
newCache.plus(slug to schema)
}
}
}
/**
* Handler for when an [Enum] is encountered
* @param clazz Class of the object to analyze
* @param cache Existing schema map to append to
*/
private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
return cache.plus(clazz.simpleName!! to EnumSchema(options))
}
/**
* Handler for when a [Map] is encountered
* @param type Map type information
* @param clazz Map class information
* @param cache Existing schema map to append to
*/
private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
logger.debug("Map detected for $type, generating schema and appending to cache")
val (keyType, valType) = type.arguments.map { it.type }
logger.debug("Obtained map types -> key: $keyType and value: $valType")
if (keyType?.classifier != String::class) {
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
}
val valClass = valType?.classifier as KClass<*>
val valClassName = valClass.simpleName
val referenceName = genericNameAdapter(type, clazz)
val valueReference = when (valClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(valType)
AnyOfReferencedSchema(subTypes.map {
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
})
}
false -> ReferencedSchema("$COMPONENT_SLUG/$valClassName")
}
val schema = DictionarySchema(additionalProperties = valueReference)
val updatedCache = generateKontent(valType, cache)
return updatedCache.plus(referenceName to schema)
}
/**
* Handler for when a [Collection] is encountered
* @param type Collection type information
* @param clazz Collection class information
* @param cache Existing schema map to append to
*/
private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
logger.debug("Collection detected for $type, generating schema and appending to cache")
val collectionType = type.arguments.first().type!!
val collectionClass = collectionType.classifier as KClass<*>
logger.debug("Obtained collection class: $collectionClass")
val referenceName = genericNameAdapter(type, clazz)
val valueReference = when (collectionClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(collectionType)
AnyOfReferencedSchema(subTypes.map {
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
})
}
false -> ReferencedSchema("$COMPONENT_SLUG/${collectionClass.simpleName}")
}
val schema = ArraySchema(items = valueReference)
val updatedCache = generateKontent(collectionType, cache)
return updatedCache.plus(referenceName to schema)
}
}

View File

@ -1,115 +0,0 @@
package io.bkbn.kompendium
import io.ktor.application.ApplicationCall
import io.ktor.features.StatusPages
import io.ktor.http.HttpMethod
import io.ktor.routing.Route
import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineContext
import io.ktor.util.pipeline.PipelineInterceptor
import io.bkbn.kompendium.KompendiumPreFlight.errorNotarizationPreFlight
import io.bkbn.kompendium.MethodParser.parseErrorInfo
import io.bkbn.kompendium.MethodParser.parseMethodInfo
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.MethodInfo.DeleteInfo
import io.bkbn.kompendium.models.meta.ResponseInfo
import io.bkbn.kompendium.models.oas.OpenApiSpecPathItem
/**
* Notarization methods are the primary way that a Ktor API using Kompendium differentiates
* from a default Ktor application. On instantiation, a notarized route, provided with the proper metadata,
* will reflectively analyze all pertinent data to build a corresponding OpenAPI entry.
*/
object Notarized {
/**
* Notarization for an HTTP GET request
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: GetInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Get) { handle(body) }
}
/**
* Notarization for an HTTP POST request
* @param TParam The class containing all parameter fields.
* 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
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: PostInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Post) { handle(body) }
}
/**
* Notarization for an HTTP Delete request
* @param TParam The class containing all parameter fields.
* 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
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: PutInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.put =
parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Put) { handle(body) }
}
/**
* Notarization for an HTTP POST request
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: DeleteInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Delete) { handle(body) }
}
/**
* Notarization for a handled exception response
* @param TErr The [Throwable] that is being handled
* @param TResp Class detailing the expected API response when handled
* @param info Response metadata
*/
inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
info: ResponseInfo<TResp>,
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit
) = errorNotarizationPreFlight<TErr, TResp>() { errorType, responseType ->
info.parseErrorInfo(errorType, responseType)
exception(handler)
}
}

View File

@ -1,5 +0,0 @@
package io.bkbn.kompendium.annotations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class KompendiumField(val name: String)

View File

@ -1,17 +0,0 @@
package io.bkbn.kompendium.annotations
/**
* Used to indicate that a field in a data class represents an OpenAPI parameter
* @param type The type of parameter, must be valid [ParamType]
* @param description Description of the parameter to include in OpenAPI Spec
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class KompendiumParam(val type: ParamType, val description: String = "")
enum class ParamType {
COOKIE,
HEADER,
PATH,
QUERY
}

View File

@ -1,8 +0,0 @@
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

@ -0,0 +1,55 @@
package io.bkbn.kompendium.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.schema.TypedSchema
import io.ktor.application.Application
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.path
import io.ktor.response.respondText
import io.ktor.util.AttributeKey
import kotlin.reflect.KClass
class Kompendium(val config: Configuration) {
class Configuration {
lateinit var spec: OpenApiSpec
var cache: SchemaMap = emptyMap()
var specRoute = "/openapi.json"
// TODO Add tests for this!!
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema)
}
// TODO Add tests for this!!
var om: ObjectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)
fun specToJson(): String = om.writeValueAsString(spec)
}
companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> {
override val key: AttributeKey<Kompendium> = AttributeKey("Kompendium")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Kompendium {
val configuration = Configuration().apply(configure)
pipeline.intercept(ApplicationCallPipeline.Call) {
if (call.request.path() == configuration.specRoute) {
call.respondText { configuration.specToJson() }
call.response.status(HttpStatusCode.OK)
}
}
return Kompendium(configuration)
}
}
}

View File

@ -0,0 +1,39 @@
package io.bkbn.kompendium.core
import io.ktor.application.feature
import io.ktor.routing.Route
import io.ktor.routing.application
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* Functions are considered preflight when they are used to intercept a method ahead of running.
*/
object KompendiumPreFlight {
/**
* Performs all content analysis on the types provided to a notarized route and adds it to the top level spec
* @param TParam
* @param TReq
* @param TResp
* @param block The function to execute, provided type information of the parameters above
* @return [Route]
*/
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.methodNotarizationPreFlight(
block: (KType, KType, KType) -> Route
): Route {
val feature = this.application.feature(Kompendium)
val requestType = typeOf<TReq>()
val responseType = typeOf<TResp>()
val paramType = typeOf<TParam>()
addToCache(paramType, requestType, responseType, feature)
return block.invoke(paramType, requestType, responseType)
}
fun addToCache(paramType: KType, requestType: KType, responseType: KType, feature: Kompendium) {
feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache)
feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache)
feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache)
}
}

View File

@ -0,0 +1,457 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Field
import io.bkbn.kompendium.annotations.FreeFormObject
import io.bkbn.kompendium.annotations.UndeclaredField
import io.bkbn.kompendium.annotations.constraint.Format
import io.bkbn.kompendium.annotations.constraint.MaxItems
import io.bkbn.kompendium.annotations.constraint.MaxLength
import io.bkbn.kompendium.annotations.constraint.MaxProperties
import io.bkbn.kompendium.annotations.constraint.Maximum
import io.bkbn.kompendium.annotations.constraint.MinItems
import io.bkbn.kompendium.annotations.constraint.MinLength
import io.bkbn.kompendium.annotations.constraint.MinProperties
import io.bkbn.kompendium.annotations.constraint.Minimum
import io.bkbn.kompendium.annotations.constraint.MultipleOf
import io.bkbn.kompendium.annotations.constraint.Pattern
import io.bkbn.kompendium.annotations.constraint.UniqueItems
import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.core.metadata.TypeMap
import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.core.util.Helpers.logged
import io.bkbn.kompendium.core.util.Helpers.toNumber
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ArraySchema
import io.bkbn.kompendium.oas.schema.ComponentSchema
import io.bkbn.kompendium.oas.schema.DictionarySchema
import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaField
import kotlin.reflect.typeOf
import org.slf4j.LoggerFactory
import java.math.BigDecimal
import java.math.BigInteger
import java.util.UUID
/**
* Responsible for generating the schema map that is used to power all object references across the API Spec.
*/
object Kontent {
private val logger = LoggerFactory.getLogger(javaClass)
/**
* Analyzes a type [T] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
* @param T type to analyze
* @param cache Existing schema map to append to
* @return an updated schema map containing all type information for [T]
*/
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> generateKontent(
cache: SchemaMap = emptyMap()
): SchemaMap {
val kontentType = typeOf<T>()
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 {
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)
}
}
/**
* Recursively fills schema map depending on [KType] classifier
* @param type [KType] to parse
* @param cache Existing schema map to append to
*/
fun generateKTypeKontent(
type: KType,
cache: SchemaMap = emptyMap(),
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
logger.debug("Parsing Kontent of $type")
when (val clazz = type.classifier as KClass<*>) {
Unit::class -> cache
Int::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int32", "integer"))
Long::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
Double::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
Float::class -> cache.plus(clazz.simpleName!! to FormattedSchema("float", "number"))
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
UUID::class -> cache.plus(clazz.simpleName!! to FormattedSchema("uuid", "string"))
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
BigInteger::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
ByteArray::class -> cache.plus(clazz.simpleName!! to FormattedSchema("byte", "string"))
else -> when {
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(type, clazz, cache)
}
}
}
/**
* In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
* @param clazz Class of the object to analyze
* @param cache Existing schema map to append to
*/
@Suppress("LongMethod", "ComplexMethod")
private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
// This needs to be simple because it will be stored under its 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 $slug, returning cache untouched")
cache
}
false -> {
logger.debug("$slug was not found in cache, generating now")
var newCache = cache
// Grabs any type parameters mapped to the corresponding type argument(s)
val typeMap: 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")
// Short circuit if data is free form
val freeForm = prop.findAnnotation<FreeFormObject>()
var name = prop.name
// todo add method to clean up
when (freeForm) {
null -> {
val baseType = scanForGeneric(typeMap, prop)
val baseClazz = baseType.classifier as KClass<*>
val allTypes = scanForSealed(baseClazz, baseType)
newCache = updateCache(newCache, field, allTypes)
var propSchema = constructComponentSchema(
typeMap = typeMap,
prop = prop,
fieldClazz = field,
clazz = baseClazz,
type = baseType,
cache = newCache
)
// todo move to helper
prop.findAnnotation<Field>()?.let { fieldOverrides ->
if (fieldOverrides.description.isNotBlank()) {
propSchema = propSchema.setDescription(fieldOverrides.description)
}
if (fieldOverrides.name.isNotBlank()) {
name = fieldOverrides.name
}
}
Pair(name, propSchema)
}
else -> {
val minProperties = prop.findAnnotation<MinProperties>()
val maxProperties = prop.findAnnotation<MaxProperties>()
val schema =
FreeFormSchema(minProperties = minProperties?.properties, maxProperties = maxProperties?.properties)
Pair(name, schema)
}
}
}
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 newCache[undeclaredType.getSimpleSlug()]!!
}
logger.debug("$slug contains $fieldMap")
var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! })
}
logger.debug("$slug schema: $schema")
newCache.plus(slug to schema)
}
}
}
/**
* Takes the type information provided and adds any missing data to the schema map
*/
private fun updateCache(cache: SchemaMap, clazz: KClass<*>, types: List<KType>): SchemaMap {
var newCache = cache
if (!cache.containsKey(clazz.simpleName)) {
logger.debug("Cache was missing ${clazz.simpleName}, adding now")
types.forEach {
newCache = generateKTypeKontent(it, newCache)
}
}
return newCache
}
/**
* Scans a class for sealed subclasses. If found, returns a list with all children. Otherwise, returns
* the base type
*/
private fun scanForSealed(clazz: KClass<*>, type: KType): List<KType> = if (clazz.isSealed) {
clazz.sealedSubclasses.map { it.createType(type.arguments) }
} else {
listOf(type)
}
/**
* Yoinks any generic types from the type map should the field be a generic
*/
private fun scanForGeneric(typeMap: TypeMap, prop: KProperty1<*, *>): KType =
if (typeMap.containsKey(prop.returnType.classifier)) {
logger.debug("Generic type detected")
typeMap[prop.returnType.classifier]?.type!!
} else {
prop.returnType
}
/**
* Constructs a [ComponentSchema]
*/
private fun constructComponentSchema(
typeMap: TypeMap,
clazz: KClass<*>,
fieldClazz: KClass<*>,
prop: KProperty1<*, *>,
type: KType,
cache: SchemaMap
): ComponentSchema =
when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(typeMap, clazz, type, prop.returnType.classifier, cache)
false -> handleStandardProperty(clazz, fieldClazz, prop, type, cache)
}.scanForConstraints(clazz, prop)
private fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
when (this) {
is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
is ArraySchema -> scanForConstraints(prop)
is DictionarySchema -> this // TODO Anything here?
is EnumSchema -> scanForConstraints(prop)
is FormattedSchema -> scanForConstraints(prop)
is FreeFormSchema -> this // todo anything here?
is ObjectSchema -> scanForConstraints(clazz, prop)
is SimpleSchema -> scanForConstraints(prop)
}
private fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {
val minItems = prop.findAnnotation<MinItems>()
val maxItems = prop.findAnnotation<MaxItems>()
val uniqueItems = prop.findAnnotation<UniqueItems>()
return this.copy(
minItems = minItems?.items,
maxItems = maxItems?.items,
uniqueItems = uniqueItems?.let { true }
)
}
private fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
if (prop.returnType.isMarkedNullable) {
return this.copy(nullable = true)
}
return this
}
private fun FormattedSchema.scanForConstraints(prop: KProperty1<*, *>): FormattedSchema {
val minimum = prop.findAnnotation<Minimum>()
val maximum = prop.findAnnotation<Maximum>()
val multipleOf = prop.findAnnotation<MultipleOf>()
var schema = this
if (prop.returnType.isMarkedNullable) {
schema = schema.copy(nullable = true)
}
return schema.copy(
minimum = minimum?.min?.toNumber(),
maximum = maximum?.max?.toNumber(),
exclusiveMinimum = minimum?.exclusive,
exclusiveMaximum = maximum?.exclusive,
multipleOf = multipleOf?.multiple?.toNumber(),
)
}
private fun SimpleSchema.scanForConstraints(prop: KProperty1<*, *>): SimpleSchema {
val minLength = prop.findAnnotation<MinLength>()
val maxLength = prop.findAnnotation<MaxLength>()
val pattern = prop.findAnnotation<Pattern>()
val format = prop.findAnnotation<Format>()
var schema = this
if (prop.returnType.isMarkedNullable) {
schema = schema.copy(nullable = true)
}
return schema.copy(
minLength = minLength?.length,
maxLength = maxLength?.length,
pattern = pattern?.pattern,
format = format?.format
)
}
private fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
var schema = this
if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! })
}
if (prop.returnType.isMarkedNullable) {
schema = schema.copy(nullable = true)
}
return schema
}
/**
* If a field has no type parameters, build its [ComponentSchema] without referencing the [TypeMap]
*/
private fun handleStandardProperty(
clazz: KClass<*>,
fieldClazz: KClass<*>,
prop: KProperty1<*, *>,
type: KType,
cache: SchemaMap
): ComponentSchema = if (clazz.isSealed) {
val refs = clazz.sealedSubclasses
.map { it.createType(type.arguments) }
.map { cache[it.getSimpleSlug()] ?: error("$it not found in cache") }
AnyOfSchema(refs)
} else {
val slug = fieldClazz.getSimpleSlug(prop)
cache[slug] ?: error("$slug not found in cache")
}
/**
* If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
*/
private fun handleGenericProperty(
typeMap: TypeMap,
clazz: KClass<*>,
type: KType,
classifier: KClassifier?,
cache: SchemaMap
): ComponentSchema = if (clazz.isSealed) {
val refs = clazz.sealedSubclasses
.map { it.createType(type.arguments) }
.map { it.getSimpleSlug() }
.map { cache[it] ?: error("$it not available in cache") }
AnyOfSchema(refs)
} else {
val slug = typeMap[classifier]?.type!!.getSimpleSlug()
cache[slug] ?: error("$slug not found in cache")
}
/**
* Handler for when an [Enum] is encountered
* @param clazz Class of the object to analyze
* @param cache Existing schema map to append to
*/
private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
return cache.plus(clazz.simpleName!! to EnumSchema(options))
}
/**
* Handler for when a [Map] is encountered
* @param type Map type information
* @param clazz Map class information
* @param cache Existing schema map to append to
*/
private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
logger.debug("Map detected for $type, generating schema and appending to cache")
val (keyType, valType) = type.arguments.map { it.type }
logger.debug("Obtained map types -> key: $keyType and value: $valType")
if (keyType?.classifier != String::class) {
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
}
var updatedCache = generateKTypeKontent(valType!!, cache)
val valClass = valType.classifier as KClass<*>
val valClassName = valClass.simpleName
val referenceName = genericNameAdapter(type, clazz)
val valueReference = when (valClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(valType)
AnyOfSchema(subTypes.map {
updatedCache = generateKTypeKontent(it, updatedCache)
updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
})
}
false -> updatedCache[valClassName] ?: error("$valClassName not found")
}
val schema = DictionarySchema(additionalProperties = valueReference)
updatedCache = generateKontent(valType, updatedCache)
return updatedCache.plus(referenceName to schema)
}
/**
* Handler for when a [Collection] is encountered
* @param type Collection type information
* @param clazz Collection class information
* @param cache Existing schema map to append to
*/
private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
logger.debug("Collection detected for $type, generating schema and appending to cache")
val collectionType = type.arguments.first().type!!
val collectionClass = collectionType.classifier as KClass<*>
logger.debug("Obtained collection class: $collectionClass")
val referenceName = genericNameAdapter(type, clazz)
var updatedCache = generateKTypeKontent(collectionType, cache)
val valueReference = when (collectionClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(collectionType)
AnyOfSchema(subTypes.map {
updatedCache = generateKTypeKontent(it, cache)
updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
})
}
false -> updatedCache[collectionClass.simpleName] ?: error("${collectionClass.simpleName} not found")
}
val schema = ArraySchema(items = valueReference)
updatedCache = generateKontent(collectionType, cache)
return updatedCache.plus(referenceName to schema)
}
}

View File

@ -1,23 +1,23 @@
package io.bkbn.kompendium
package io.bkbn.kompendium.core
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
import io.bkbn.kompendium.models.oas.OpenApiSpecReferencable
import io.bkbn.kompendium.models.oas.OpenApiSpecReferenceObject
import io.bkbn.kompendium.models.oas.OpenApiSpecRequest
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 io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kontent.generateKontent
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.MethodInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.util.Helpers
import io.bkbn.kompendium.core.util.Helpers.capitalized
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.oas.path.PathOperation
import io.bkbn.kompendium.oas.payload.MediaType
import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
@ -26,7 +26,8 @@ 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 java.util.Locale
import java.util.UUID
/**
* The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
@ -45,29 +46,19 @@ object MethodParser {
info: MethodInfo<*, *>,
paramType: KType,
requestType: KType,
responseType: KType
) = OpenApiSpecPathItemOperation(
responseType: KType,
feature: Kompendium
) = PathOperation(
summary = info.summary,
description = info.description,
operationId = info.operationId,
tags = info.tags,
deprecated = info.deprecated,
parameters = paramType.toParameterSpec(),
responses = responseType.toResponseSpec(info.responseInfo)?.let { mapOf(it) }.let {
when (it) {
null -> {
val throwables = parseThrowables(info.canThrow)
when (throwables.isEmpty()) {
true -> null
false -> throwables
}
}
else -> it.plus(parseThrowables(info.canThrow))
}
},
parameters = paramType.toParameterSpec(feature),
responses = parseResponse(responseType, info.responseInfo, feature).plus(parseExceptions(info.canThrow, feature)),
requestBody = when (info) {
is MethodInfo.PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
is MethodInfo.PostInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
is PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature)
is PostInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature)
else -> null
},
security = if (info.securitySchemes.isNotEmpty()) listOf(
@ -76,57 +67,55 @@ object MethodParser {
) else null
)
/**
* Adds the error to the [Kompendium.errorMap] for reference in notarized routes.
* @param errorType [KType] of the throwable being handled
* @param responseType [KType] the type of the response sent in event of error
*/
fun ResponseInfo<*>.parseErrorInfo(
errorType: KType,
responseType: KType
) {
Kompendium.errorMap = Kompendium.errorMap.plus(errorType to responseType.toResponseSpec(this))
private fun parseResponse(
responseType: KType,
responseInfo: ResponseInfo<*>?,
feature: Kompendium
): Map<Int, Response<*>> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
private fun parseExceptions(
exceptionInfo: Set<ExceptionInfo<*>>,
feature: Kompendium,
): Map<Int, Response<*>> = exceptionInfo.associate { info ->
feature.config.cache = generateKontent(info.responseType, feature.config.cache)
val response = Response(
description = info.description,
content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
)
Pair(info.status.value, response)
}
/**
* Parses possible errors thrown by a route
* @param throwables Set of classes that can be thrown
* @return Mapping of status codes to their corresponding error spec
*/
private fun parseThrowables(throwables: Set<KClass<*>>): Map<Int, OpenApiSpecReferencable> = throwables.mapNotNull {
Kompendium.errorMap[it.createType()]
}.toMap()
/**
* Converts a [KType] to an [OpenApiSpecRequest]
* Converts a [KType] to an [Request]
* @receiver [KType] to convert
* @param requestInfo request metadata
* @return Will return a generated [OpenApiSpecRequest] if requestInfo is not null
* @return Will return a generated [Request] if requestInfo is not null
*/
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?): OpenApiSpecRequest<*>? =
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request<*>? =
when (requestInfo) {
null -> null
else -> {
OpenApiSpecRequest(
Request(
description = requestInfo.description,
content = resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf()
content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf(),
required = requestInfo.required
)
}
}
/**
* Converts a [KType] to a pairing of http status code to [OpenApiSpecRequest]
* Converts a [KType] to a pairing of http status code to [Response]
* @receiver [KType] to convert
* @param responseInfo response metadata
* @return Will return a generated [Pair] if responseInfo is not null
*/
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?): Pair<Int, OpenApiSpecResponse<*>>? =
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response<*>>? =
when (responseInfo) {
null -> null
else -> {
val specResponse = OpenApiSpecResponse(
val specResponse = Response(
description = responseInfo.description,
content = resolveContent(this, responseInfo.mediaTypes, responseInfo.examples)
content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples)
)
Pair(responseInfo.status.value, specResponse)
}
@ -139,27 +128,27 @@ object MethodParser {
* @param examples Mapping of named examples of valid bodies.
* @return Named mapping of media types.
*/
private fun <F> resolveContent(
private fun <F> Kompendium.resolveContent(
type: KType,
mediaTypes: List<String>,
examples: Map<String, F>
): Map<String, OpenApiSpecMediaType<F>>? {
): Map<String, MediaType<F>>? {
val classifier = type.classifier as KClass<*>
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith {
val schema = if (classifier.isSealed) {
val refs = classifier.sealedSubclasses
.map { it.createType(type.arguments) }
.map { it.getReferenceSlug() }
.map { OpenApiSpecReferenceObject(it) }
OpenApiAnyOf(refs)
.map { it.getSimpleSlug() }
.map { config.cache[it] ?: error("$it not available") }
AnyOfSchema(refs)
} else {
val ref = type.getReferenceSlug()
OpenApiSpecReferenceObject(ref)
val ref = type.getSimpleSlug()
config.cache[ref] ?: error("$ref not available")
}
OpenApiSpecMediaType(
MediaType(
schema = schema,
examples = examples.mapValues { (_, v) -> ExampleWrapper(v) }.ifEmpty { null }
examples = examples.mapValues { (_, v) -> MediaType.Example(v) }.ifEmpty { null }
)
}
} else null
@ -167,29 +156,28 @@ object MethodParser {
/**
* Parses a type for all parameter information. All fields in the receiver
* must be annotated with [io.bkbn.kompendium.annotations.KompendiumParam].
* must be annotated with [io.bkbn.kompendium.annotations.Param].
* @receiver type
* @return list of valid parameter specs as detailed by the [KType] members
* @throws [IllegalStateException] if the class could not be parsed properly
*/
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
private fun KType.toParameterSpec(feature: Kompendium): List<Parameter> {
val clazz = classifier as KClass<*>
return clazz.memberProperties.filter { prop ->
prop.findAnnotation<KompendiumParam>() != null
prop.findAnnotation<Param>() != null
}.map { prop ->
val field = prop.javaField?.type?.kotlin
?: error("Unable to parse field type from $prop")
val anny = prop.findAnnotation<KompendiumParam>()
val wrapperSchema = feature.config.cache[this.getSimpleSlug()]!! as ObjectSchema
val anny = prop.findAnnotation<Param>()
?: error("Field ${prop.name} is not annotated with KompendiumParam")
val schema = Kompendium.cache[field.getSimpleSlug(prop)]
val schema = wrapperSchema.properties[prop.name]
?: error("Could not find component type for $prop")
val defaultValue = getDefaultParameterValue(clazz, prop)
OpenApiSpecParameter(
Parameter(
name = prop.name,
`in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue),
description = anny.description.ifBlank { null },
required = !prop.returnType.isMarkedNullable
description = schema.description,
required = !prop.returnType.isMarkedNullable && defaultValue == null
)
}
}
@ -215,7 +203,7 @@ object MethodParser {
.associateWith { defaultValueInjector(it) }
val instance = constructor.callBy(values)
val methods = clazz.java.methods
val getterName = "get${prop.name.capitalize()}"
val getterName = "get${prop.name.capitalized()}"
val getterFunction = methods.find { it.name == getterName }
?: error("Could not associate ${prop.name} with a getter")
return getterFunction.invoke(instance)

View File

@ -0,0 +1,114 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.ktor.application.ApplicationCall
import io.ktor.application.feature
import io.ktor.http.HttpMethod
import io.ktor.routing.Route
import io.ktor.routing.application
import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineInterceptor
/**
* Notarization methods are the primary way that a Ktor API using Kompendium differentiates
* from a default Ktor application. On instantiation, a notarized route, provided with the proper metadata,
* will reflectively analyze all pertinent data to build a corresponding OpenAPI entry.
*/
object Notarized {
/**
* Notarization for an HTTP GET request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: GetInfo<TParam, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.get = postProcess(baseInfo)
return method(HttpMethod.Get) { handle(body) }
}
/**
* Notarization for an HTTP POST request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: PostInfo<TParam, TReq, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, TReq, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.post = postProcess(baseInfo)
return method(HttpMethod.Post) { handle(body) }
}
/**
* Notarization for an HTTP Delete request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: PutInfo<TParam, TReq, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route = methodNotarizationPreFlight<TParam, TReq, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.put = postProcess(baseInfo)
return method(HttpMethod.Put) { handle(body) }
}
/**
* Notarization for an HTTP POST request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: DeleteInfo<TParam, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.delete = postProcess(baseInfo)
return method(HttpMethod.Delete) { handle(body) }
}
/**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything
* that matches the RegEx pattern `/\\(.+\\)`
*/
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
}

View File

@ -0,0 +1,12 @@
package io.bkbn.kompendium.core.metadata
import io.ktor.http.HttpStatusCode
import kotlin.reflect.KType
data class ExceptionInfo<TResp : Any>(
val responseType: KType,
val status: HttpStatusCode,
val description: String,
val mediaTypes: List<String> = listOf("application/json"),
val examples: Map<String, TResp> = emptyMap()
)

View File

@ -1,4 +1,4 @@
package io.bkbn.kompendium.models.meta
package io.bkbn.kompendium.core.metadata
data class RequestInfo<TReq>(
val description: String,

View File

@ -1,4 +1,4 @@
package io.bkbn.kompendium.models.meta
package io.bkbn.kompendium.core.metadata
import io.ktor.http.HttpStatusCode

View File

@ -0,0 +1,5 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.schema.ComponentSchema
typealias SchemaMap = Map<String, ComponentSchema>

View File

@ -0,0 +1,6 @@
package io.bkbn.kompendium.core.metadata
import kotlin.reflect.KTypeParameter
import kotlin.reflect.KTypeProjection
typealias TypeMap = Map<KTypeParameter, KTypeProjection>

View File

@ -0,0 +1,16 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class DeleteInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,16 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class GetInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,24 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
sealed interface MethodInfo<TParam, TResp> {
val summary: String
val description: String?
get() = null
val tags: Set<String>
get() = emptySet()
val deprecated: Boolean
get() = false
val securitySchemes: Set<String>
get() = emptySet()
val canThrow: Set<ExceptionInfo<*>>
get() = emptySet()
val responseInfo: ResponseInfo<TResp>
// TODO Is this even used anywhere?
val parameterExamples: Map<String, TParam>
get() = emptyMap()
val operationId: String?
get() = null
}

View File

@ -0,0 +1,18 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class PostInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>?,
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,18 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class PutInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>,
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,4 +1,4 @@
package io.bkbn.kompendium.routes
package io.bkbn.kompendium.core.routes
import io.ktor.application.call
import io.ktor.html.respondHtml
@ -13,20 +13,19 @@ import kotlinx.html.script
import kotlinx.html.style
import kotlinx.html.title
import kotlinx.html.unsafe
import io.bkbn.kompendium.models.oas.OpenApiSpec
/**
* Provides an out-of-the-box route to view docs using ReDoc
* @param oas spec to reference
* @param pageTitle Webpage title you wish to be displayed on your docs
* @param specUrl url to point ReDoc to the OpenAPI json document
*/
fun Routing.redoc(oas: OpenApiSpec, specUrl: String = "/openapi.json") {
fun Routing.redoc(pageTitle: String = "Docs", specUrl: String = "/openapi.json") {
route("/docs") {
get {
call.respondHtml {
head {
title {
+"${oas.info.title}"
+"$pageTitle"
}
meta {
charset = "utf-8"

View File

@ -1,6 +1,5 @@
package io.bkbn.kompendium.util
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.util.Helpers.getReferenceSlug
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
@ -8,17 +7,18 @@ import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory
import java.util.Locale
object Helpers {
private val logger = LoggerFactory.getLogger(javaClass)
const val COMPONENT_SLUG = "#/components/schemas"
private const val COMPONENT_SLUG = "#/components/schemas"
val UNIT_TYPE by lazy { Unit::class.createType() }
/**
* 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 object and will log their state ahead of function invocation
* along with the result of the function invocation
*/
fun <T> logged(functionName: String, entities: Map<String, Any>, block: () -> T): T {
@ -70,4 +70,16 @@ object Helpers {
.map { it.simpleName }
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-")
}
fun String.capitalized() = replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
fun String.toNumber(): Number {
return try {
this.toInt()
} catch (e: NumberFormatException) {
this.toDouble()
}
}
}

View File

@ -1,6 +0,0 @@
package io.bkbn.kompendium.models.meta
import kotlin.reflect.KType
import io.bkbn.kompendium.models.oas.OpenApiSpecResponse
typealias ErrorMap = Map<KType, Pair<Int, OpenApiSpecResponse<*>>?>

View File

@ -1,104 +0,0 @@
package io.bkbn.kompendium.models.meta
import kotlin.reflect.KClass
sealed class MethodInfo<TParam, TResp>(
open val summary: String,
open val description: String? = null,
open val tags: Set<String> = emptySet(),
open val deprecated: Boolean = false,
open val securitySchemes: Set<String> = emptySet(),
open val canThrow: Set<KClass<*>> = emptySet(),
open val responseInfo: ResponseInfo<TResp>? = null,
open val parameterExamples: Map<String, TParam> = emptyMap(),
open val operationId: String? = null
) {
data class GetInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
responseInfo = responseInfo,
parameterExamples = parameterExamples,
operationId = operationId
)
data class PostInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>? = null,
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
responseInfo = responseInfo,
parameterExamples = parameterExamples,
operationId = operationId
)
data class PutInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>? = null,
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
parameterExamples = parameterExamples,
operationId = operationId
)
data class DeleteInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
parameterExamples = parameterExamples,
operationId = operationId
)
}

View File

@ -1,5 +0,0 @@
package io.bkbn.kompendium.models.meta
import io.bkbn.kompendium.models.oas.OpenApiSpecComponentSchema
typealias SchemaMap = Map<String, OpenApiSpecComponentSchema>

View File

@ -1,14 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpec(
val openapi: String = "3.0.3",
val info: OpenApiSpecInfo,
// TODO Needs to default to server object with url of `/`
val servers: MutableList<OpenApiSpecServer> = mutableListOf(),
val paths: MutableMap<String, OpenApiSpecPathItem> = mutableMapOf(),
val components: OpenApiSpecComponents = OpenApiSpecComponents(),
// todo needs to reference objects in the components -> security scheme 🤔
val security: MutableList<Map<String, List<String>>> = mutableListOf(),
val tags: MutableList<OpenApiSpecTag> = mutableListOf(),
val externalDocs: OpenApiSpecExternalDocumentation? = null
)

View File

@ -1,42 +0,0 @@
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)
is EnumSchema -> this.copy(default = default)
is SimpleSchema -> this.copy(default = default)
is FormatSchema -> this.copy(default = default)
is ArraySchema -> this.copy(default = 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 AnyOfReferencedSchema(val anyOf: List<ReferencedSchema>) : OpenApiSpecComponentSchema()
data class ObjectSchema(
val properties: Map<String, OpenApiSpecComponentSchema>,
override val default: Any? = null
) : TypedSchema("object", default)
data class DictionarySchema(
val additionalProperties: OpenApiSpecComponentSchema,
override val default: Any? = null
) : TypedSchema("object", default)
data class EnumSchema(
val `enum`: Set<String>, override val default: Any? = null
) : TypedSchema("string", default)
data class SimpleSchema(override val type: String, override val default: Any? = null) : TypedSchema(type, default)
data class FormatSchema(val format: String, override val type: String, override val default: Any? = null) :
TypedSchema(type, default)
data class ArraySchema(val items: OpenApiSpecComponentSchema, override val default: Any? = null) :
TypedSchema("array", default)

View File

@ -1,7 +0,0 @@
package io.bkbn.kompendium.models.oas
// TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object
data class OpenApiSpecComponents(
val schemas: MutableMap<String, OpenApiSpecComponentSchema> = mutableMapOf(),
val securitySchemes: MutableMap<String, OpenApiSpecSchemaSecurity> = mutableMapOf()
)

View File

@ -1,8 +0,0 @@
package io.bkbn.kompendium.models.oas
import java.net.URI
data class OpenApiSpecExternalDocumentation(
val url: URI,
val description: String?
)

View File

@ -1,12 +0,0 @@
package io.bkbn.kompendium.models.oas
import java.net.URI
data class OpenApiSpecInfo(
var title: String? = null,
var version: String? = null,
var description: String? = null,
var termsOfService: URI? = null,
var contact: OpenApiSpecInfoContact? = null,
var license: OpenApiSpecInfoLicense? = null
)

View File

@ -1,9 +0,0 @@
package io.bkbn.kompendium.models.oas
import java.net.URI
data class OpenApiSpecInfoContact(
var name: String,
var url: URI? = null,
var email: String? = null // TODO Enforce email?
)

View File

@ -1,8 +0,0 @@
package io.bkbn.kompendium.models.oas
import java.net.URI
data class OpenApiSpecInfoLicense(
var name: String,
var url: URI? = null
)

View File

@ -1,10 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecLink(
val operationRef: String?, // todo mutually exclusive with operationId
val operationId: String?,
val parameters: Map<String, String>, // todo sheesh https://swagger.io/specification/#link-object
val requestBody: String, // todo same
val description: String?,
val server: OpenApiSpecServer?
)

View File

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

View File

@ -1,10 +0,0 @@
package io.bkbn.kompendium.models.oas
import java.net.URI
data class OpenApiSpecOAuthFlow(
val authorizationUrl: URI? = null,
val tokenUrl: URI? = null,
val refreshUrl: URI? = null,
val scopes: Map<String, String>? = null
)

View File

@ -1,5 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecOAuthFlows(
val implicit: OpenApiSpecOAuthFlow?,
)

View File

@ -1,14 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecPathItem(
var get: OpenApiSpecPathItemOperation? = null,
var put: OpenApiSpecPathItemOperation? = null,
var post: OpenApiSpecPathItemOperation? = null,
var delete: OpenApiSpecPathItemOperation? = null,
var options: OpenApiSpecPathItemOperation? = null,
var head: OpenApiSpecPathItemOperation? = null,
var patch: OpenApiSpecPathItemOperation? = null,
var trace: OpenApiSpecPathItemOperation? = null,
var servers: List<OpenApiSpecServer>? = null,
var parameters: List<OpenApiSpecReferencable>? = null
)

View File

@ -1,19 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecPathItemOperation(
var tags: Set<String> = emptySet(),
var summary: String? = null,
var description: String? = null,
var externalDocs: OpenApiSpecExternalDocumentation? = null,
var operationId: String? = null,
var parameters: List<OpenApiSpecReferencable>? = null,
var requestBody: OpenApiSpecReferencable? = null,
// TODO How to enforce `default` requirement 🧐
var responses: Map<Int, OpenApiSpecReferencable>? = null,
var callbacks: Map<String, OpenApiSpecReferencable>? = null,
var deprecated: Boolean = false,
// todo big yikes... also needs to reference objects in the security scheme 🤔
var security: List<Map<String, List<String>>>? = null,
var servers: List<OpenApiSpecServer>? = null,
var `x-codegen-request-body-name`: String? = null
)

View File

@ -1,31 +0,0 @@
package io.bkbn.kompendium.models.oas
sealed interface 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
data class OpenApiSpecParameter(
val name: String,
val `in`: String, // TODO Enum? "query", "header", "path" or "cookie"
val schema: OpenApiSpecComponentSchema,
val description: String? = null,
val required: Boolean = true,
val deprecated: Boolean = false,
val allowEmptyValue: Boolean? = null,
val style: String? = null,
val explode: Boolean? = null
) : OpenApiSpecReferencable
data class OpenApiSpecRequest<T>(
val description: String?,
val content: Map<String, OpenApiSpecMediaType<T>>,
val required: Boolean = false
) : OpenApiSpecReferencable

View File

@ -1,11 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecSchemaSecurity(
val type: String? = null, // TODO Enum? "apiKey", "http", "oauth2", "openIdConnect"
val name: String? = null,
val `in`: String? = null,
val scheme: String? = null,
val flows: OpenApiSpecOAuthFlows? = null,
val bearerFormat: String? = null,
val description: String? = null,
)

View File

@ -1,9 +0,0 @@
package io.bkbn.kompendium.models.oas
import java.net.URI
data class OpenApiSpecServer(
val url: URI,
val description: String? = null,
var variables: Map<String, OpenApiSpecServerVariable>? = null
)

View File

@ -1,7 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecServerVariable(
val `enum`: Set<String>, // todo enforce not empty
val default: String,
val description: String?
)

View File

@ -1,7 +0,0 @@
package io.bkbn.kompendium.models.oas
data class OpenApiSpecTag(
val name: String,
val description: String? = null,
val externalDocs: OpenApiSpecExternalDocumentation? = null
)

View File

@ -1,13 +0,0 @@
package io.bkbn.kompendium.path
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import kotlin.reflect.KClass
interface IPathCalculator {
fun calculate(route: Route?, tail: String = ""): String
fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: IPathCalculator.(Route, String) -> String
)
}

View File

@ -1,54 +0,0 @@
package io.bkbn.kompendium.path
import io.ktor.routing.PathSegmentConstantRouteSelector
import io.ktor.routing.PathSegmentParameterRouteSelector
import io.ktor.routing.RootRouteSelector
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import io.ktor.routing.TrailingSlashRouteSelector
import io.ktor.util.InternalAPI
import kotlin.reflect.KClass
/**
* Responsible for calculating a url path from a provided [Route]
*/
@OptIn(InternalAPI::class)
internal object PathCalculator: IPathCalculator {
private val pathHandler: RouteHandlerMap = mutableMapOf()
init {
addCustomRouteHandler(RootRouteSelector::class) { _, tail -> tail }
addCustomRouteHandler(PathSegmentParameterRouteSelector::class) { route, tail ->
val newTail = "/${route.selector}$tail"
calculate(route.parent, newTail)
}
addCustomRouteHandler(PathSegmentConstantRouteSelector::class) { route, tail ->
val newTail = "/${route.selector}$tail"
calculate(route.parent, newTail)
}
addCustomRouteHandler(TrailingSlashRouteSelector::class) { route, tail ->
val newTail = tail.ifBlank { "/" }
calculate(route.parent, newTail)
}
}
@OptIn(InternalAPI::class)
override fun calculate(
route: Route?,
tail: String
): String = when (route) {
null -> tail
else -> when (pathHandler.containsKey(route.selector::class)) {
true -> pathHandler[route.selector::class]!!.invoke(this, route, tail)
else -> error("No handler has been registered for ${route.selector}")
}
}
override fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: IPathCalculator.(Route, String) -> String
) {
pathHandler[selector] = handler
}
}

View File

@ -1,7 +0,0 @@
package io.bkbn.kompendium.path
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import kotlin.reflect.KClass
typealias RouteHandlerMap = MutableMap<KClass<out RouteSelector>, IPathCalculator.(Route, String) -> String>

View File

@ -1,31 +0,0 @@
package io.bkbn.kompendium.routes
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.models.oas.OpenApiSpec
import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.route
/**
* Provides an out-of-the-box route to return the generated [OpenApiSpec]
* @param oas spec that is returned
* @param om provider for Jackson
*/
fun Routing.openApi(
oas: OpenApiSpec,
om: ObjectMapper = objectMapper
) {
route("/openapi.json") {
get {
call.respondText { om.writeValueAsString(oas) }
}
}
}
private val objectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)

View File

@ -1,643 +0,0 @@
package io.bkbn.kompendium
import io.ktor.application.Application
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication
import java.net.URI
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
import io.bkbn.kompendium.models.oas.OpenApiSpecInfoContact
import io.bkbn.kompendium.models.oas.OpenApiSpecInfoLicense
import io.bkbn.kompendium.models.oas.OpenApiSpecServer
import io.bkbn.kompendium.routes.openApi
import io.bkbn.kompendium.routes.redoc
import io.bkbn.kompendium.util.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.util.complexType
import io.bkbn.kompendium.util.jacksonConfigModule
import io.bkbn.kompendium.util.emptyGet
import io.bkbn.kompendium.util.genericPolymorphicResponse
import io.bkbn.kompendium.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.util.headerParameter
import io.bkbn.kompendium.util.kotlinxConfigModule
import io.bkbn.kompendium.util.nestedUnderRootModule
import io.bkbn.kompendium.util.nonRequiredParamsGet
import io.bkbn.kompendium.util.notarizedDeleteModule
import io.bkbn.kompendium.util.notarizedGetModule
import io.bkbn.kompendium.util.notarizedGetWithMultipleThrowables
import io.bkbn.kompendium.util.notarizedGetWithNotarizedException
import io.bkbn.kompendium.util.notarizedPostModule
import io.bkbn.kompendium.util.notarizedPutModule
import io.bkbn.kompendium.util.pathParsingTestModule
import io.bkbn.kompendium.util.polymorphicCollectionResponse
import io.bkbn.kompendium.util.polymorphicInterfaceResponse
import io.bkbn.kompendium.util.polymorphicMapResponse
import io.bkbn.kompendium.util.polymorphicResponse
import io.bkbn.kompendium.util.primitives
import io.bkbn.kompendium.util.returnsList
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
import io.bkbn.kompendium.util.undeclaredType
import io.bkbn.kompendium.util.withDefaultParameter
import io.bkbn.kompendium.util.withExamples
import io.bkbn.kompendium.util.withOperationId
internal class KompendiumTest {
@AfterTest
fun `reset kompendium`() {
Kompendium.resetSchema()
}
@Test
fun `Kompendium can be instantiated with no details`() {
assertEquals(Kompendium.openApiSpec.openapi, "3.0.3", "Kompendium has a default spec version of 3.0.3")
}
@Test
fun `Notarized Get records all expected information`() {
withTestApplication({
kotlinxConfigModule()
docs()
notarizedGetModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Get does not interrupt the pipeline`() {
withTestApplication({
jacksonConfigModule()
docs()
notarizedGetModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/test").response.content
// expect
val expected = "hey dude ‼️ congratz on the get request"
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Post records all expected information`() {
withTestApplication({
jacksonConfigModule()
docs()
notarizedPostModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_post.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized post does not interrupt the pipeline`() {
withTestApplication({
kotlinxConfigModule()
docs()
notarizedPostModule()
}) {
// do
val json = handleRequest(HttpMethod.Post, "/test").response.content
// expect
val expected = "hey dude ✌️ congratz on the post request"
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Put records all expected information`() {
withTestApplication({
jacksonConfigModule()
docs()
notarizedPutModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_put.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized put does not interrupt the pipeline`() {
withTestApplication({
jacksonConfigModule()
docs()
notarizedPutModule()
}) {
// do
val json = handleRequest(HttpMethod.Put, "/test").response.content
// expect
val expected = "hey pal 🌝 whatcha doin' here?"
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized delete records all expected information`() {
withTestApplication({
jacksonConfigModule()
docs()
notarizedDeleteModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_delete.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized delete does not interrupt the pipeline`() {
withTestApplication({
jacksonConfigModule()
docs()
notarizedDeleteModule()
}) {
// do
val status = handleRequest(HttpMethod.Delete, "/test").response.status()
// expect
assertEquals(HttpStatusCode.NoContent, status, "No content status should be received")
}
}
@Test
fun `Path parser stores the expected path`() {
withTestApplication({
jacksonConfigModule()
docs()
pathParsingTestModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("path_parser.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can notarize the root route`() {
withTestApplication({
jacksonConfigModule()
docs()
rootModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("root_route.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can call the root route`() {
withTestApplication({
kotlinxConfigModule()
docs()
rootModule()
}) {
// do
val result = handleRequest(HttpMethod.Get, "/").response.content
// expect
val expected = "☎️🏠🌲"
assertEquals(expected, result, "Should be the same")
}
}
@Test
fun `Nested under root module does not append trailing slash`() {
withTestApplication({
jacksonConfigModule()
docs()
nestedUnderRootModule()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("nested_under_root.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can notarize a trailing slash route`() {
withTestApplication({
jacksonConfigModule()
docs()
trailingSlash()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("trailing_slash.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can call a trailing slash route`() {
withTestApplication({
jacksonConfigModule()
docs()
trailingSlash()
}) {
// do
val result = handleRequest(HttpMethod.Get, "/test/").response.content
// expect
val expected = "🙀👾"
assertEquals(expected, result, "Should be the same")
}
}
@Test
fun `Can notarize a complex type`() {
withTestApplication({
jacksonConfigModule()
docs()
complexType()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("complex_type.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can notarize primitives`() {
withTestApplication({
jacksonConfigModule()
docs()
primitives()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_primitives.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can notarize a top level list response`() {
withTestApplication({
jacksonConfigModule()
docs()
returnsList()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("response_list.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can notarize route with no request params and no response body`() {
withTestApplication({
kotlinxConfigModule()
docs()
emptyGet()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("no_request_params_and_no_response_body.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can notarize route with non-required params`() {
withTestApplication({
jacksonConfigModule()
docs()
nonRequiredParamsGet()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("non_required_params.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can add operationId`() {
withTestApplication({
jacksonConfigModule()
docs()
withOperationId()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_get_with_operation_id.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Generates the expected redoc`() {
withTestApplication({
jacksonConfigModule()
docs()
returnsList()
}) {
// do
val html = handleRequest(HttpMethod.Get, "/docs").response.content
// expected
val expected = getFileSnapshot("redoc.html")
assertEquals(expected, html)
}
}
@Test
fun `Generates additional responses when passed a throwable`() {
withTestApplication({
statusPageModule()
jacksonConfigModule()
docs()
notarizedGetWithNotarizedException()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_get_with_exception_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Generates additional responses when passed multiple throwables`() {
withTestApplication({
statusPageMultiExceptions()
jacksonConfigModule()
docs()
notarizedGetWithMultipleThrowables()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_get_with_multiple_exception_responses.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate example response and request bodies`() {
withTestApplication({
kotlinxConfigModule()
docs()
withExamples()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("example_req_and_resp.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a default parameter value`() {
withTestApplication({
jacksonConfigModule()
docs()
withDefaultParameter()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("query_with_default_parameter.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
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 collection with polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicCollectionResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("polymorphic_list_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a map with a polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicMapResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("polymorphic_map_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a polymorphic response from a sealed interface`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicInterfaceResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("sealed_interface_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a response type with a generic type`() {
withTestApplication({
jacksonConfigModule()
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({
jacksonConfigModule()
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({
kotlinxConfigModule()
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")
}
}
@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")
}
}
@Test
fun `Can add a custom header parameter with a name override`() {
withTestApplication({
jacksonConfigModule()
docs()
headerParameter()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("override_parameter_name.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo(
title = "Test API",
version = "1.33.7",
description = "An amazing, fully-ish 😉 generated API spec",
termsOfService = URI("https://example.com"),
contact = OpenApiSpecInfoContact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = OpenApiSpecInfoLicense(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
OpenApiSpecServer(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
OpenApiSpecServer(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
private fun Application.docs() {
routing {
openApi(oas)
redoc(oas)
}
}
}

View File

@ -1,204 +0,0 @@
package io.bkbn.kompendium
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import io.bkbn.kompendium.Kontent.generateKontent
import io.bkbn.kompendium.Kontent.generateParameterKontent
import io.bkbn.kompendium.models.oas.DictionarySchema
import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.models.oas.ObjectSchema
import io.bkbn.kompendium.models.oas.ReferencedSchema
import io.bkbn.kompendium.util.*
@ExperimentalStdlibApi
internal class KontentTest {
@Test
fun `Unit returns empty map on generate`() {
// do
val result = generateKontent<Unit>()
// expect
assertTrue { result.isEmpty() }
}
@Test
fun `Primitive types return a single map result`() {
// do
val result = generateKontent<Long>()
// expect
assertEquals(1, result.count(), "Should have a single result")
assertEquals(FormatSchema("int64", "integer"), result["Long"])
}
@Test
fun `Object with BigDecimal and BigInteger types`() {
// do
val result = generateKontent<TestBigNumberModel>()
// expect
assertEquals(3, result.count())
assertTrue { result.containsKey(TestBigNumberModel::class.simpleName) }
assertEquals(FormatSchema("double", "number"), result["BigDecimal"])
assertEquals(FormatSchema("int64", "integer"), result["BigInteger"])
}
@Test
fun `Object with ByteArray type`() {
// do
val result = generateKontent<TestByteArrayModel>()
// expect
assertEquals(2, result.count())
assertTrue { result.containsKey(TestByteArrayModel::class.simpleName) }
assertEquals(FormatSchema("byte", "string"), result["ByteArray"])
}
@Test
fun `Objects reference their base types in the cache`() {
// do
val result = generateKontent<TestSimpleModel>()
// expect
assertNotNull(result)
assertEquals(3, result.count())
assertTrue { result.containsKey(TestSimpleModel::class.simpleName) }
}
@Test
fun `generation works for nested object types`() {
// do
val result = generateKontent<TestNestedModel>()
// expect
assertNotNull(result)
assertEquals(4, result.count())
assertTrue { result.containsKey(TestNestedModel::class.simpleName) }
assertTrue { result.containsKey(TestSimpleModel::class.simpleName) }
}
@Test
fun `generation does not repeat for cached items`() {
// when
val clazz = TestNestedModel::class
val initialCache = generateKontent<TestNestedModel>()
// do
val result = generateKontent<TestSimpleModel>(initialCache)
// expect TODO Spy to check invocation count?
assertNotNull(result)
assertEquals(4, result.count())
assertTrue { result.containsKey(clazz.simpleName) }
assertTrue { result.containsKey(TestSimpleModel::class.simpleName) }
}
@Test
fun `generation allows for enum fields`() {
// do
val result = generateKontent<TestSimpleWithEnums>()
// expect
assertNotNull(result)
assertEquals(3, result.count())
assertTrue { result.containsKey(TestSimpleWithEnums::class.simpleName) }
}
@Test
fun `generation allows for map fields`() {
// do
val result = generateKontent<TestSimpleWithMap>()
// expect
assertNotNull(result)
assertEquals(5, result.count())
assertTrue { result.containsKey("Map-String-TestSimpleModel") }
assertTrue { result.containsKey(TestSimpleWithMap::class.simpleName) }
val os = result[TestSimpleWithMap::class.simpleName] as ObjectSchema
val expectedRef = ReferencedSchema("#/components/schemas/Map-String-TestSimpleModel")
assertEquals(expectedRef, os.properties["b"])
}
@Test
fun `map fields that are not string result in error`() {
// expect
assertFailsWith<IllegalStateException> { generateKontent<TestInvalidMap>() }
}
@Test
fun `generation allows for collection fields`() {
// do
val result = generateKontent<TestSimpleWithList>()
// expect
assertNotNull(result)
assertEquals(6, result.count())
assertTrue { result.containsKey("List-TestSimpleModel") }
assertTrue { result.containsKey(TestSimpleWithList::class.simpleName) }
}
@Test
fun `Can parse enum list as a field`() {
// do
val result = generateKontent<TestSimpleWithEnumList>()
// expect
assertNotNull(result)
}
@Test
fun `UUID schema support`() {
// do
val result = generateKontent<TestWithUUID>()
// expect
assertNotNull(result)
assertEquals(2, result.count())
assertTrue { result.containsKey(UUID::class.simpleName) }
assertTrue { result.containsKey(TestWithUUID::class.simpleName) }
val expectedSchema = result[UUID::class.simpleName] as FormatSchema
assertEquals(FormatSchema("uuid", "string"), expectedSchema)
}
@Test
fun `Generate top level list response`() {
// do
val result = generateKontent<List<TestSimpleModel>>()
// expect
assertNotNull(result)
}
@Test
fun `Can handle a complex type`() {
// do
val result = generateKontent<ComplexRequest>()
// expect
assertNotNull(result)
assertEquals(7, result.count())
assertTrue { result.containsKey("Map-String-CrazyItem") }
val ds = result["Map-String-CrazyItem"] as DictionarySchema
val rs = ds.additionalProperties as ReferencedSchema
assertEquals(ReferencedSchema("#/components/schemas/CrazyItem"), rs)
}
@Test
fun `Parameter kontent filters out top level declaration`() {
// do
val result = generateParameterKontent<TestSimpleModel>()
// expect
assertNotNull(result)
assertEquals(2, result.count())
assertFalse { result.containsKey(TestSimpleModel::class.simpleName) }
}
}

View File

@ -0,0 +1,276 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers.apiFunctionalityTest
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTest
import io.bkbn.kompendium.core.util.complexType
import io.bkbn.kompendium.core.util.constrainedDoubleInfo
import io.bkbn.kompendium.core.util.constrainedIntInfo
import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.exclusiveMinMax
import io.bkbn.kompendium.core.util.formattedParam
import io.bkbn.kompendium.core.util.freeFormObject
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.headerParameter
import io.bkbn.kompendium.core.util.minMaxArray
import io.bkbn.kompendium.core.util.minMaxFreeForm
import io.bkbn.kompendium.core.util.minMaxString
import io.bkbn.kompendium.core.util.multipleOfDouble
import io.bkbn.kompendium.core.util.multipleOfInt
import io.bkbn.kompendium.core.util.nestedUnderRootModule
import io.bkbn.kompendium.core.util.nonRequiredParamsGet
import io.bkbn.kompendium.core.util.notarizedDeleteModule
import io.bkbn.kompendium.core.util.notarizedGetModule
import io.bkbn.kompendium.core.util.notarizedGetWithGenericErrorResponse
import io.bkbn.kompendium.core.util.notarizedGetWithMultipleThrowables
import io.bkbn.kompendium.core.util.notarizedGetWithNotarizedException
import io.bkbn.kompendium.core.util.notarizedGetWithPolymorphicErrorResponse
import io.bkbn.kompendium.core.util.notarizedPostModule
import io.bkbn.kompendium.core.util.notarizedPutModule
import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.overrideFieldInfo
import io.bkbn.kompendium.core.util.pathParsingTestModule
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.polymorphicInterfaceResponse
import io.bkbn.kompendium.core.util.polymorphicMapResponse
import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.primitives
import io.bkbn.kompendium.core.util.regexString
import io.bkbn.kompendium.core.util.requiredParameter
import io.bkbn.kompendium.core.util.returnsList
import io.bkbn.kompendium.core.util.rootModule
import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.core.util.undeclaredType
import io.bkbn.kompendium.core.util.uniqueArray
import io.bkbn.kompendium.core.util.withDefaultParameter
import io.bkbn.kompendium.core.util.withExamples
import io.bkbn.kompendium.core.util.withOperationId
import io.kotest.core.spec.style.DescribeSpec
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") {
it("Can notarize a get request") {
// act
openApiTest("notarized_get.json") { notarizedGetModule() }
}
it("Can notarize a post request") {
// act
openApiTest("notarized_post.json") { notarizedPostModule() }
}
it("Can notarize a put request") {
// act
openApiTest("notarized_put.json") { notarizedPutModule() }
}
it("Can notarize a delete request") {
// act
openApiTest("notarized_delete.json") { notarizedDeleteModule() }
}
it("Can notarize a complex type") {
// act
openApiTest("complex_type.json") { complexType() }
}
it("Can notarize primitives") {
// act
openApiTest("notarized_primitives.json") { primitives() }
}
it("Can notarize a top level list response") {
// act
openApiTest("response_list.json") { returnsList() }
}
it("Can notarize a route with non-required params") {
// act
openApiTest("non_required_params.json") { nonRequiredParamsGet() }
}
}
describe("Notarized Ktor Functionality Tests") {
it("Can notarized a get request and return the expected result") {
// act
apiFunctionalityTest("hey dude ‼️ congratz on the get request") { notarizedGetModule() }
}
it("Can notarize a post request and return the expected result") {
// act
apiFunctionalityTest(
"hey dude ✌️ congratz on the post request",
httpMethod = HttpMethod.Post
) { notarizedPostModule() }
}
it("Can notarize a put request and return the expected result") {
// act
apiFunctionalityTest("hey pal 🌝 whatcha doin' here?", httpMethod = HttpMethod.Put) { notarizedPutModule() }
}
it("Can notarize a delete request and return the expected result") {
// act
apiFunctionalityTest(
null,
httpMethod = HttpMethod.Delete,
expectedStatusCode = HttpStatusCode.NoContent
) { notarizedDeleteModule() }
}
it("Can notarize the root route and return the expected result") {
// act
apiFunctionalityTest("☎️🏠🌲", "/") { rootModule() }
}
it("Can notarize a trailing slash route and return the expected result") {
// act
apiFunctionalityTest("🙀👾", "/test/") { trailingSlash() }
}
}
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
// act
openApiTest("path_parser.json") { pathParsingTestModule() }
}
it("Can notarize the root route") {
// act
openApiTest("root_route.json") { rootModule() }
}
it("Can notarize a route under the root module without appending trailing slash") {
// act
openApiTest("nested_under_root.json") { nestedUnderRootModule() }
}
it("Can notarize a route with a trailing slash") {
// act
openApiTest("trailing_slash.json") { trailingSlash() }
}
}
describe("Exceptions") {
it("Can add an exception status code to a response") {
// act
openApiTest("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() }
}
it("Can support multiple response codes") {
// act
openApiTest("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() }
}
it("Can add a polymorphic exception response") {
// act
openApiTest("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() }
}
it("Can add a generic exception response") {
// act
openApiTest("generic_exception.json") { notarizedGetWithGenericErrorResponse() }
}
}
describe("Examples") {
it("Can generate example response and request bodies") {
// act
openApiTest("example_req_and_resp.json") { withExamples() }
}
}
describe("Defaults") {
it("Can generate a default parameter values") {
// act
openApiTest("query_with_default_parameter.json") { withDefaultParameter() }
}
}
describe("Required Fields") {
it("Marks a parameter required if there is no default and it is not marked nullable") {
openApiTest("required_param.json") { requiredParameter() }
}
it("Does not mark a parameter as required if a default value is provided") {
openApiTest("default_param.json") { defaultParameter() }
}
it("Does not mark a field as required if a default value is provided") {
openApiTest("default_field.json") { defaultField() }
}
it("Marks a field as nullable when expected") {
openApiTest("nullable_field.json") { nullableField() }
}
}
describe("Polymorphism and Generics") {
it("can generate a polymorphic response type") {
// act
openApiTest("polymorphic_response.json") { polymorphicResponse() }
}
it("Can generate a collection with polymorphic response type") {
// act
openApiTest("polymorphic_list_response.json") { polymorphicCollectionResponse() }
}
it("Can generate a map with a polymorphic response type") {
// act
openApiTest("polymorphic_map_response.json") { polymorphicMapResponse() }
}
it("Can generate a polymorphic response from a sealed interface") {
// act
openApiTest("sealed_interface_response.json") { polymorphicInterfaceResponse() }
}
it("Can generate a response type with a generic type") {
// act
openApiTest("generic_response.json") { simpleGenericResponse() }
}
it("Can generate a polymorphic response type with generics") {
// act
openApiTest("polymorphic_response_with_generics.json") { genericPolymorphicResponse() }
}
it("Can handle an absolutely psycho inheritance test") {
// act
openApiTest("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
}
}
describe("Miscellaneous") {
it("Can generate the necessary ReDoc home page") {
// act
apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
}
it("Can add an operation id to a notarized route") {
// act
openApiTest("notarized_get_with_operation_id.json") { withOperationId() }
}
it("Can add an undeclared field") {
// act
openApiTest("undeclared_field.json") { undeclaredType() }
}
it("Can add a custom header parameter with a name override") {
// act
openApiTest("override_parameter_name.json") { headerParameter() }
}
it("Can override field values via annotation") {
openApiTest("field_override.json") { overrideFieldInfo() }
}
}
describe("Constraints") {
it("Can set a minimum and maximum integer value") {
openApiTest("min_max_int_field.json") { constrainedIntInfo() }
}
it("Can set a minimum and maximum double value") {
openApiTest("min_max_double_field.json") { constrainedDoubleInfo() }
}
it("Can set an exclusive min and exclusive max integer value") {
openApiTest("exclusive_min_max.json") { exclusiveMinMax() }
}
it("Can add a custom format to a string field") {
openApiTest("formatted_param_type.json") { formattedParam() }
}
it("Can set a minimum and maximum length on a string field") {
openApiTest("min_max_string.json") { minMaxString() }
}
it("Can set a custom regex pattern on a string field") {
openApiTest("regex_string.json") { regexString() }
}
it("Can set a minimum and maximum item count on an array field") {
openApiTest("min_max_array.json") { minMaxArray() }
}
it("Can set a unique items constraint on an array field") {
openApiTest("unique_array.json") { uniqueArray() }
}
it("Can set a multiple-of constraint on an int field") {
openApiTest("multiple_of_int.json") { multipleOfInt() }
}
it("Can set a multiple of constraint on an double field") {
openApiTest("multiple_of_double.json") { multipleOfDouble() }
}
it("Can set a minimum and maximum number of properties on a free-form type") {
openApiTest("min_max_free_form.json") { minMaxFreeForm() }
}
}
describe("Free Form") {
it("Can create a free-form field") {
openApiTest("free_form_object.json") { freeFormObject() }
}
}
})

View File

@ -0,0 +1,173 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.Kontent.generateKontent
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.TestBigNumberModel
import io.bkbn.kompendium.core.fixtures.TestByteArrayModel
import io.bkbn.kompendium.core.fixtures.TestInvalidMap
import io.bkbn.kompendium.core.fixtures.TestNestedModel
import io.bkbn.kompendium.core.fixtures.TestSimpleModel
import io.bkbn.kompendium.core.fixtures.TestSimpleWithEnumList
import io.bkbn.kompendium.core.fixtures.TestSimpleWithEnums
import io.bkbn.kompendium.core.fixtures.TestSimpleWithList
import io.bkbn.kompendium.core.fixtures.TestSimpleWithMap
import io.bkbn.kompendium.core.fixtures.TestWithUUID
import io.bkbn.kompendium.oas.schema.DictionarySchema
import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.maps.beEmpty
import io.kotest.matchers.maps.shouldContainKey
import io.kotest.matchers.maps.shouldHaveKey
import io.kotest.matchers.maps.shouldHaveSize
import io.kotest.matchers.maps.shouldNotHaveKey
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import java.util.UUID
class KontentTest : DescribeSpec({
describe("Kontent analysis") {
it("Can return an empty map when passed Unit") {
// act
val result = generateKontent<Unit>()
// assert
result should beEmpty()
}
it("Can return a single map result when analyzing a primitive") {
// act
val result = generateKontent<Long>()
// assert
result shouldHaveSize 1
result["Long"] shouldBe FormattedSchema("int64", "integer")
}
it("Can handle BigDecimal and BigInteger Types") {
// act
val result = generateKontent<TestBigNumberModel>()
// assert
result shouldHaveSize 3
result shouldContainKey TestBigNumberModel::class.simpleName!!
result["BigDecimal"] shouldBe FormattedSchema("double", "number")
result["BigInteger"] shouldBe FormattedSchema("int64", "integer")
}
it("Can handle ByteArray type") {
// act
val result = generateKontent<TestByteArrayModel>()
// assert
result shouldHaveSize 2
result shouldContainKey TestByteArrayModel::class.simpleName!!
result["ByteArray"] shouldBe FormattedSchema("byte", "string")
}
it("Allows objects to reference their base type in the cache") {
// act
val result = generateKontent<TestSimpleModel>()
// assert
result shouldNotBe null
result shouldHaveSize 3
result shouldContainKey TestSimpleModel::class.simpleName!!
}
it("Can generate cache for nested object types") {
// act
val result = generateKontent<TestNestedModel>()
// assert
result shouldNotBe null
result shouldHaveSize 4
result shouldContainKey TestNestedModel::class.simpleName!!
result shouldContainKey TestSimpleModel::class.simpleName!!
}
it("Does not repeat generation for cached items") {
// arrange
val clazz = TestNestedModel::class
val initialCache = generateKontent<TestNestedModel>()
// act
val result = generateKontent<TestSimpleModel>(initialCache)
// assert TODO Spy to check invocation count?
result shouldNotBe null
result shouldHaveSize 4
result shouldContainKey clazz.simpleName!!
result shouldContainKey TestSimpleModel::class.simpleName!!
}
it("allows for generation of enum types") {
// act
val result = generateKontent<TestSimpleWithEnums>()
// assert
result shouldNotBe null
result shouldHaveSize 3
result shouldContainKey TestSimpleWithEnums::class.simpleName!!
}
it("Allows for generation of map fields") {
// act
val result = generateKontent<TestSimpleWithMap>()
// assert
result shouldNotBe null
result shouldHaveSize 5
result shouldContainKey "Map-String-TestSimpleModel"
result shouldContainKey TestSimpleWithMap::class.simpleName!!
result[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
}
it("Throws an error if a map is of an invalid type") {
// assert
shouldThrow<IllegalStateException> { generateKontent<TestInvalidMap>() }
}
it("Can generate for collection fields") {
// act
val result = generateKontent<TestSimpleWithList>()
// assert
result shouldNotBe null
result shouldHaveSize 6
result shouldContainKey "List-TestSimpleModel"
result shouldContainKey TestSimpleWithList::class.simpleName!!
}
it("Can parse an enum list as a field") {
// act
val result = generateKontent<TestSimpleWithEnumList>()
// assert
result shouldNotBe null
result shouldHaveSize 4
result shouldHaveKey "List-SimpleEnum"
}
it("Can support UUIDs") {
// act
val result = generateKontent<TestWithUUID>()
// assert
result shouldNotBe null
result shouldHaveSize 2
result shouldContainKey UUID::class.simpleName!!
result shouldContainKey TestWithUUID::class.simpleName!!
result[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
}
it("Can generate a top level list response") {
// act
val result = generateKontent<List<TestSimpleModel>>()
// assert
result shouldNotBe null
result shouldHaveSize 4
result shouldContainKey "List-TestSimpleModel"
}
it("Can handle a complex type") {
// act
val result = generateKontent<ComplexRequest>()
// assert
result shouldNotBe null
result shouldHaveSize 7
result shouldContainKey "Map-String-CrazyItem"
result["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
}
}
})

View File

@ -1,73 +1,39 @@
package io.bkbn.kompendium.util
package io.bkbn.kompendium.core.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.bkbn.kompendium.core.Notarized.notarizedDelete
import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.Notarized.notarizedPut
import io.bkbn.kompendium.core.fixtures.Bibbity
import io.bkbn.kompendium.core.fixtures.ComplexGibbit
import io.bkbn.kompendium.core.fixtures.DefaultParameter
import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.Mysterious
import io.bkbn.kompendium.core.fixtures.SimpleGibbit
import io.bkbn.kompendium.core.fixtures.TestFieldOverride
import io.bkbn.kompendium.core.fixtures.TestHelpers.DEFAULT_TEST_ENDPOINT
import io.bkbn.kompendium.core.fixtures.TestNested
import io.bkbn.kompendium.core.fixtures.TestRequest
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestResponseInfo
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.defaultField
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.defaultParam
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.formattedParam
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.minMaxString
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.nullableField
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.regexString
import io.bkbn.kompendium.core.fixtures.TestResponseInfo.requiredParam
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.serialization.json
fun Application.jacksonConfigModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
}
fun Application.kotlinxConfigModule() {
install(ContentNegotiation) {
json()
}
}
fun Application.statusPageModule() {
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo(
HttpStatusCode.BadRequest,
"Bad Things Happened"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
fun Application.statusPageMultiExceptions() {
install(StatusPages) {
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"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
fun Application.notarizedGetWithNotarizedException() {
routing {
@ -89,6 +55,26 @@ fun Application.notarizedGetWithMultipleThrowables() {
}
}
fun Application.notarizedGetWithPolymorphicErrorResponse() {
routing {
route(DEFAULT_TEST_ENDPOINT) {
notarizedGet(TestResponseInfo.testGetWithPolymorphicException) {
error("something terrible has happened!")
}
}
}
}
fun Application.notarizedGetWithGenericErrorResponse() {
routing {
route(DEFAULT_TEST_ENDPOINT) {
notarizedGet(TestResponseInfo.testGetWithGenericException) {
error("something terrible has happened!")
}
}
}
}
fun Application.notarizedGetModule() {
routing {
route("/test") {
@ -213,21 +199,11 @@ fun Application.primitives() {
}
}
fun Application.emptyGet() {
routing {
route("/test/empty") {
notarizedGet(TestResponseInfo.trulyEmptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
fun Application.withExamples() {
routing {
route("/test/examples") {
notarizedPost(
info = MethodInfo.PostInfo<Unit, TestRequest, TestResponse>(
info = PostInfo<Unit, TestRequest, TestResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
requestInfo = RequestInfo(
@ -254,9 +230,13 @@ fun Application.withDefaultParameter() {
routing {
route("/test") {
notarizedGet(
info = MethodInfo.GetInfo<DefaultParameter, TestResponse>(
info = GetInfo<DefaultParameter, TestResponse>(
summary = "Testing Default Params",
description = "Should have a default parameter value"
description = "Should have a default parameter value",
responseInfo = ResponseInfo(
HttpStatusCode.OK,
"A good response"
)
)
) {
call.respond(TestResponse("hey"))
@ -280,7 +260,7 @@ fun Application.withOperationId(){
fun Application.nonRequiredParamsGet() {
routing {
route("/test/optional") {
notarizedGet(TestResponseInfo.emptyTestGetInfo) {
notarizedGet(TestResponseInfo.testOptionalParams) {
call.respond(HttpStatusCode.OK)
}
}
@ -381,3 +361,173 @@ fun Application.simpleGenericResponse() {
}
}
}
fun Application.overrideFieldInfo() {
routing {
route("/test/field_override") {
notarizedGet(TestResponseInfo.fieldOverride) {
call.respond(HttpStatusCode.OK, TestFieldOverride(true))
}
}
}
}
fun Application.constrainedIntInfo() {
routing {
route("/test/constrained_int") {
notarizedGet(TestResponseInfo.minMaxInt) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.constrainedDoubleInfo() {
routing {
route("/test/constrained_int") {
notarizedGet(TestResponseInfo.minMaxDouble) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.exclusiveMinMax() {
routing {
route("/test/constrained_int") {
notarizedGet(TestResponseInfo.exclusiveMinMax) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.requiredParameter() {
routing {
route("/test/required_param") {
notarizedGet(requiredParam) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.defaultParameter() {
routing {
route("/test/required_param") {
notarizedGet(defaultParam) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.defaultField() {
routing {
route("/test/required_param") {
notarizedPost(defaultField) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.nullableField() {
routing {
route("/test/required_param") {
notarizedPost(nullableField) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.formattedParam() {
routing {
route("/test/required_param") {
notarizedGet(formattedParam) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.minMaxString() {
routing {
route("/test/required_param") {
notarizedGet(minMaxString) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.regexString() {
routing {
route("/test/required_param") {
notarizedGet(regexString) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.minMaxArray() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.minMaxArray) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.uniqueArray() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.uniqueArray) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.multipleOfInt() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.multipleOfInt) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.multipleOfDouble() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.multipleOfDouble) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.freeFormObject() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.freeFormObject) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}
fun Application.minMaxFreeForm() {
routing {
route("/test/required_param") {
notarizedGet(TestResponseInfo.minMaxFreeForm) {
call.respond(HttpStatusCode.OK, TestResponse("hi"))
}
}
}
}

View File

@ -1,11 +0,0 @@
package io.bkbn.kompendium.util
import java.io.File
object TestHelpers {
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
}

View File

@ -1,111 +0,0 @@
package io.bkbn.kompendium.util
import java.util.UUID
import io.bkbn.kompendium.annotations.KompendiumField
import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.annotations.UndeclaredField
import java.math.BigDecimal
import java.math.BigInteger
data class TestSimpleModel(val a: String, val b: Int)
data class TestBigNumberModel(val a: BigDecimal, val b: BigInteger)
data class TestByteArrayModel(val a: ByteArray)
data class TestNestedModel(val inner: TestSimpleModel)
data class TestSimpleWithEnums(val a: String, val b: SimpleEnum)
data class TestSimpleWithMap(val a: String, val b: Map<String, TestSimpleModel>)
data class TestSimpleWithList(val a: Boolean, val b: List<TestSimpleModel>)
data class TestSimpleWithEnumList(val a: Double, val b: List<SimpleEnum>)
data class TestInvalidMap(val a: Map<Int, TestSimpleModel>)
data class TestParams(
@KompendiumParam(ParamType.PATH) val a: String,
@KompendiumParam(ParamType.QUERY) val aa: Int
)
data class TestNested(val nesty: String)
data class TestWithUUID(val id: UUID)
data class TestRequest(
@KompendiumField(name = "field_name")
val fieldName: TestNested,
val b: Double,
val aaa: List<Long>
)
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(
val org: String,
@KompendiumField("amazing_field")
val amazingField: String,
val tables: List<NestedComplexItem>
)
data class NestedComplexItem(
val name: String,
val alias: CustomAlias
)
typealias CustomAlias = Map<String, CrazyItem>
data class CrazyItem(val enumeration: SimpleEnum)
enum class SimpleEnum {
ONE,
TWO
}
data class DefaultParameter(
@KompendiumParam(ParamType.QUERY) val a: Int = 100,
@KompendiumParam(ParamType.PATH) val b: String?,
@KompendiumParam(ParamType.PATH) val c: Boolean
)
data class ExceptionResponse(val message: String)
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 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>
data class Gibbity<T>(val a: 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)
data class HeaderNameTest(
@KompendiumParam(type = ParamType.HEADER) val `X-UserEmail`: String
)

View File

@ -1,123 +0,0 @@
package io.bkbn.kompendium.util
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")
private val testGetListResponse =
ResponseInfo<List<TestResponse>>(HttpStatusCode.OK, "A Successful List-y Endeavor")
private val testPostResponse = ResponseInfo<TestCreatedResponse>(HttpStatusCode.Created, "A Successful Endeavor")
private val testPostResponseAgain = ResponseInfo<Boolean>(HttpStatusCode.Created, "A Successful Endeavor")
private val testDeleteResponse =
ResponseInfo<Unit>(HttpStatusCode.NoContent, "A Successful Endeavor", mediaTypes = emptyList())
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 = GetInfo<TestParams, TestResponse>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetResponse
)
val testGetInfoAgain = GetInfo<TestParams, List<TestResponse>>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetListResponse
)
val testGetWithException = testGetInfo.copy(
canThrow = setOf(Exception::class)
)
val testGetWithMultipleExceptions = testGetInfo.copy(
canThrow = setOf(AccessDeniedException::class, Exception::class)
)
val testPostInfo = PostInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test post endpoint",
description = "Post your tests here!",
responseInfo = testPostResponse,
requestInfo = testRequest
)
val testPutInfo = PutInfo<Unit, ComplexRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponse,
requestInfo = complexRequest
)
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponse,
requestInfo = testRequest
)
val testPutInfoAgain = PutInfo<Unit, Int, Boolean>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponseAgain,
requestInfo = testRequestAgain
)
val testDeleteInfo = DeleteInfo<TestParams, Unit>(
summary = "Test delete endpoint",
description = "testing my deletes",
responseInfo = testDeleteResponse
)
val emptyTestGetInfo =
GetInfo<OptionalParams, 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 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>>(
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 undeclaredResponseType = GetInfo<Unit, Mysterious>(
summary = "spooky class",
description = "break this glass in scenario of emergency",
responseInfo = simpleOkResponse()
)
val headerParam = GetInfo<HeaderNameTest, TestResponse>(
summary = "testing header stuffs",
description = "Good for many things",
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

@ -1,133 +1,126 @@
{
"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"
"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"
"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" : {
"put" : {
"tags" : [ ],
"summary" : "Test put endpoint",
"description" : "Put your tests here!",
"parameters" : [ ],
"requestBody" : {
"description" : "A Complex request",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ComplexRequest"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"put": {
"tags": [],
"summary": "Test put endpoint",
"description": "Put your tests here!",
"parameters": [],
"requestBody": {
"description": "A Complex request",
"content": {
"application/json": {
"schema": {
"properties": {
"amazing_field": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"properties": {
"alias": {
"additionalProperties": {
"properties": {
"enumeration": {
"enum": [
"ONE",
"TWO"
],
"type": "string"
}
},
"required": [
"enumeration"
],
"type": "object"
},
"type": "object"
},
"name": {
"type": "string"
}
},
"required": [
"name",
"alias"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"org",
"amazingField",
"tables"
],
"type": "object"
}
}
},
"required" : false
"required": true
},
"responses" : {
"201" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestCreatedResponse"
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
}
},
"required": [
"id",
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleEnum" : {
"type" : "string",
"enum" : [ "ONE", "TWO" ]
},
"CrazyItem" : {
"type" : "object",
"properties" : {
"enumeration" : {
"$ref" : "#/components/schemas/SimpleEnum"
}
}
},
"Map-String-CrazyItem" : {
"type" : "object",
"additionalProperties" : {
"$ref" : "#/components/schemas/CrazyItem"
}
},
"NestedComplexItem" : {
"type" : "object",
"properties" : {
"alias" : {
"$ref" : "#/components/schemas/Map-String-CrazyItem"
},
"name" : {
"$ref" : "#/components/schemas/String"
}
}
},
"List-NestedComplexItem" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/NestedComplexItem"
}
},
"ComplexRequest" : {
"type" : "object",
"properties" : {
"amazingField" : {
"$ref" : "#/components/schemas/String"
},
"org" : {
"$ref" : "#/components/schemas/String"
},
"tables" : {
"$ref" : "#/components/schemas/List-NestedComplexItem"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"TestCreatedResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,164 +1,203 @@
{
"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"
"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"
"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"
} ]
"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": [
{
"properties": {
"a": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"f": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"required": [
"b",
"f"
],
"type": "object"
}
]
}
}
}
}
},
"deprecated" : false
"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"
} ]
"/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": [
{
"properties": {
"a": {
"anyOf": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"c": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b",
"c"
],
"type": "object"
}
]
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"f": {
"anyOf": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"c": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b",
"c"
],
"type": "object"
}
]
}
},
"required": [
"b",
"f"
],
"type": "object"
}
]
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestNested" : {
"type" : "object",
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Gibbity-TestNested" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/TestNested"
}
}
},
"Bibbity-TestNested" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"f" : {
"$ref" : "#/components/schemas/TestNested"
}
}
},
"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"
}
}
},
"Gibbity-FlibbityGibbit" : {
"type" : "object",
"properties" : {
"a" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/SimpleGibbit"
}, {
"$ref" : "#/components/schemas/ComplexGibbit"
} ]
}
}
},
"Bibbity-FlibbityGibbit" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"f" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/SimpleGibbit"
}, {
"$ref" : "#/components/schemas/ComplexGibbit"
} ]
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -0,0 +1,87 @@
{
"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/required_param": {
"post": {
"tags": [],
"summary": "default param",
"description": "Cool stuff",
"parameters": [],
"requestBody": {
"description": "cool",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"type": "string"
},
"b": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b"
],
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,75 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "default param",
"description": "Cool stuff",
"parameters": [
{
"name": "b",
"in": "query",
"schema": {
"type": "string",
"default": "heyo"
},
"required": false,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,77 +1,119 @@
{
"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"
"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"
"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/examples" : {
"post" : {
"tags" : [ ],
"summary" : "Example Parameters",
"description" : "A test for setting parameter examples",
"parameters" : [ ],
"requestBody" : {
"description" : "Test",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestRequest"
},
"examples" : {
"one" : {
"value" : {
"fieldName" : {
"nesty" : "hey"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/examples": {
"post": {
"tags": [],
"summary": "Example Parameters",
"description": "A test for setting parameter examples",
"parameters": [],
"requestBody": {
"description": "Test",
"content": {
"application/json": {
"schema": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"b" : 4.0,
"aaa" : [ ]
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"two" : {
"value" : {
"fieldName" : {
"nesty" : "hello"
"required": [
"fieldName",
"b",
"aaa"
],
"type": "object"
},
"examples": {
"one": {
"value": {
"fieldName": {
"nesty": "hey"
},
"b" : 3.8,
"aaa" : [ 31324234 ]
"b": 4.0,
"aaa": []
}
},
"two": {
"value": {
"fieldName": {
"nesty": "hello"
},
"b": 3.8,
"aaa": [
31324234
]
}
}
}
}
},
"required" : false
"required": true
},
"responses" : {
"201" : {
"description" : "nice",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
"responses": {
"201": {
"description": "nice",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
},
"examples" : {
"test" : {
"value" : {
"c" : "spud"
"examples": {
"test": {
"value": {
"c": "spud"
}
}
}
@ -79,62 +121,13 @@
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"Long" : {
"type" : "integer",
"format" : "int64"
},
"List-Long" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Long"
}
},
"Double" : {
"type" : "number",
"format" : "double"
},
"String" : {
"type" : "string"
},
"TestNested" : {
"type" : "object",
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
}
},
"TestRequest" : {
"type" : "object",
"properties" : {
"aaa" : {
"$ref" : "#/components/schemas/List-Long"
},
"b" : {
"$ref" : "#/components/schemas/Double"
},
"fieldName" : {
"$ref" : "#/components/schemas/TestNested"
}
}
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -0,0 +1,69 @@
{
"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/constrained_int": {
"get": {
"tags": [],
"summary": "Constrained int field",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"format": "int32",
"type": "integer",
"minimum": 5,
"maximum": 100,
"exclusiveMinimum": true,
"exclusiveMaximum": true
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,65 @@
{
"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/field_override": {
"get": {
"tags": [],
"summary": "A Response with a spicy field",
"description": "Important info within!",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"real_name": {
"type": "boolean",
"description": "A Field that is super important!"
}
},
"required": [
"b"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,75 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [
{
"name": "a",
"in": "query",
"schema": {
"type": "string",
"format": "password"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,65 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"data": {
"additionalProperties": true,
"type": "object"
}
},
"required": [
"data"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,121 @@
{
"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": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
},
"400": {
"description": "Wow serious things went wrong",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"f": {
"type": "string"
}
},
"required": [
"b",
"f"
],
"type": "object"
}
]
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,73 +1,69 @@
{
"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"
"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"
"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"
"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": {
"properties": {
"messy": {
"type": "string"
},
"potato": {
"format": "int32",
"type": "integer"
}
},
"required": [
"messy",
"potato"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"TestGeneric-Int" : {
"type" : "object",
"properties" : {
"messy" : {
"$ref" : "#/components/schemas/String"
},
"potato" : {
"$ref" : "#/components/schemas/Int"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -0,0 +1,69 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 10,
"type": "array"
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,69 @@
{
"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/constrained_int": {
"get": {
"tags": [],
"summary": "Constrained int field",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"format": "double",
"type": "number",
"minimum": 5.5,
"maximum": 13.37,
"exclusiveMinimum": false,
"exclusiveMaximum": false
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,67 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"data": {
"minProperties": 5,
"maxProperties": 10,
"additionalProperties": true,
"type": "object"
}
},
"required": [
"data"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,69 @@
{
"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/constrained_int": {
"get": {
"tags": [],
"summary": "Constrained int field",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"format": "int32",
"type": "integer",
"minimum": 5,
"maximum": 100,
"exclusiveMinimum": false,
"exclusiveMaximum": false
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,66 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"type": "string",
"minLength": 42,
"maxLength": 1337
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,66 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"format": "double",
"type": "number",
"multipleOf": 2.5
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,66 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"format": "int32",
"type": "integer",
"multipleOf": 5
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,87 +1,84 @@
{
"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"
"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"
"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" : {
"/testerino" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/testerino": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,42 +0,0 @@
{
"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/empty" : {
"get" : {
"tags" : [ ],
"summary" : "No request params and response body",
"description" : "testing more",
"parameters" : [ ],
"deprecated" : false
}
}
},
"components" : {
"schemas" : { },
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -1,62 +1,69 @@
{
"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"
"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"
"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/optional" : {
"get" : {
"tags" : [ ],
"summary" : "No request params and response body",
"description" : "testing more",
"parameters" : [ {
"name" : "notRequired",
"in" : "query",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/optional": {
"get": {
"tags": [],
"summary": "No request params and response body",
"description": "testing more",
"parameters": [
{
"name": "notRequired",
"in": "query",
"schema": {
"type": "string",
"nullable": true
},
"required": false,
"deprecated": false
},
"required" : false,
"deprecated" : false
}, {
"name" : "required",
"in" : "query",
"schema" : {
"type" : "string"
},
"required" : true,
"deprecated" : false
} ],
"deprecated" : false
{
"name": "required",
"in": "query",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"204": {
"description": "Empty"
}
},
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -40,8 +40,8 @@
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
"format" : "int32",
"type" : "integer"
},
"required" : true,
"deprecated" : false
@ -56,15 +56,6 @@
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
},
"security" : [ ],

View File

@ -1,87 +1,84 @@
{
"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"
"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"
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,105 +1,102 @@
{
"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"
"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"
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
},
"400" : {
"description" : "Bad Things Happened",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ExceptionResponse"
"400": {
"description": "Bad Things Happened",
"content": {
"application/json": {
"schema": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"ExceptionResponse" : {
"type" : "object",
"properties" : {
"message" : {
"$ref" : "#/components/schemas/String"
}
}
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,108 +1,120 @@
{
"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"
"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"
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
},
"403" : {
"description" : "New API who dis?"
"403": {
"description": "Access Denied",
"content": {
"application/json": {
"schema": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
},
"400" : {
"description" : "Bad Things Happened",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ExceptionResponse"
"400": {
"description": "Bad Things Happened",
"content": {
"application/json": {
"schema": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"ExceptionResponse" : {
"type" : "object",
"properties" : {
"message" : {
"$ref" : "#/components/schemas/String"
}
}
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,88 +1,85 @@
{
"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"
"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"
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"operationId" : "getTest",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"operationId": "getTest",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,137 +1,129 @@
{
"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"
"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"
"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" : {
"post" : {
"tags" : [ ],
"summary" : "Test post endpoint",
"description" : "Post your tests here!",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"post": {
"tags": [],
"summary": "Test post endpoint",
"description": "Post your tests here!",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"requestBody" : {
"description" : "A Test request",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestRequest"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "A Test request",
"content": {
"application/json": {
"schema": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"required": [
"fieldName",
"b",
"aaa"
],
"type": "object"
}
}
},
"required" : false
"required": true
},
"responses" : {
"201" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestCreatedResponse"
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
}
},
"required": [
"id",
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"Long" : {
"type" : "integer",
"format" : "int64"
},
"List-Long" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Long"
}
},
"Double" : {
"type" : "number",
"format" : "double"
},
"String" : {
"type" : "string"
},
"TestNested" : {
"type" : "object",
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
}
},
"TestRequest" : {
"type" : "object",
"properties" : {
"aaa" : {
"$ref" : "#/components/schemas/List-Long"
},
"b" : {
"$ref" : "#/components/schemas/Double"
},
"fieldName" : {
"$ref" : "#/components/schemas/TestNested"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"TestCreatedResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,73 +1,68 @@
{
"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"
"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"
"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" : {
"put" : {
"tags" : [ ],
"summary" : "Test put endpoint",
"description" : "Put your tests here!",
"parameters" : [ ],
"requestBody" : {
"description" : "A Test request",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Int"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"put": {
"tags": [],
"summary": "Test put endpoint",
"description": "Put your tests here!",
"parameters": [],
"requestBody": {
"description": "A Test request",
"content": {
"application/json": {
"schema": {
"format": "int32",
"type": "integer"
}
}
},
"required" : false
"required": true
},
"responses" : {
"201" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Boolean"
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"Int" : {
"type" : "integer",
"format" : "int32"
},
"Boolean" : {
"type" : "boolean"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,137 +1,129 @@
{
"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"
"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"
"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" : {
"put" : {
"tags" : [ ],
"summary" : "Test put endpoint",
"description" : "Put your tests here!",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test": {
"put": {
"tags": [],
"summary": "Test put endpoint",
"description": "Put your tests here!",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"requestBody" : {
"description" : "A Test request",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestRequest"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"requestBody": {
"description": "A Test request",
"content": {
"application/json": {
"schema": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"required": [
"fieldName",
"b",
"aaa"
],
"type": "object"
}
}
},
"required" : false
"required": true
},
"responses" : {
"201" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestCreatedResponse"
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
}
},
"required": [
"id",
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"Long" : {
"type" : "integer",
"format" : "int64"
},
"List-Long" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Long"
}
},
"Double" : {
"type" : "number",
"format" : "double"
},
"String" : {
"type" : "string"
},
"TestNested" : {
"type" : "object",
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
}
},
"TestRequest" : {
"type" : "object",
"properties" : {
"aaa" : {
"$ref" : "#/components/schemas/List-Long"
},
"b" : {
"$ref" : "#/components/schemas/Double"
},
"fieldName" : {
"$ref" : "#/components/schemas/TestNested"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"TestCreatedResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -0,0 +1,84 @@
{
"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/required_param": {
"post": {
"tags": [],
"summary": "default param",
"description": "Cool stuff",
"parameters": [],
"requestBody": {
"description": "cool",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"type": "string",
"nullable": true
}
},
"required": [
"a"
],
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,74 +1,74 @@
{
"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"
"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"
"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/with_header" : {
"get" : {
"tags" : [ ],
"summary" : "testing header stuffs",
"description" : "Good for many things",
"parameters" : [ {
"name" : "X-UserEmail",
"in" : "header",
"schema" : {
"type" : "string"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/with_header": {
"get": {
"tags": [],
"summary": "testing header stuffs",
"description": "Good for many things",
"parameters": [
{
"name": "X-UserEmail",
"in": "header",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,87 +1,84 @@
{
"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"
"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"
"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" : {
"/this/is/a/complex/path/with/an/{id}" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/this/is/a/complex/path/with/an/{id}": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -0,0 +1,122 @@
{
"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": {
"get": {
"tags": [],
"summary": "Another get test",
"description": "testing more",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
},
"501": {
"description": "The Gibbits are ANGRY",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"c": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b",
"c"
],
"type": "object"
}
]
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,91 +1,87 @@
{
"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"
"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"
"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"
"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": {
"items": {
"anyOf": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"c": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b",
"c"
],
"type": "object"
}
]
},
"type": "array"
}
}
}
}
},
"deprecated" : false
"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" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,91 +1,87 @@
{
"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"
"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"
"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"
"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": {
"additionalProperties": {
"anyOf": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"c": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b",
"c"
],
"type": "object"
}
]
},
"type": "object"
}
}
}
}
},
"deprecated" : false
"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" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,85 +1,84 @@
{
"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"
"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"
"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"
} ]
"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": [
{
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"c": {
"format": "int32",
"type": "integer"
}
},
"required": [
"b",
"c"
],
"type": "object"
}
]
}
}
}
}
},
"deprecated" : false
"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"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,89 +1,99 @@
{
"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"
"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"
"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"
} ]
"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": [
{
"properties": {
"a": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"required": [
"a"
],
"type": "object"
},
{
"properties": {
"b": {
"type": "string"
},
"f": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
}
},
"required": [
"b",
"f"
],
"type": "object"
}
]
}
}
}
}
},
"deprecated" : false
"deprecated": false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestNested" : {
"type" : "object",
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Gibbity-TestNested" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/TestNested"
}
}
},
"Bibbity-TestNested" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"f" : {
"$ref" : "#/components/schemas/TestNested"
}
}
}
},
"securitySchemes" : { }
"components": {
"securitySchemes": {}
},
"security" : [ ],
"tags" : [ ]
"security": [],
"tags": []
}

View File

@ -1,87 +1,95 @@
{
"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"
"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"
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Testing Default Params",
"description" : "Should have a default parameter value",
"parameters" : [ {
"name" : "a",
"in" : "query",
"schema" : {
"default" : 100,
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
}, {
"name" : "b",
"in" : "path",
"schema" : {
"type" : "string"
},
"required" : false,
"deprecated" : false
}, {
"name" : "c",
"in" : "path",
"schema" : {
"type" : "boolean"
},
"required" : true,
"deprecated" : false
} ],
"deprecated" : false
}
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
],
"paths": {
"/test": {
"get": {
"tags": [],
"summary": "Testing Default Params",
"description": "Should have a default parameter value",
"parameters": [
{
"name": "a",
"in": "query",
"schema": {
"format": "int32",
"type": "integer",
"default": 100
},
"required": false,
"deprecated": false
},
{
"name": "b",
"in": "path",
"schema": {
"type": "string",
"nullable": true
},
"required": false,
"deprecated": false
},
{
"name": "c",
"in": "path",
"schema": {
"type": "boolean"
},
"required": true,
"deprecated": false
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"Boolean" : {
"type" : "boolean"
],
"responses": {
"200": {
"description": "A good response",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
},
"securitySchemes" : { }
}
},
"security" : [ ],
"tags" : [ ]
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Test API</title>
<title>Docs</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">

View File

@ -0,0 +1,65 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"a": {
"type": "string",
"pattern": "^\\d{3}-\\d{2}-\\d{4}$"
}
},
"required": [
"a"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,74 @@
{
"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/required_param": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"parameters": [
{
"name": "a",
"in": "query",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

Some files were not shown because too many files have changed in this diff Show More