breaking change: V3 alpha (#256)

This commit is contained in:
Ryan Brink
2022-08-13 09:59:59 -07:00
committed by GitHub
parent 48969a8fcc
commit c73c9b4605
308 changed files with 5410 additions and 10204 deletions

View File

@ -1,8 +1,8 @@
plugins {
kotlin("jvm") version "1.6.21" apply false
kotlin("plugin.serialization") version "1.6.21" apply false
id("io.bkbn.sourdough.library.jvm") version "0.6.0" apply false
id("io.bkbn.sourdough.application.jvm") version "0.6.0" apply false
kotlin("jvm") version "1.7.10" apply false
kotlin("plugin.serialization") version "1.7.10" apply false
id("io.bkbn.sourdough.library.jvm") version "0.9.0" apply false
id("io.bkbn.sourdough.application.jvm") version "0.9.0" apply false
id("io.bkbn.sourdough.root") version "0.9.0"
id("com.github.jakemarsden.git-hooks") version "0.0.2"
id("org.jetbrains.dokka") version "1.7.10"

62
core/build.gradle.kts Normal file
View File

@ -0,0 +1,62 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
id("java-test-fixtures")
}
sourdoughLibrary {
libraryName.set("Kompendium Core")
libraryDescription.set("Core functionality for the Kompendium library")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
// VERSIONS
val kotestVersion: String by project
val ktorVersion: String by project
// IMPLEMENTATION
api(projects.kompendiumOas)
api(projects.kompendiumJsonSchema)
implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("ch.qos.logback:logback-classic:1.2.11")
// TEST FIXTURES
testFixturesApi("io.kotest:kotest-runner-junit5-jvm:$kotestVersion")
testFixturesApi("io.kotest:kotest-assertions-core-jvm:$kotestVersion")
testFixturesApi("io.kotest:kotest-property-jvm:$kotestVersion")
testFixturesApi("io.kotest:kotest-assertions-json-jvm:$kotestVersion")
testFixturesApi("io.kotest:kotest-assertions-ktor-jvm:4.4.3")
testFixturesApi("io.ktor:ktor-server-core:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-jackson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-gson:$ktorVersion")
testFixturesApi("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
testFixturesApi("io.ktor:ktor-server-content-negotiation:$ktorVersion")
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -0,0 +1,8 @@
package io.bkbn.kompendium.core.attribute
import io.bkbn.kompendium.oas.OpenApiSpec
import io.ktor.util.AttributeKey
object KompendiumAttributes {
val openApiSpec = AttributeKey<OpenApiSpec>("OpenApiSpec")
}

View File

@ -0,0 +1,40 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class DeleteInfo private constructor(
override val response: ResponseInfo,
override val errors: MutableList<ResponseInfo>,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfo {
companion object {
fun builder(init: Builder.() -> Unit): DeleteInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfo.Builder<DeleteInfo>() {
override fun build() = DeleteInfo(
response = response!!,
errors = errors,
tags = tags,
summary = summary!!,
description = description!!,
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,40 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class GetInfo private constructor(
override val response: ResponseInfo,
override val errors: MutableList<ResponseInfo>,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfo {
companion object {
fun builder(init: Builder.() -> Unit): GetInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfo.Builder<GetInfo>() {
override fun build() = GetInfo(
response = response ?: error("You must provide a response in order to notarize a GET"),
errors = errors,
tags = tags,
summary = summary ?: error("You must provide a summary in order to notarize a GET"),
description = description ?: error("You must provide a description in order to notarize a GET"),
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,40 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class HeadInfo private constructor(
override val response: ResponseInfo,
override val errors: MutableList<ResponseInfo>,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfo {
companion object {
fun builder(init: Builder.() -> Unit): HeadInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfo.Builder<HeadInfo>() {
override fun build() = HeadInfo(
response = response!!,
errors = errors,
tags = tags,
summary = summary!!,
description = description!!,
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,64 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
sealed interface MethodInfo {
val response: ResponseInfo
val errors: List<ResponseInfo>
val tags: Set<String>
val summary: String
val description: String
val externalDocumentation: ExternalDocumentation?
get() = null
val operationId: String?
get() = null
val deprecated: Boolean
get() = false
val parameters: List<Parameter>
get() = emptyList()
abstract class Builder<T : MethodInfo> {
internal var response: ResponseInfo? = null
internal var summary: String? = null
internal var description: String? = null
internal var externalDocumentation: ExternalDocumentation? = null
internal var operationId: String? = null
internal var deprecated: Boolean = false
internal var tags: Set<String> = emptySet()
internal var parameters: List<Parameter> = emptyList()
internal var errors: MutableList<ResponseInfo> = mutableListOf()
fun response(init: ResponseInfo.Builder.() -> Unit) = apply {
val builder = ResponseInfo.Builder()
builder.init()
this.response = builder.build()
}
fun canRespond(init: ResponseInfo.Builder.() -> Unit) = apply {
val builder = ResponseInfo.Builder()
builder.init()
errors.add(builder.build())
}
fun canRespond(responses: List<ResponseInfo>) = apply {
errors.addAll(responses)
}
fun summary(s: String) = apply { this.summary = s }
fun description(s: String) = apply { this.description = s }
fun externalDocumentation(docs: ExternalDocumentation) = apply { this.externalDocumentation = docs }
fun operationId(id: String) = apply { this.operationId = id }
fun isDeprecated() = apply { this.deprecated = true }
fun tags(vararg tags: String) = apply { this.tags = tags.toSet() }
fun parameters(vararg parameters: Parameter) = apply { this.parameters = parameters.toList() }
abstract fun build(): T
}
}

View File

@ -0,0 +1,15 @@
package io.bkbn.kompendium.core.metadata
sealed interface MethodInfoWithRequest : MethodInfo {
val request: RequestInfo
abstract class Builder<T: MethodInfoWithRequest> : MethodInfo.Builder<T>() {
internal var request: RequestInfo? = null
fun request(init: RequestInfo.Builder.() -> Unit) = apply {
val builder = RequestInfo.Builder()
builder.init()
this.request = builder.build()
}
}
}

View File

@ -0,0 +1,40 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class OptionsInfo private constructor(
override val response: ResponseInfo,
override val errors: MutableList<ResponseInfo>,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfo {
companion object {
fun builder(init: Builder.() -> Unit): OptionsInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfo.Builder<OptionsInfo>() {
override fun build() = OptionsInfo(
response = response!!,
errors = errors,
tags = tags,
summary = summary!!,
description = description!!,
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,42 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class PatchInfo private constructor(
override val request: RequestInfo,
override val errors: MutableList<ResponseInfo>,
override val response: ResponseInfo,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfoWithRequest {
companion object {
fun builder(init: Builder.() -> Unit): PatchInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfoWithRequest.Builder<PatchInfo>() {
override fun build() = PatchInfo(
request = request!!,
errors = errors,
response = response!!,
tags = tags,
summary = summary!!,
description = description!!,
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,42 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class PostInfo private constructor(
override val request: RequestInfo,
override val errors: MutableList<ResponseInfo>,
override val response: ResponseInfo,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfoWithRequest {
companion object {
fun builder(init: Builder.() -> Unit): PostInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfoWithRequest.Builder<PostInfo>() {
override fun build() = PostInfo(
request = request!!,
errors = errors,
response = response!!,
tags = tags,
summary = summary!!,
description = description!!,
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,42 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.common.ExternalDocumentation
import io.bkbn.kompendium.oas.payload.Parameter
class PutInfo private constructor(
override val request: RequestInfo,
override val errors: MutableList<ResponseInfo>,
override val response: ResponseInfo,
override val tags: Set<String>,
override val summary: String,
override val description: String,
override val externalDocumentation: ExternalDocumentation?,
override val operationId: String?,
override val deprecated: Boolean,
override val parameters: List<Parameter>
): MethodInfoWithRequest {
companion object {
fun builder(init: Builder.() -> Unit): PutInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder : MethodInfoWithRequest.Builder<PutInfo>() {
override fun build() = PutInfo(
request = request!!,
errors = errors,
response = response!!,
tags = tags,
summary = summary!!,
description = description!!,
externalDocumentation = externalDocumentation,
operationId = operationId,
deprecated = deprecated,
parameters = parameters
)
}
}

View File

@ -0,0 +1,46 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.payload.MediaType
import kotlin.reflect.KType
import kotlin.reflect.typeOf
class RequestInfo private constructor(
val requestType: KType,
val description: String,
val examples: Map<String, MediaType.Example>?
) {
companion object {
fun builder(init: Builder.() -> Unit): RequestInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder {
private var requestType: KType? = null
private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null
fun requestType(t: KType) = apply {
this.requestType = t
}
inline fun <reified T> requestType() = apply { requestType(typeOf<T>()) }
fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
println(this.examples)
}
fun build() = RequestInfo(
requestType = requestType!!,
description = description!!,
examples = examples
)
}
}

View File

@ -0,0 +1,53 @@
package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.oas.payload.MediaType
import io.ktor.http.HttpStatusCode
import kotlin.reflect.KType
import kotlin.reflect.typeOf
class ResponseInfo private constructor(
val responseCode: HttpStatusCode,
val responseType: KType,
val description: String,
val examples: Map<String, MediaType.Example>?
) {
companion object {
fun builder(init: Builder.() -> Unit): ResponseInfo {
val builder = Builder()
builder.init()
return builder.build()
}
}
class Builder {
private var responseCode: HttpStatusCode? = null
private var responseType: KType? = null
private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null
fun responseCode(code: HttpStatusCode) = apply {
this.responseCode = code
}
fun responseType(t: KType) = apply {
this.responseType = t
}
inline fun <reified T> responseType() = apply { responseType(typeOf<T>()) }
fun description(s: String) = apply { this.description = s }
fun examples(vararg e: Pair<String, Any>) = apply {
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
println(this.examples)
}
fun build() = ResponseInfo(
responseCode = responseCode ?: error("You must provide a response code in order to build a Response!"),
responseType = responseType ?: error("You must provide a response type in order to build a Response!"),
description = description ?: error("You must provide a description in order to build a Response!"),
examples = examples
)
}
}

View File

@ -0,0 +1,45 @@
package io.bkbn.kompendium.core.plugin
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.oas.OpenApiSpec
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.response.respond
import io.ktor.server.routing.Routing
import io.ktor.server.routing.application
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlin.reflect.KClass
import kotlin.reflect.KType
object NotarizedApplication {
class Config {
lateinit var spec: OpenApiSpec
var openApiJson: Routing.() -> Unit = {
route("/openapi.json") {
get {
call.respond(HttpStatusCode.OK, this@route.application.attributes[KompendiumAttributes.openApiSpec])
}
}
}
var customTypes: Map<KType, JsonSchema> = emptyMap()
}
operator fun invoke() = createApplicationPlugin(
name = "NotarizedApplication",
createConfiguration = ::Config
) {
val spec = pluginConfig.spec
val routing = application.routing { }
pluginConfig.openApiJson(routing)
pluginConfig.customTypes.forEach { (type, schema) ->
spec.components.schemas[type.getSimpleSlug()] = schema
}
application.attributes.put(KompendiumAttributes.openApiSpec, spec)
}
}

View File

@ -0,0 +1,171 @@
package io.bkbn.kompendium.core.plugin
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.MethodInfo
import io.bkbn.kompendium.core.metadata.MethodInfoWithRequest
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.path.Path
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.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.Hook
import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.routing.Route
import kotlin.reflect.KClass
import kotlin.reflect.KType
object NotarizedRoute {
class Config {
var tags: Set<String> = emptySet()
var parameters: List<Parameter> = emptyList()
var get: GetInfo? = null
var post: PostInfo? = null
var put: PutInfo? = null
var delete: DeleteInfo? = null
var patch: PatchInfo? = null
var head: HeadInfo? = null
var options: OptionsInfo? = null
var security: Map<String, List<String>>? = null
internal var path: Path? = null
}
private object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> {
override fun install(pipeline: ApplicationCallPipeline, handler: (ApplicationCallPipeline) -> Unit) {
handler(pipeline)
}
}
operator fun invoke() = createRouteScopedPlugin(
name = "NotarizedRoute",
createConfiguration = ::Config
) {
// This is required in order to introspect the route path
on(InstallHook) {
val route = it as? Route ?: return@on
val spec = application.attributes[KompendiumAttributes.openApiSpec]
val routePath = route.calculateRoutePath()
require(spec.paths[routePath] == null) {
"""
The specified path ${Parameter.Location.path} has already been documented!
Please make sure that all notarized paths are unique
""".trimIndent()
}
spec.paths[routePath] = pluginConfig.path!!
}
val spec = application.attributes[KompendiumAttributes.openApiSpec]
val path = Path()
path.parameters = pluginConfig.parameters
pluginConfig.get?.addToSpec(path, spec, pluginConfig)
pluginConfig.delete?.addToSpec(path, spec, pluginConfig)
pluginConfig.head?.addToSpec(path, spec, pluginConfig)
pluginConfig.options?.addToSpec(path, spec, pluginConfig)
pluginConfig.post?.addToSpec(path, spec, pluginConfig)
pluginConfig.put?.addToSpec(path, spec, pluginConfig)
pluginConfig.patch?.addToSpec(path, spec, pluginConfig)
pluginConfig.path = path
}
private fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: Config) {
SchemaGenerator.fromTypeOrUnit(this.response.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema
}
errors.forEach { error ->
SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas)?.let { schema ->
spec.components.schemas[error.responseType.getSimpleSlug()] = schema
}
}
when (this) {
is MethodInfoWithRequest -> {
SchemaGenerator.fromTypeOrUnit(this.request.requestType, spec.components.schemas)?.let { schema ->
spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema
}
}
else -> {}
}
val operations = this.toPathOperation(config)
when (this) {
is DeleteInfo -> path.delete = operations
is GetInfo -> path.get = operations
is HeadInfo -> path.head = operations
is PatchInfo -> path.patch = operations
is PostInfo -> path.post = operations
is PutInfo -> path.put = operations
is OptionsInfo -> path.options = operations
}
}
private fun MethodInfo.toPathOperation(config: Config) = PathOperation(
tags = config.tags.plus(this.tags),
summary = this.summary,
description = this.description,
externalDocs = this.externalDocumentation,
operationId = this.operationId,
deprecated = this.deprecated,
parameters = this.parameters,
security = config.security
?.map { (k, v) -> k to v }
?.map { listOf(it).toMap() }
?.toList(),
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples),
required = true
)
else -> null
},
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples)
)
).plus(this.errors.toResponseMap())
)
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples)
)
}
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mapOf(
"application/json" to MediaType(
schema = ReferenceDefinition(this.getReferenceSlug()),
examples = examples
)
)
}
private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
}

View File

@ -1,10 +1,11 @@
package io.bkbn.kompendium.core.routes
import io.ktor.application.call
import io.ktor.html.respondHtml
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.link
@ -22,7 +23,7 @@ import kotlinx.html.unsafe
fun Routing.redoc(pageTitle: String = "Docs", specUrl: String = "/openapi.json") {
route("/docs") {
get {
call.respondHtml {
call.respondHtml(HttpStatusCode.OK) {
head {
title {
+pageTitle

View File

@ -0,0 +1,35 @@
package io.bkbn.kompendium.core.util
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.jvm.javaField
import org.slf4j.LoggerFactory
import java.lang.reflect.ParameterizedType
import java.util.Locale
object Helpers {
private const val COMPONENT_SLUG = "#/components/schemas"
fun KType.getSimpleSlug(): String = when {
this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>)
else -> (classifier as KClass<*>).simpleName ?: error("Could not determine simple name for $this")
}
fun KType.getReferenceSlug(): String = when {
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}"
}
/**
* Adapts a class with type parameters into a reference friendly string
*/
private fun genericNameAdapter(type: KType, clazz: KClass<*>): String {
val classNames = type.arguments
.map { it.type?.classifier as KClass<*> }
.map { it.simpleName }
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-")
}
}

View File

@ -0,0 +1,201 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
import io.bkbn.kompendium.core.util.TestModules.complexRequest
import io.bkbn.kompendium.core.util.TestModules.dateTimeString
import io.bkbn.kompendium.core.util.TestModules.defaultField
import io.bkbn.kompendium.core.util.TestModules.defaultParameter
import io.bkbn.kompendium.core.util.TestModules.exampleParams
import io.bkbn.kompendium.core.util.TestModules.nestedUnderRoot
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParams
import io.bkbn.kompendium.core.util.TestModules.notarizedDelete
import io.bkbn.kompendium.core.util.TestModules.notarizedGet
import io.bkbn.kompendium.core.util.TestModules.singleException
import io.bkbn.kompendium.core.util.TestModules.genericException
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.TestModules.headerParameter
import io.bkbn.kompendium.core.util.TestModules.multipleExceptions
import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam
import io.bkbn.kompendium.core.util.TestModules.polymorphicException
import io.bkbn.kompendium.core.util.TestModules.notarizedHead
import io.bkbn.kompendium.core.util.TestModules.notarizedOptions
import io.bkbn.kompendium.core.util.TestModules.notarizedPatch
import io.bkbn.kompendium.core.util.TestModules.notarizedPost
import io.bkbn.kompendium.core.util.TestModules.notarizedPut
import io.bkbn.kompendium.core.util.TestModules.nullableEnumField
import io.bkbn.kompendium.core.util.TestModules.nullableField
import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject
import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.primitives
import io.bkbn.kompendium.core.util.TestModules.reqRespExamples
import io.bkbn.kompendium.core.util.TestModules.requiredParams
import io.bkbn.kompendium.core.util.TestModules.returnsList
import io.bkbn.kompendium.core.util.TestModules.rootRoute
import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing
import io.bkbn.kompendium.core.util.TestModules.trailingSlash
import io.bkbn.kompendium.core.util.TestModules.withOperationId
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.kotest.core.spec.style.DescribeSpec
import kotlin.reflect.typeOf
import java.time.Instant
class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") {
it("Can notarize a get request") {
openApiTestAllSerializers("T0001__notarized_get.json") { notarizedGet() }
}
it("Can notarize a post request") {
openApiTestAllSerializers("T0002__notarized_post.json") { notarizedPost() }
}
it("Can notarize a put request") {
openApiTestAllSerializers("T0003__notarized_put.json") { notarizedPut() }
}
it("Can notarize a delete request") {
openApiTestAllSerializers("T0004__notarized_delete.json") { notarizedDelete() }
}
it("Can notarize a patch request") {
openApiTestAllSerializers("T0005__notarized_patch.json") { notarizedPatch() }
}
it("Can notarize a head request") {
openApiTestAllSerializers("T0006__notarized_head.json") { notarizedHead() }
}
it("Can notarize an options request") {
openApiTestAllSerializers("T0007__notarized_options.json") { notarizedOptions() }
}
it("Can notarize a complex type") {
openApiTestAllSerializers("T0008__complex_type.json") { complexRequest() }
}
it("Can notarize primitives") {
openApiTestAllSerializers("T0009__notarized_primitives.json") { primitives() }
}
it("Can notarize a top level list response") {
openApiTestAllSerializers("T0010__response_list.json") { returnsList() }
}
it("Can notarize a route with non-required params") {
openApiTestAllSerializers("T0011__non_required_params.json") { nonRequiredParams() }
}
}
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
openApiTestAllSerializers("T0012__path_parser.json") { simplePathParsing() }
}
it("Can notarize the root route") {
openApiTestAllSerializers("T0013__root_route.json") { rootRoute() }
}
it("Can notarize a route under the root module without appending trailing slash") {
openApiTestAllSerializers("T0014__nested_under_root.json") { nestedUnderRoot() }
}
it("Can notarize a route with a trailing slash") {
openApiTestAllSerializers("T0015__trailing_slash.json") { trailingSlash() }
}
}
describe("Exceptions") {
it("Can add an exception status code to a response") {
openApiTestAllSerializers("T0016__notarized_get_with_exception_response.json") { singleException() }
}
it("Can support multiple response codes") {
openApiTestAllSerializers("T0017__notarized_get_with_multiple_exception_responses.json") { multipleExceptions() }
}
it("Can add a polymorphic exception response") {
openApiTestAllSerializers("T0018__polymorphic_error_status_codes.json") { polymorphicException() }
}
it("Can add a generic exception response") {
openApiTestAllSerializers("T0019__generic_exception.json") { genericException() }
}
}
describe("Examples") {
it("Can generate example response and request bodies") {
openApiTestAllSerializers("T0020__example_req_and_resp.json") { reqRespExamples() }
}
it("Can describe example parameters") {
openApiTestAllSerializers("T0021__example_parameters.json") { exampleParams() }
}
}
describe("Defaults") {
it("Can generate a default parameter value") {
openApiTestAllSerializers("T0022__query_with_default_parameter.json") { defaultParameter() }
}
}
describe("Required Fields") {
it("Marks a parameter as required if there is no default and it is not marked nullable") {
openApiTestAllSerializers("T0023__required_param.json") { requiredParams() }
}
it("Can mark a parameter as not required") {
openApiTestAllSerializers("T0024__non_required_param.json") { nonRequiredParam() }
}
it("Does not mark a field as required if a default value is provided") {
openApiTestAllSerializers("T0025__default_field.json") { defaultField() }
}
it("Does not mark a nullable field as required") {
openApiTestAllSerializers("T0026__nullable_field.json") { nullableField() }
}
}
describe("Polymorphism and Generics") {
it("can generate a polymorphic response type") {
openApiTestAllSerializers("T0027__polymorphic_response.json") { polymorphicResponse() }
}
it("Can generate a collection with polymorphic response type") {
openApiTestAllSerializers("T0028__polymorphic_list_response.json") { polymorphicCollectionResponse() }
}
it("Can generate a map with a polymorphic response type") {
openApiTestAllSerializers("T0029__polymorphic_map_response.json") { polymorphicMapResponse() }
}
it("Can generate a response type with a generic type") {
openApiTestAllSerializers("T0030__simple_generic_response.json") { simpleGenericResponse() }
}
it("Can generate a response type with a nested generic type") {
openApiTestAllSerializers("T0031__nested_generic_response.json") { nestedGenericResponse() }
}
it("Can generate a polymorphic response type with generics") {
openApiTestAllSerializers("T0032__polymorphic_response_with_generics.json") { genericPolymorphicResponse() }
}
it("Can handle an absolutely psycho inheritance test") {
openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
}
}
describe("Miscellaneous") {
xit("Can generate the necessary ReDoc home page") {
// TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
}
it("Can add an operation id to a notarized route") {
openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() }
}
xit("Can add an undeclared field") {
// TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() }
}
it("Can add a custom header parameter with a name override") {
openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() }
}
xit("Can override field name") {
// TODO Assess strategies here
}
xit("Can serialize a recursive type") {
// TODO openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() }
}
it("Nullable fields do not lead to doom") {
openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() }
}
it("Can have a nullable enum as a member field") {
openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() }
}
}
describe("Constraints") {
// TODO Assess strategies here
}
describe("Formats") {
it("Can set a format for a simple type schema") {
openApiTestAllSerializers(
snapshotName = "T0038__formatted_date_time_string.json",
customTypes = mapOf(typeOf<Instant>() to TypeDefinition(type = "string", format = "date"))
) { dateTimeString() }
}
}
describe("Free Form") {
// todo Assess strategies here
}
})

View File

@ -0,0 +1,609 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ColumnSchema
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.DateTimeString
import io.bkbn.kompendium.core.fixtures.DefaultField
import io.bkbn.kompendium.core.fixtures.ExceptionResponse
import io.bkbn.kompendium.core.fixtures.Flibbity
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.NullableEnum
import io.bkbn.kompendium.core.fixtures.NullableField
import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
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.TestSimpleRequest
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.HeadInfo
import io.bkbn.kompendium.core.metadata.OptionsInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import io.ktor.server.routing.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.head
import io.ktor.server.routing.options
import io.ktor.server.routing.patch
import io.ktor.server.routing.post
import io.ktor.server.routing.put
import io.ktor.server.routing.route
object TestModules {
private const val defaultPath = "/test/{a}"
private const val rootPath = "/"
private const val defaultResponseDescription = "A Successful Endeavor"
private const val defaultRequestDescription = "You gotta send it"
private const val defaultPathSummary = "Great Summary!"
private const val defaultPathDescription = "testing more"
private val defaultParams = listOf(
Parameter(
name = "a",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING,
),
Parameter(
name = "aa",
`in` = Parameter.Location.query,
schema = TypeDefinition.INT
)
)
fun Routing.notarizedGet() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
get = GetInfo.builder {
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description(defaultResponseDescription)
}
summary(defaultPathSummary)
description(defaultPathDescription)
}
}
get {
call.respondText { "hey dude ‼️ congrats on the get request" }
}
}
}
fun Routing.notarizedPost() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<TestSimpleRequest>()
description("A Test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
post {
call.respondText { "hey dude ‼️ congrats on the post request" }
}
}
}
fun Routing.notarizedPut() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<TestSimpleRequest>()
description("A Test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
put {
call.respondText { "hey dude ‼️ congrats on the post request" }
}
}
}
fun Routing.notarizedDelete() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
delete = DeleteInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseCode(HttpStatusCode.NoContent)
responseType<Unit>()
description(defaultResponseDescription)
}
}
}
}
delete {
call.respond(HttpStatusCode.NoContent)
}
}
fun Routing.notarizedPatch() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
patch = PatchInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
description("A Test request")
requestType<TestSimpleRequest>()
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
patch {
call.respond(HttpStatusCode.Created) { TestCreatedResponse(123, "Nice!") }
}
}
}
fun Routing.notarizedHead() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
head = HeadInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description("great!")
responseCode(HttpStatusCode.Created)
responseType<Unit>()
}
}
}
head {
call.respond(HttpStatusCode.OK)
}
}
}
fun Routing.notarizedOptions() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
options = OptionsInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("nice")
}
}
}
options {
call.respond(HttpStatusCode.NoContent)
}
}
}
fun Routing.complexRequest() {
route(rootPath) {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<ComplexRequest>()
description("A Complex request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
patch {
call.respond(HttpStatusCode.Created, TestCreatedResponse(123, "nice!"))
}
}
}
fun Routing.primitives() {
route(rootPath) {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<Int>()
description("A Test Request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<Boolean>()
description(defaultResponseDescription)
}
}
}
}
}
fun Routing.returnsList() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description("A Successful List-y Endeavor")
responseCode(HttpStatusCode.OK)
responseType<List<TestResponse>>()
}
}
}
}
}
fun Routing.nonRequiredParams() {
route("/optional") {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "notRequired",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
required = false,
),
Parameter(
name = "required",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseType<Unit>()
description("Empty")
responseCode(HttpStatusCode.NoContent)
}
}
}
}
}
fun Routing.simplePathParsing() {
route("/this") {
route("/is") {
route("/a") {
route("/complex") {
route("path") {
route("with/an/{id}") {
install(NotarizedRoute()) {
get = GetInfo.builder {
parameters = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
}
}
}
}
fun Routing.rootRoute() {
route(rootPath) {
install(NotarizedRoute()) {
parameters = listOf(defaultParams.last())
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
fun Routing.nestedUnderRoot() {
route("/") {
route("/testerino") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
fun Routing.trailingSlash() {
route("/test") {
route("/") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
fun Routing.singleException() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.BadRequest)
responseType<ExceptionResponse>()
}
}
}
}
}
fun Routing.multipleExceptions() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.BadRequest)
responseType<ExceptionResponse>()
}
canRespond {
description("Access Denied")
responseCode(HttpStatusCode.Forbidden)
responseType<ExceptionResponse>()
}
}
}
}
}
fun Routing.polymorphicException() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.InternalServerError)
responseType<FlibbityGibbit>()
}
}
}
}
}
fun Routing.genericException() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.BadRequest)
responseType<Flibbity<String>>()
}
}
}
}
}
fun Routing.reqRespExamples() {
route(rootPath) {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
description(defaultRequestDescription)
requestType<TestRequest>()
examples(
"Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList())
)
}
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
examples(
"Testerino" to TestResponse("Heya")
)
}
}
}
}
}
fun Routing.exampleParams() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING,
examples = mapOf(
"foo" to Parameter.Example("testing")
)
)
)
)
fun Routing.defaultParameter() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING.withDefault("IDK")
)
)
)
fun Routing.requiredParams() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
)
fun Routing.nonRequiredParam() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
required = false
)
)
)
fun Routing.defaultField() = basicGetGenerator<DefaultField>()
fun Routing.nullableField() = basicGetGenerator<NullableField>()
fun Routing.polymorphicResponse() = basicGetGenerator<FlibbityGibbit>()
fun Routing.polymorphicCollectionResponse() = basicGetGenerator<List<FlibbityGibbit>>()
fun Routing.polymorphicMapResponse() = basicGetGenerator<Map<String, FlibbityGibbit>>()
fun Routing.simpleGenericResponse() = basicGetGenerator<Gibbity<String>>()
fun Routing.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>()
fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
fun Routing.nullableEnumField() = basicGetGenerator<NullableEnum>()
fun Routing.dateTimeString() = basicGetGenerator<DateTimeString>()
fun Routing.headerParameter() = basicGetGenerator<TestResponse>( params = listOf(
Parameter(
name = "X-User-Email",
`in` = Parameter.Location.header,
schema = TypeDefinition.STRING,
required = true
)
))
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
private inline fun <reified T> Routing.basicGetGenerator(
params: List<Parameter> = emptyList(),
operationId: String? = null
) {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
operationId?.let { operationId(it) }
parameters = params
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<T>()
}
}
}
}
}
}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,12 +27,26 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"description": "testing more",
"operationId": "getTest",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
@ -46,32 +61,20 @@
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -79,8 +82,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,38 +27,18 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"post": {
"tags": [],
"summary": "Test post endpoint",
"description": "Post your tests here!",
"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
}
],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
"$ref": "#/components/schemas/TestSimpleRequest"
}
}
},
@ -76,61 +57,64 @@
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestRequest": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"field_name",
"b",
"aaa"
],
"type": "object"
},
"TestNested": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
},
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
"type": "number",
"format": "int32"
}
},
"required": [
"id",
"c"
],
"type": "object"
"c",
"id"
]
},
"TestSimpleRequest": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "number",
"format": "int32"
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,38 +27,18 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"put": {
"tags": [],
"summary": "Test put endpoint",
"description": "Put your tests here!",
"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
}
],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
"$ref": "#/components/schemas/TestSimpleRequest"
}
}
},
@ -76,61 +57,64 @@
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestRequest": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"field_name",
"b",
"aaa"
],
"type": "object"
},
"TestNested": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
},
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
"type": "number",
"format": "int32"
}
},
"required": [
"id",
"c"
],
"type": "object"
"c",
"id"
]
},
"TestSimpleRequest": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "number",
"format": "int32"
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,47 +27,45 @@
}
],
"paths": {
"/test/polymorphic": {
"get": {
"/test/{a}": {
"delete": {
"tags": [],
"summary": "Single Generic",
"description": "Simple generic data class",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestGeneric-Int"
}
}
}
"204": {
"description": "A Successful Endeavor"
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {
"TestGeneric-Int": {
"properties": {
"messy": {
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"potato": {
"format": "int32",
"type": "integer"
}
},
"required": [
"messy",
"potato"
],
"type": "object"
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {},
"securitySchemes": {}
},
"security": [],

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,18 +27,18 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"patch": {
"tags": [],
"summary": "Test patch endpoint",
"description": "patch your tests here!",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
"$ref": "#/components/schemas/TestSimpleRequest"
}
}
},
@ -49,63 +50,71 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestRequest": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"field_name",
"b",
"aaa"
],
"type": "object"
},
"TestNested": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
},
"TestResponse": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c"
],
"type": "object"
"c",
"id"
]
},
"TestSimpleRequest": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "number",
"format": "int32"
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,21 +27,43 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"head": {
"tags": [],
"summary": "Test head endpoint",
"description": "head test 💀",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"201": {
"description": "great!"
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,11 +27,26 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"options": {
"tags": [],
"summary": "Test options",
"description": "endpoint of options",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "nice",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
@ -45,32 +61,20 @@
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "nice",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -78,8 +82,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,11 +27,11 @@
}
],
"paths": {
"/test": {
"/": {
"put": {
"tags": [],
"summary": "Test put endpoint",
"description": "Put your tests here!",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Complex request",
@ -56,14 +57,33 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ComplexRequest": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"amazing_field": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"ComplexRequest": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
@ -77,13 +97,13 @@
}
},
"required": [
"amazingField",
"org",
"amazing_field",
"tables"
],
"type": "object"
]
},
"NestedComplexItem": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
@ -96,44 +116,23 @@
}
},
"required": [
"name",
"alias"
],
"type": "object"
"alias",
"name"
]
},
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"$ref": "#/components/schemas/SimpleEnum"
"enum": [
"ONE",
"TWO"
]
}
},
"required": [
"enumeration"
],
"type": "object"
},
"SimpleEnum": {
"enum": [
"ONE",
"TWO"
],
"type": "string"
},
"TestCreatedResponse": {
"properties": {
"c": {
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
}
},
"required": [
"id",
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,19 +27,18 @@
}
],
"paths": {
"/test": {
"/": {
"put": {
"tags": [],
"summary": "Test put endpoint",
"description": "Put your tests here!",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A Test request",
"description": "A Test Request",
"content": {
"application/json": {
"schema": {
"format": "int32",
"type": "integer"
"$ref": "#/components/schemas/Int"
}
}
},
@ -50,18 +50,28 @@
"content": {
"application/json": {
"schema": {
"type": "boolean"
"$ref": "#/components/schemas/Boolean"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {},
"schemas": {
"Boolean": {
"type": "boolean"
},
"Int": {
"type": "number",
"format": "int32"
}
},
"securitySchemes": {}
},
"security": [],

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,11 +27,26 @@
}
],
"paths": {
"/test": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful List-y Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/List-TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
@ -45,35 +61,20 @@
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A Successful List-y Endeavor",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/TestResponse"
},
"type": "array"
}
}
}
}
},
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -81,8 +82,13 @@
},
"required": [
"c"
],
"type": "object"
]
},
"List-TestResponse": {
"items": {
"$ref": "#/components/schemas/TestResponse"
},
"type": "array"
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,18 +27,25 @@
}
],
"paths": {
"/test/optional": {
"/optional": {
"get": {
"tags": [],
"summary": "No request params and response body",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"204": {
"description": "Empty"
}
},
"deprecated": false
},
"parameters": [
{
"name": "notRequired",
"in": "query",
"schema": {
"type": "string",
"nullable": true
"type": "string"
},
"required": false,
"deprecated": false
@ -51,16 +59,10 @@
"required": true,
"deprecated": false
}
],
"responses": {
"204": {
"description": "Empty"
}
},
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -29,27 +30,17 @@
"/this/is/a/complex/path/with/an/{id}": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [
{
"name": "a",
"name": "id",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
@ -65,12 +56,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -78,8 +72,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,32 +27,12 @@
}
],
"paths": {
"/testerino": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"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
}
],
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
@ -65,12 +46,26 @@
}
},
"deprecated": false
},
"parameters": [
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -78,8 +73,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,26 +27,15 @@
}
],
"paths": {
"/test/required_param": {
"/testerino": {
"get": {
"tags": [],
"summary": "default param",
"description": "Cool stuff",
"parameters": [
{
"name": "b",
"in": "query",
"schema": {
"type": "string",
"default": "heyo"
},
"required": false,
"deprecated": false
}
],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
@ -56,12 +46,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -69,8 +62,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -0,0 +1,72 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"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": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,32 +27,12 @@
}
],
"paths": {
"/test": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"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
}
],
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
@ -75,12 +56,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -88,10 +72,10 @@
},
"required": [
"c"
],
"type": "object"
]
},
"ExceptionResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
@ -99,8 +83,7 @@
},
"required": [
"message"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,32 +27,12 @@
}
],
"paths": {
"/test": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"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
}
],
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
@ -63,16 +44,6 @@
}
}
},
"403": {
"description": "Access Denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
},
"400": {
"description": "Bad Things Happened",
"content": {
@ -82,15 +53,28 @@
}
}
}
},
"403": {
"description": "Access Denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExceptionResponse"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -98,10 +82,10 @@
},
"required": [
"c"
],
"type": "object"
]
},
"ExceptionResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
@ -109,8 +93,7 @@
},
"required": [
"message"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,32 +27,12 @@
}
],
"paths": {
"/test": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"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
}
],
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
@ -63,31 +44,27 @@
}
}
},
"501": {
"description": "The Gibbits are ANGRY",
"500": {
"description": "Bad Things Happened",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SimpleGibbit"
},
{
"$ref": "#/components/schemas/ComplexGibbit"
}
]
"$ref": "#/components/schemas/FlibbityGibbit"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -95,10 +72,30 @@
},
"required": [
"c"
],
"type": "object"
]
},
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
@ -109,27 +106,17 @@
},
"required": [
"a"
],
"type": "object"
]
},
"ComplexGibbit": {
"properties": {
"b": {
"type": "string"
"FlibbityGibbit": {
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
"c": {
"format": "int32",
"type": "integer"
},
"z": {
"type": "string"
{
"$ref": "#/components/schemas/SimpleGibbit"
}
},
"required": [
"b",
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,32 +27,12 @@
}
],
"paths": {
"/test": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"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
}
],
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
@ -64,30 +45,26 @@
}
},
"400": {
"description": "Wow serious things went wrong",
"description": "Bad Things Happened",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Gibbity-String"
},
{
"$ref": "#/components/schemas/Bibbity-String"
}
]
"$ref": "#/components/schemas/Flibbity-String"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -95,21 +72,10 @@
},
"required": [
"c"
],
"type": "object"
},
"Gibbity-String": {
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
]
},
"Bibbity-String": {
"type": "object",
"properties": {
"b": {
"type": "string"
@ -121,8 +87,28 @@
"required": [
"b",
"f"
],
"type": "object"
]
},
"Gibbity-String": {
"type": "object",
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
]
},
"Flibbity-String": {
"anyOf": [
{
"$ref": "#/components/schemas/Bibbity-String"
},
{
"$ref": "#/components/schemas/Gibbity-String"
}
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,39 +27,28 @@
}
],
"paths": {
"/test/examples": {
"/": {
"post": {
"tags": [],
"summary": "Example Parameters",
"description": "A test for setting parameter examples",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "Test",
"description": "You gotta send it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
},
"examples": {
"one": {
"Testerina": {
"value": {
"fieldName": {
"nesty": "hey"
"nesty": "asdf"
},
"b": 4.0,
"b": 1.5,
"aaa": []
}
},
"two": {
"value": {
"fieldName": {
"nesty": "hello"
},
"b": 3.8,
"aaa": [
31324234
]
}
}
}
}
@ -66,17 +56,17 @@
"required": true
},
"responses": {
"201": {
"description": "nice",
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
},
"examples": {
"test": {
"Testerino": {
"value": {
"c": "spud"
"c": "Heya"
}
}
}
@ -85,47 +75,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestRequest": {
"properties": {
"aaa": {
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"b": {
"format": "double",
"type": "number"
},
"field_name": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"field_name",
"b",
"aaa"
],
"type": "object"
},
"TestNested": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
},
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -133,8 +91,42 @@
},
"required": [
"c"
],
"type": "object"
]
},
"TestRequest": {
"type": "object",
"properties": {
"aaa": {
"items": {
"type": "number",
"format": "int64"
},
"type": "array"
},
"b": {
"type": "number",
"format": "double"
},
"fieldName": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"aaa",
"b",
"fieldName"
]
},
"TestNested": {
"type": "object",
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,30 +27,25 @@
}
],
"paths": {
"/test/": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [
{
"name": "a",
"name": "id",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
"deprecated": false,
"examples": {
"foo": {
"value": "testing"
}
}
}
],
"responses": {
@ -65,12 +61,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -78,8 +77,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,27 +27,18 @@
}
],
"paths": {
"/test": {
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [
{
"name": "a",
"name": "id",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
"type": "string",
"default": "IDK"
},
"required": true,
"deprecated": false
@ -65,12 +57,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -78,8 +73,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -29,27 +30,17 @@
"/": {
"get": {
"tags": [],
"summary": "Another get test",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [
{
"name": "a",
"name": "id",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
@ -65,12 +56,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -78,8 +72,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,25 +27,25 @@
}
],
"paths": {
"/test/required_param": {
"/": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [
{
"name": "a",
"name": "id",
"in": "query",
"schema": {
"type": "string"
},
"required": true,
"required": false,
"deprecated": false
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
@ -55,12 +56,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -68,8 +72,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,45 +27,46 @@
}
],
"paths": {
"/test/constrained_int": {
"/": {
"get": {
"tags": [],
"summary": "Constrained int field",
"description": "Cool stuff",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MinMaxDouble"
"$ref": "#/components/schemas/DefaultField"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"MinMaxDouble": {
"DefaultField": {
"type": "object",
"properties": {
"a": {
"format": "double",
"type": "string"
},
"b": {
"type": "number",
"minimum": 5.5,
"maximum": 13.37,
"exclusiveMinimum": false,
"exclusiveMaximum": false
"format": "int32"
}
},
"required": [
"a"
],
"type": "object"
"b"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,45 +27,47 @@
}
],
"paths": {
"/test/constrained_int": {
"/": {
"get": {
"tags": [],
"summary": "Constrained int field",
"description": "Cool stuff",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExclusiveMinMax"
"$ref": "#/components/schemas/NullableField"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ExclusiveMinMax": {
"NullableField": {
"type": "object",
"properties": {
"a": {
"format": "int32",
"type": "integer",
"minimum": 5,
"maximum": 100,
"exclusiveMinimum": true,
"exclusiveMaximum": true
"oneOf": [
{
"type": "null"
},
{
"type": "string"
}
]
}
},
"required": [
"a"
],
"type": "object"
"required": []
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,41 +27,54 @@
}
],
"paths": {
"/test/polymorphiclist": {
"/": {
"get": {
"tags": [],
"summary": "Oh so many gibbits",
"description": "Polymorphic list response",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/SimpleGibbit"
},
{
"$ref": "#/components/schemas/ComplexGibbit"
}
]
},
"type": "array"
"$ref": "#/components/schemas/FlibbityGibbit"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
@ -71,27 +85,17 @@
},
"required": [
"a"
],
"type": "object"
]
},
"ComplexGibbit": {
"properties": {
"b": {
"type": "string"
"FlibbityGibbit": {
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
"c": {
"format": "int32",
"type": "integer"
},
"z": {
"type": "string"
{
"$ref": "#/components/schemas/SimpleGibbit"
}
},
"required": [
"b",
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,41 +27,54 @@
}
],
"paths": {
"/test/polymorphicmap": {
"/": {
"get": {
"tags": [],
"summary": "By gawd that's a lot of gibbits",
"description": "Polymorphic list response",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"anyOf": [
{
"$ref": "#/components/schemas/SimpleGibbit"
},
{
"$ref": "#/components/schemas/ComplexGibbit"
}
]
},
"type": "object"
"$ref": "#/components/schemas/List-FlibbityGibbit"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
@ -71,27 +85,20 @@
},
"required": [
"a"
],
"type": "object"
]
},
"ComplexGibbit": {
"properties": {
"b": {
"type": "string"
"List-FlibbityGibbit": {
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
"c": {
"format": "int32",
"type": "integer"
},
"z": {
"type": "string"
{
"$ref": "#/components/schemas/SimpleGibbit"
}
]
},
"required": [
"b",
"c"
],
"type": "object"
"type": "array"
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,38 +27,54 @@
}
],
"paths": {
"/test/polymorphic": {
"/": {
"get": {
"tags": [],
"summary": "All the gibbits",
"description": "Polymorphic response",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SimpleGibbit"
},
{
"$ref": "#/components/schemas/ComplexGibbit"
}
]
"$ref": "#/components/schemas/Map-String-FlibbityGibbit"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
@ -68,26 +85,19 @@
},
"required": [
"a"
],
"type": "object"
]
},
"ComplexGibbit": {
"properties": {
"b": {
"type": "string"
"Map-String-FlibbityGibbit": {
"additionalProperties": {
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
"c": {
"format": "int32",
"type": "integer"
},
"z": {
"type": "string"
{
"$ref": "#/components/schemas/SimpleGibbit"
}
]
},
"required": [
"b",
"c"
],
"type": "object"
}
},

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,31 +27,34 @@
}
],
"paths": {
"/test/date_time_format": {
"/": {
"get": {
"tags": [],
"summary": "Date time string test",
"description": "Cool stuff",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DateTimeString"
"$ref": "#/components/schemas/Gibbity-String"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"DateTimeString": {
"Gibbity-String": {
"type": "object",
"properties": {
"a": {
"type": "string"
@ -58,8 +62,7 @@
},
"required": [
"a"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,45 +27,45 @@
}
],
"paths": {
"/test/required_param": {
"/": {
"get": {
"tags": [],
"summary": "required param",
"description": "Cool stuff",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MinMaxArray"
"$ref": "#/components/schemas/Gibbity-Map"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"MinMaxArray": {
"Gibbity-Map": {
"type": "object",
"properties": {
"a": {
"items": {
"additionalProperties": {
"type": "string"
},
"minItems": 1,
"maxItems": 10,
"type": "array"
"type": "object"
}
},
"required": [
"a"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,73 +27,69 @@
}
],
"paths": {
"/test/polymorphic": {
"/": {
"get": {
"tags": [],
"summary": "More flibbity",
"description": "Polymorphic with generics",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Gibbity-TestNested"
},
{
"$ref": "#/components/schemas/Bibbity-TestNested"
}
]
"$ref": "#/components/schemas/Flibbity-Double"
}
}
}
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Gibbity-TestNested": {
"properties": {
"a": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"a"
],
"type": "object"
},
"TestNested": {
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
],
"type": "object"
},
"Bibbity-TestNested": {
"Bibbity-Double": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"f": {
"$ref": "#/components/schemas/TestNested"
"type": "number",
"format": "double"
}
},
"required": [
"b",
"f"
],
"type": "object"
]
},
"Gibbity-Double": {
"type": "object",
"properties": {
"a": {
"type": "number",
"format": "double"
}
},
"required": [
"a"
]
},
"Flibbity-Double": {
"anyOf": [
{
"$ref": "#/components/schemas/Bibbity-Double"
},
{
"$ref": "#/components/schemas/Gibbity-Double"
}
]
}
},
"securitySchemes": {}

View File

@ -0,0 +1,145 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"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": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Flibbity-FlibbityGibbit"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ComplexGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"c": {
"type": "number",
"format": "int32"
},
"z": {
"type": "string"
}
},
"required": [
"b",
"c",
"z"
]
},
"SimpleGibbit": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"z": {
"type": "string"
}
},
"required": [
"a"
]
},
"Bibbity-FlibbityGibbit": {
"type": "object",
"properties": {
"b": {
"type": "string"
},
"f": {
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
{
"$ref": "#/components/schemas/SimpleGibbit"
}
]
}
},
"required": [
"b",
"f"
]
},
"Gibbity-FlibbityGibbit": {
"type": "object",
"properties": {
"a": {
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
{
"$ref": "#/components/schemas/SimpleGibbit"
}
]
}
},
"required": [
"a"
]
},
"Flibbity-FlibbityGibbit": {
"anyOf": [
{
"$ref": "#/components/schemas/Bibbity-FlibbityGibbit"
},
{
"$ref": "#/components/schemas/Gibbity-FlibbityGibbit"
}
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,26 +27,16 @@
}
],
"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
}
],
"summary": "Great Summary!",
"description": "testing more",
"operationId": "getThisDude",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
@ -56,12 +47,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -69,8 +63,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,14 +27,14 @@
}
],
"paths": {
"/test/with_header": {
"/": {
"get": {
"tags": [],
"summary": "testing header stuffs",
"description": "Good for many things",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [
{
"name": "X-UserEmail",
"name": "X-User-Email",
"in": "header",
"schema": {
"type": "string"
@ -44,7 +45,7 @@
],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
@ -55,12 +56,15 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
@ -68,8 +72,7 @@
},
"required": [
"c"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -0,0 +1,121 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"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": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProfileUpdateRequest"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"ProfileUpdateRequest": {
"type": "object",
"properties": {
"metadata": {
"oneOf": [
{
"type": "null"
},
{
"type": "object",
"properties": {
"isPrivate": {
"oneOf": [
{
"type": "null"
},
{
"type": "boolean"
}
]
},
"otherThing": {
"oneOf": [
{
"type": "null"
},
{
"type": "string"
}
]
}
},
"required": []
}
]
},
"mood": {
"oneOf": [
{
"type": "null"
},
{
"type": "string"
}
]
},
"viewCount": {
"oneOf": [
{
"type": "null"
},
{
"type": "number",
"format": "int64"
}
]
}
},
"required": []
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,15 +27,15 @@
}
],
"paths": {
"/nullable/enum": {
"/": {
"get": {
"tags": [],
"summary": "Has a nullable enum field",
"description": "should still work!",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
@ -45,25 +46,31 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"NullableEnum": {
"type": "object",
"properties": {
"a": {
"$ref": "#/components/schemas/TestEnum"
}
"oneOf": [
{
"type": "null"
},
"type": "object"
},
"TestEnum": {
{
"enum": [
"YES",
"NO"
],
"type": "string"
]
}
]
}
},
"required": []
}
},
"securitySchemes": {}

View File

@ -1,5 +1,6 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
@ -26,15 +27,15 @@
}
],
"paths": {
"/test/date_time_format": {
"/": {
"get": {
"tags": [],
"summary": "Date time string test",
"description": "Cool stuff",
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A successful endeavor",
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
@ -45,22 +46,28 @@
}
},
"deprecated": false
}
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Instant": {
"type": "string",
"format": "date"
},
"DateTimeString": {
"type": "object",
"properties": {
"a": {
"type": "string",
"format": "date-time"
"format": "date"
}
},
"required": [
"a"
],
"type": "object"
]
}
},
"securitySchemes": {}

View File

@ -0,0 +1,104 @@
package io.bkbn.kompendium.core.fixtures
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.oas.server.Server
import io.kotest.assertions.json.shouldEqualJson
import io.kotest.assertions.ktor.client.shouldHaveStatus
import io.kotest.matchers.shouldNot
import io.kotest.matchers.string.beBlank
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.gson.gson
import io.ktor.serialization.jackson.jackson
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.Routing
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import kotlin.reflect.KType
import kotlinx.serialization.json.Json
import java.io.File
import java.net.URI
object TestHelpers {
private const val OPEN_API_ENDPOINT = "/openapi.json"
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
/**
* Performs the baseline expected tests on an OpenAPI result. Confirms that the endpoint
* exists as expected, and that the content matches the expected blob found in the specified file
* @param snapshotName The snapshot file to retrieve from the resources folder
*/
private suspend fun ApplicationTestBuilder.compareOpenAPISpec(snapshotName: String) {
val response = client.get(OPEN_API_ENDPOINT)
response shouldHaveStatus HttpStatusCode.OK
response.bodyAsText() shouldNot beBlank()
response.bodyAsText() shouldEqualJson getFileSnapshot(snapshotName)
}
/**
* This will take a provided JSON snapshot file, retrieve it from the resource folder,
* and build a test ktor server to compare the expected output with the output found in the default
* OpenAPI json endpoint. By default, this will run the same test with Gson, Kotlinx, and Jackson serializers
* @param snapshotName The snapshot file to retrieve from the resources folder
* @param moduleFunction Initializer for the application to allow tests to pass the required Ktor modules
*/
fun openApiTestAllSerializers(
snapshotName: String,
customTypes: Map<KType, JsonSchema> = emptyMap(),
routeUnderTest: Routing.() -> Unit
) {
openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, customTypes)
openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, customTypes)
openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, customTypes)
}
private fun openApiTest(
snapshotName: String,
serializer: SupportedSerializer,
routeUnderTest: Routing.() -> Unit,
typeOverrides: Map<KType, JsonSchema> = emptyMap()
) = testApplication {
install(NotarizedApplication()) {
customTypes = typeOverrides
spec = defaultSpec()
}
install(ContentNegotiation) {
when (serializer) {
SupportedSerializer.KOTLINX -> json(Json {
encodeDefaults = true
explicitNulls = false
serializersModule = KompendiumSerializersModule.module
})
SupportedSerializer.GSON -> gson()
SupportedSerializer.JACKSON -> jackson(ContentType.Application.Json) {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
}
routing {
redoc()
routeUnderTest()
}
compareOpenAPISpec(snapshotName)
}
}

View File

@ -0,0 +1,120 @@
package io.bkbn.kompendium.core.fixtures
import kotlinx.serialization.Serializable
import java.time.Instant
@Serializable
data class TestNested(val nesty: String)
@Serializable
data class TestRequest(
val fieldName: TestNested,
val b: Double,
val aaa: List<Long>
)
@Serializable
data class TestSimpleRequest(
val a: String,
val b: Int
)
@Serializable
data class TestResponse(val c: String)
@Serializable
enum class TestEnum {
YES,
NO
}
@Serializable
data class NullableEnum(val a: TestEnum? = null)
data class TestCreatedResponse(val id: Int, val c: String)
data class DateTimeString(
val a: Instant
)
data class DefaultField(
val a: String = "hi",
val b: Int
)
data class NullableField(
val a: String?
)
data class ComplexRequest(
val org: String,
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 ExceptionResponse(val message: String)
sealed class FlibbityGibbit {
abstract val z: String
}
data class SimpleGibbit(val a: String, override val z: String = "z") : FlibbityGibbit()
data class ComplexGibbit(val b: String, val c: Int, override val z: String) : 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>
data class NestedFlibbity<T>(
val flibbity: Flibbity<T>
)
enum class ColumnMode {
NULLABLE,
REQUIRED,
REPEATED
}
data class ColumnSchema(
val name: String,
val type: String,
val description: String,
val mode: ColumnMode,
val subColumns: List<ColumnSchema> = emptyList()
)
@Serializable
public data class ProfileUpdateRequest(
public val mood: String?,
public val viewCount: Long?,
public val metadata: ProfileMetadataUpdateRequest?
)
@Serializable
public data class ProfileMetadataUpdateRequest(
public val isPrivate: Boolean?,
public val otherThing: String?
)

View File

@ -13,7 +13,7 @@ object TestSpecs {
info = Info(
title = "Test API",
version = "1.33.7",
description = "An amazing, fully-ish 😉 generated API spec",
description = "An amazing, fully-ish \uD83D\uDE09 generated API spec",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",

View File

@ -4,17 +4,15 @@ complexity:
LongParameterList:
active: true
functionThreshold: 10
constructorThreshold: 10
constructorThreshold: 15
ComplexMethod:
threshold: 20
formatting:
ParameterListWrapping:
active: false
style:
MaxLineLength:
excludes: ['**/test/**/*']
active: true
maxLineLength: 120
excludeCommentStatements: true
MagicNumber:
excludes: ['**/kompendium-playground/**/*', '**/test/**/*']
naming:

View File

@ -1,5 +1,5 @@
# Kompendium
project.version=2.3.5
project.version=3.0.0-alpha
# Kotlin
kotlin.code.style=official
# Gradle
@ -8,5 +8,5 @@ org.gradle.vfs.verbose=true
org.gradle.jvmargs=-Xmx2000m
# Dependencies
ktorVersion=1.6.8
ktorVersion=2.1.0
kotestVersion=5.4.2

View File

@ -0,0 +1,32 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdoughLibrary {
libraryName.set("Kompendium JSON Schema")
libraryDescription.set("Json Schema Generator")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.10")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
testImplementation(testFixtures(projects.kompendiumCore))
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -0,0 +1,65 @@
package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.handler.CollectionHandler
import io.bkbn.kompendium.json.schema.handler.EnumHandler
import io.bkbn.kompendium.json.schema.handler.MapHandler
import io.bkbn.kompendium.json.schema.handler.SimpleObjectHandler
import io.bkbn.kompendium.json.schema.handler.SealedObjectHandler
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.typeOf
object SchemaGenerator {
inline fun <reified T : Any?> fromTypeToSchema(cache: MutableMap<String, JsonSchema> = mutableMapOf()) =
fromTypeToSchema(typeOf<T>(), cache)
fun fromTypeToSchema(type: KType, cache: MutableMap<String, JsonSchema>): JsonSchema {
cache[type.getSimpleSlug()]?.let {
return it
}
return when (val clazz = type.classifier as KClass<*>) {
Unit::class -> error(
"""
Unit cannot be converted to JsonSchema.
If you are looking for a method will return null when called with Unit,
please call SchemaGenerator.fromTypeOrUnit()
""".trimIndent()
)
Int::class -> checkForNull(type, TypeDefinition.INT)
Long::class -> checkForNull(type, TypeDefinition.LONG)
Double::class -> checkForNull(type, TypeDefinition.DOUBLE)
Float::class -> checkForNull(type, TypeDefinition.FLOAT)
String::class -> checkForNull(type, TypeDefinition.STRING)
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
else -> when {
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache)
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache)
else -> {
if (clazz.isSealed) {
SealedObjectHandler.handle(type, clazz, cache)
} else {
SimpleObjectHandler.handle(type, clazz, cache)
}
}
}
}
}
fun fromTypeOrUnit(type: KType, cache: MutableMap<String, JsonSchema> = mutableMapOf()): JsonSchema? =
when (type.classifier as KClass<*>) {
Unit::class -> null
else -> fromTypeToSchema(type, cache)
}
private fun checkForNull(type: KType, schema: JsonSchema): JsonSchema = when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), schema)
false -> schema
}
}

View File

@ -0,0 +1,6 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class AnyOfDefinition(val anyOf: Set<JsonSchema>) : JsonSchema

View File

@ -0,0 +1,10 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class ArrayDefinition(
val items: JsonSchema
) : JsonSchema {
val type: String = "array"
}

View File

@ -0,0 +1,8 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class EnumDefinition(
val enum: Set<String>
) : JsonSchema

View File

@ -0,0 +1,33 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable(with = JsonSchema.Serializer::class)
sealed interface JsonSchema {
object Serializer : KSerializer<JsonSchema> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JsonSchema", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): JsonSchema {
error("Abandon all hope ye who enter 💀")
}
override fun serialize(encoder: Encoder, value: JsonSchema) {
when (value) {
is ReferenceDefinition -> ReferenceDefinition.serializer().serialize(encoder, value)
is TypeDefinition -> TypeDefinition.serializer().serialize(encoder, value)
is EnumDefinition -> EnumDefinition.serializer().serialize(encoder, value)
is ArrayDefinition -> ArrayDefinition.serializer().serialize(encoder, value)
is MapDefinition -> MapDefinition.serializer().serialize(encoder, value)
is NullableDefinition -> NullableDefinition.serializer().serialize(encoder, value)
is OneOfDefinition -> OneOfDefinition.serializer().serialize(encoder, value)
is AnyOfDefinition -> AnyOfDefinition.serializer().serialize(encoder, value)
}
}
}
}

View File

@ -0,0 +1,10 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class MapDefinition(
val additionalProperties: JsonSchema
) : JsonSchema {
val type: String = "object"
}

View File

@ -0,0 +1,6 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class NullableDefinition(val type: String = "null") : JsonSchema

View File

@ -0,0 +1,8 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class OneOfDefinition(val oneOf: Set<JsonSchema>) : JsonSchema {
constructor(vararg types: JsonSchema) : this(types.toSet())
}

View File

@ -0,0 +1,6 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable
@Serializable
data class ReferenceDefinition(val `$ref`: String) : JsonSchema

View File

@ -0,0 +1,47 @@
package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class TypeDefinition(
val type: String,
val format: String? = null,
val description: String? = null,
val properties: Map<String, JsonSchema>? = null,
val required: Set<String>? = null,
@Contextual val default: Any? = null,
) : JsonSchema {
fun withDefault(default: Any): TypeDefinition = this.copy(default = default)
companion object {
val INT = TypeDefinition(
type = "number",
format = "int32"
)
val LONG = TypeDefinition(
type = "number",
format = "int64"
)
val DOUBLE = TypeDefinition(
type = "number",
format = "double"
)
val FLOAT = TypeDefinition(
type = "number",
format = "float"
)
val STRING = TypeDefinition(
type = "string"
)
val BOOLEAN = TypeDefinition(
type = "boolean"
)
}
}

View File

@ -0,0 +1,33 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import kotlin.reflect.KType
object CollectionHandler {
fun handle(type: KType, cache: MutableMap<String, JsonSchema>): JsonSchema {
val collectionType = type.arguments.first().type!!
val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache).let {
if (it is TypeDefinition && it.type == "object") {
cache[collectionType.getSimpleSlug()] = it
ReferenceDefinition(collectionType.getReferenceSlug())
} else {
it
}
}
val definition = ArrayDefinition(typeSchema)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)
false -> definition
}
}
}

View File

@ -0,0 +1,21 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import kotlin.reflect.KClass
import kotlin.reflect.KType
object EnumHandler {
fun handle(type: KType, clazz: KClass<*>): JsonSchema {
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
val definition = EnumDefinition(enum = options)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)
false -> definition
}
}
}

View File

@ -0,0 +1,37 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.MapDefinition
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import kotlin.reflect.KClass
import kotlin.reflect.KType
object MapHandler {
fun handle(type: KType, cache: MutableMap<String, JsonSchema>): JsonSchema {
require(type.arguments.first().type!!.classifier as KClass<*> == String::class) {
"JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}"
}
val valueType = type.arguments[1].type!!
val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache).let {
if (it is TypeDefinition && it.type == "object") {
cache[valueType.getSimpleSlug()] = it
ReferenceDefinition(valueType.getReferenceSlug())
} else {
it
}
}
val definition = MapDefinition(valueSchema)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)
false -> definition
}
}
}

View File

@ -0,0 +1,32 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
object SealedObjectHandler {
fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
val subclasses = clazz.sealedSubclasses
.map { it.createType(type.arguments) }
.map { t ->
SchemaGenerator.fromTypeToSchema(t, cache).let { js ->
if (js is TypeDefinition && js.type == "object") {
cache[t.getSimpleSlug()] = js
ReferenceDefinition(t.getReferenceSlug())
} else {
js
}
}
}
.toSet()
return AnyOfDefinition(subclasses)
}
}

View File

@ -0,0 +1,76 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.KTypeParameter
import kotlin.reflect.KTypeProjection
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
object SimpleObjectHandler {
fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
// cache[type.getSimpleSlug()] = ReferenceDefinition("RECURSION_PLACEHOLDER")
val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
val props = clazz.memberProperties.associate { prop ->
val schema = when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(prop, typeMap, cache)
false -> handleProperty(prop, cache)
}
prop.name to schema
}
val required = clazz.memberProperties.filterNot { prop -> prop.returnType.isMarkedNullable }
.filterNot { prop -> clazz.primaryConstructor!!.parameters.find { it.name == prop.name }!!.isOptional }
.map { it.name }
.toSet()
val definition = TypeDefinition(
type = "object",
properties = props,
required = required
)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)
false -> definition
}
}
private fun handleGenericProperty(
prop: KProperty<*>,
typeMap: Map<KTypeParameter, KTypeProjection>,
cache: MutableMap<String, JsonSchema>
): JsonSchema {
val type = typeMap[prop.returnType.classifier]?.type!!
return SchemaGenerator.fromTypeToSchema(type, cache).let {
if (it is TypeDefinition && it.type == "object") {
cache[type.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug())
} else {
it
}
}
}
private fun handleProperty(prop: KProperty<*>, cache: MutableMap<String, JsonSchema>): JsonSchema =
SchemaGenerator.fromTypeToSchema(prop.returnType, cache).let {
if (it is TypeDefinition && it.type == "object") {
cache[prop.returnType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug())
} else {
it
}
}
}

View File

@ -0,0 +1,29 @@
package io.bkbn.kompendium.json.schema.util
import kotlin.reflect.KClass
import kotlin.reflect.KType
object Helpers {
private const val COMPONENT_SLUG = "#/components/schemas"
fun KType.getSimpleSlug(): String = when {
this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>)
else -> (classifier as KClass<*>).simpleName ?: error("Could not determine simple name for $this")
}
fun KType.getReferenceSlug(): String = when {
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).simpleName}"
}
/**
* Adapts a class with type parameters into a reference friendly string
*/
private fun genericNameAdapter(type: KType, clazz: KClass<*>): String {
val classNames = type.arguments
.map { it.type?.classifier as KClass<*> }
.map { it.simpleName }
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-")
}
}

View File

@ -0,0 +1,9 @@
package io.bkbn.kompendium.json.schema.util
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import kotlin.reflect.KType
data class ReferenceCache(
val referenceRootPath: String = "#/",
val cache: MutableMap<KType, JsonSchema> = mutableMapOf()
)

View File

@ -0,0 +1,98 @@
package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.SimpleEnum
import io.bkbn.kompendium.core.fixtures.SlammaJamma
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.kotest.assertions.json.shouldEqualJson
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import kotlinx.serialization.json.Json
class SchemaGeneratorTest : DescribeSpec({
describe("Scalars") {
it("Can generate the schema for an Int") {
jsonSchemaTest<Int>("T0001__scalar_int.json")
}
it("Can generate the schema for a Boolean") {
jsonSchemaTest<Boolean>("T0002__scalar_bool.json")
}
it("Can generate the schema for a String") {
jsonSchemaTest<String>("T0003__scalar_string.json")
}
}
describe("Objects") {
it("Can generate the schema for a simple object") {
jsonSchemaTest<TestSimpleRequest>("T0004__simple_object.json")
}
it("Can generate the schema for a complex object") {
jsonSchemaTest<ComplexRequest>("T0005__complex_object.json")
}
it("Can generate the schema for a nullable object") {
jsonSchemaTest<TestSimpleRequest?>("T0006__nullable_object.json")
}
it("Can generate the schema for a polymorphic object") {
jsonSchemaTest<FlibbityGibbit>("T0015__polymorphic_object.json")
}
xit("Can generate the schema for a recursive type") {
// TODO jsonSchemaTest<SlammaJamma>("T0016__recursive_object.json")
}
}
describe("Enums") {
it("Can generate the schema for a simple enum") {
jsonSchemaTest<SimpleEnum>("T0007__simple_enum.json")
}
it("Can generate the schema for a nullable enum") {
jsonSchemaTest<SimpleEnum?>("T0008__nullable_enum.json")
}
}
describe("Arrays") {
it("Can generate the schema for an array of scalars") {
jsonSchemaTest<List<Int>>("T0009__scalar_array.json")
}
it("Can generate the schema for an array of objects") {
jsonSchemaTest<List<TestResponse>>("T0010__object_array.json")
}
it("Can generate the schema for a nullable array") {
jsonSchemaTest<List<Int>?>("T0011__nullable_array.json")
}
}
describe("Maps") {
it("Can generate the schema for a map of scalars") {
jsonSchemaTest<Map<String, Int>>("T0012__scalar_map.json")
}
it("Throws an error when map keys are not strings") {
shouldThrow<IllegalArgumentException> { SchemaGenerator.fromTypeToSchema<Map<Int, Int>>() }
}
it("Can generate the schema for a map of objects") {
jsonSchemaTest<Map<String, TestResponse>>("T0013__object_map.json")
}
it("Can generate the schema for a nullable map") {
jsonSchemaTest<Map<String, Int>?>("T0014__nullable_map.json")
}
}
}) {
companion object {
private val json = Json {
encodeDefaults = true
explicitNulls = false
prettyPrint = true
}
private fun JsonSchema.serialize() = json.encodeToString(JsonSchema.serializer(), this)
private inline fun <reified T> jsonSchemaTest(snapshotName: String) {
// act
val schema = SchemaGenerator.fromTypeToSchema<T>()
// todo add cache assertions!!!
// assert
schema.serialize() shouldEqualJson getFileSnapshot(snapshotName)
}
}
}

View File

@ -0,0 +1,4 @@
{
"type": "number",
"format": "int32"
}

View File

@ -0,0 +1,3 @@
{
"type": "boolean"
}

View File

@ -0,0 +1,3 @@
{
"type": "string"
}

View File

@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "number",
"format": "int32"
}
},
"required": [
"a",
"b"
]
}

View File

@ -0,0 +1,22 @@
{
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem"
},
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
}

View File

@ -0,0 +1,23 @@
{
"oneOf": [
{
"type": "null"
},
{
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "number",
"format": "int32"
}
},
"required": [
"a",
"b"
]
}
]
}

View File

@ -0,0 +1,3 @@
{
"enum": [ "ONE", "TWO" ]
}

View File

@ -0,0 +1,13 @@
{
"oneOf": [
{
"type": "null"
},
{
"enum": [
"ONE",
"TWO"
]
}
]
}

View File

@ -0,0 +1,7 @@
{
"items": {
"type": "number",
"format": "int32"
},
"type": "array"
}

View File

@ -0,0 +1,6 @@
{
"items": {
"$ref": "#/components/schemas/TestResponse"
},
"type": "array"
}

View File

@ -0,0 +1,14 @@
{
"oneOf": [
{
"type": "null"
},
{
"items": {
"type": "number",
"format": "int32"
},
"type": "array"
}
]
}

View File

@ -0,0 +1,7 @@
{
"additionalProperties": {
"type": "number",
"format": "int32"
},
"type": "object"
}

View File

@ -0,0 +1,6 @@
{
"additionalProperties": {
"$ref": "#/components/schemas/TestResponse"
},
"type": "object"
}

View File

@ -0,0 +1,14 @@
{
"oneOf": [
{
"type": "null"
},
{
"additionalProperties": {
"type": "number",
"format": "int32"
},
"type": "object"
}
]
}

View File

@ -0,0 +1,10 @@
{
"anyOf": [
{
"$ref": "#/components/schemas/ComplexGibbit"
},
{
"$ref": "#/components/schemas/SimpleGibbit"
}
]
}

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