feat: Multi Serialization Support (#134)

This commit is contained in:
Ryan Brink
2022-01-03 21:05:30 -05:00
committed by GitHub
parent c6ed261fe4
commit da104d0a63
56 changed files with 475 additions and 326 deletions

View File

@ -6,6 +6,8 @@
- Support for including parameter examples via `MethodInfo` - Support for including parameter examples via `MethodInfo`
### Changed ### Changed
- Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization
- Fixed bug where overridden field names were not reflected in serialized object and required array
### Remove ### Remove

View File

@ -4,18 +4,18 @@ plugins {
} }
dependencies { dependencies {
// VERSIONS
val ktorVersion: String by project
val kotestVersion: String by project
// IMPLEMENTATION // IMPLEMENTATION
api(projects.kompendiumOas) api(projects.kompendiumOas)
api(projects.kompendiumAnnotations) api(projects.kompendiumAnnotations)
val ktorVersion: String by project
val kotestVersion: String by project
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion)
implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = "2.13.0")
// TEST FIXTURES // TEST FIXTURES
testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion) testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion)

View File

@ -1,8 +1,5 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.metadata.SchemaMap import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.schema.TypedSchema import io.bkbn.kompendium.oas.schema.TypedSchema
@ -12,7 +9,7 @@ import io.ktor.application.ApplicationFeature
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.request.path import io.ktor.request.path
import io.ktor.response.respondText import io.ktor.response.respond
import io.ktor.util.AttributeKey import io.ktor.util.AttributeKey
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -28,13 +25,6 @@ class Kompendium(val config: Configuration) {
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) { fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema) cache = cache.plus(clazz.simpleName!! to schema)
} }
// TODO Add tests for this!!
var om: ObjectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)
fun specToJson(): String = om.writeValueAsString(spec)
} }
companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> { companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> {
@ -44,8 +34,7 @@ class Kompendium(val config: Configuration) {
pipeline.intercept(ApplicationCallPipeline.Call) { pipeline.intercept(ApplicationCallPipeline.Call) {
if (call.request.path() == configuration.specRoute) { if (call.request.path() == configuration.specRoute) {
call.respondText { configuration.specToJson() } call.respond(HttpStatusCode.OK, configuration.spec)
call.response.status(HttpStatusCode.OK)
} }
} }

View File

@ -199,8 +199,13 @@ object Kontent {
logger.debug("$slug contains $fieldMap") logger.debug("$slug contains $fieldMap")
var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap)) var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList() val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
// todo de-dup this logic
if (requiredParams.isNotEmpty()) { if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! }) schema = schema.copy(required = requiredParams.map { param ->
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
?.let { field -> field.name.ifBlank { param.name!! } }
?: param.name!!
})
} }
logger.debug("$slug schema: $schema") logger.debug("$slug schema: $schema")
newCache.plus(slug to schema) newCache.plus(slug to schema)
@ -335,8 +340,13 @@ object Kontent {
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList() val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
var schema = this var schema = this
// todo dedup this
if (requiredParams.isNotEmpty()) { if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! }) schema = schema.copy(required = requiredParams.map { param ->
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
?.let { field -> field.name.ifBlank { param.name!! } }
?: param.name!!
})
} }
if (prop.returnType.isMarkedNullable) { if (prop.returnType.isMarkedNullable) {

View File

@ -72,12 +72,12 @@ object MethodParser {
responseType: KType, responseType: KType,
responseInfo: ResponseInfo<*>?, responseInfo: ResponseInfo<*>?,
feature: Kompendium feature: Kompendium
): Map<Int, Response<*>> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty() ): Map<Int, Response> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
private fun parseExceptions( private fun parseExceptions(
exceptionInfo: Set<ExceptionInfo<*>>, exceptionInfo: Set<ExceptionInfo<*>>,
feature: Kompendium, feature: Kompendium,
): Map<Int, Response<*>> = exceptionInfo.associate { info -> ): Map<Int, Response> = exceptionInfo.associate { info ->
feature.config.cache = generateKontent(info.responseType, feature.config.cache) feature.config.cache = generateKontent(info.responseType, feature.config.cache)
val response = Response( val response = Response(
description = info.description, description = info.description,
@ -92,13 +92,14 @@ object MethodParser {
* @param requestInfo request metadata * @param requestInfo request metadata
* @return Will return a generated [Request] if requestInfo is not null * @return Will return a generated [Request] if requestInfo is not null
*/ */
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request<*>? = private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request? =
when (requestInfo) { when (requestInfo) {
null -> null null -> null
else -> { else -> {
Request( Request(
description = requestInfo.description, description = requestInfo.description,
content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf(), content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples as Map<String, Any>)
?: mapOf(),
required = requestInfo.required required = requestInfo.required
) )
} }
@ -110,13 +111,13 @@ object MethodParser {
* @param responseInfo response metadata * @param responseInfo response metadata
* @return Will return a generated [Pair] if responseInfo is not null * @return Will return a generated [Pair] if responseInfo is not null
*/ */
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response<*>>? = private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response>? =
when (responseInfo) { when (responseInfo) {
null -> null null -> null
else -> { else -> {
val specResponse = Response( val specResponse = Response(
description = responseInfo.description, description = responseInfo.description,
content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples) content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples as Map<String, Any>)
) )
Pair(responseInfo.status.value, specResponse) Pair(responseInfo.status.value, specResponse)
} }
@ -129,11 +130,11 @@ object MethodParser {
* @param examples Mapping of named examples of valid bodies. * @param examples Mapping of named examples of valid bodies.
* @return Named mapping of media types. * @return Named mapping of media types.
*/ */
private fun <F> Kompendium.resolveContent( private fun Kompendium.resolveContent(
type: KType, type: KType,
mediaTypes: List<String>, mediaTypes: List<String>,
examples: Map<String, F> examples: Map<String, Any>
): Map<String, MediaType<F>>? { ): Map<String, MediaType>? {
val classifier = type.classifier as KClass<*> val classifier = type.classifier as KClass<*>
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) { return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith { mediaTypes.associateWith {

View File

@ -80,7 +80,7 @@
}, },
"required": [ "required": [
"org", "org",
"amazingField", "amazing_field",
"tables" "tables"
], ],
"type": "object" "type": "object"

View File

@ -62,7 +62,7 @@
} }
}, },
"required": [ "required": [
"fieldName", "field_name",
"b", "b",
"aaa" "aaa"
], ],

View File

@ -45,7 +45,7 @@
} }
}, },
"required": [ "required": [
"b" "real_name"
], ],
"type": "object" "type": "object"
} }

View File

@ -82,7 +82,7 @@
} }
}, },
"required": [ "required": [
"fieldName", "field_name",
"b", "b",
"aaa" "aaa"
], ],

View File

@ -82,7 +82,7 @@
} }
}, },
"required": [ "required": [
"fieldName", "field_name",
"b", "b",
"aaa" "aaa"
], ],

View File

@ -1,3 +1,8 @@
plugins { plugins {
id("io.bkbn.sourdough.library") id("io.bkbn.sourdough.library")
kotlin("plugin.serialization") version "1.6.0"
}
dependencies {
implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1")
} }

View File

@ -6,7 +6,9 @@ import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.info.Info import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.path.Path import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class OpenApiSpec( data class OpenApiSpec(
val openapi: String = "3.0.3", val openapi: String = "3.0.3",
val info: Info, val info: Info,

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.common package io.bkbn.kompendium.oas.common
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class ExternalDocumentation( data class ExternalDocumentation(
@Serializable(with = UriSerializer::class)
val url: URI, val url: URI,
val description: String? val description: String?
) )

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.common package io.bkbn.kompendium.oas.common
import kotlinx.serialization.Serializable
@Serializable
data class Tag( data class Tag(
val name: String, val name: String,
val description: String? = null, val description: String? = null,

View File

@ -1,7 +1,9 @@
package io.bkbn.kompendium.oas.component package io.bkbn.kompendium.oas.component
import io.bkbn.kompendium.oas.security.SecuritySchema import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.Serializable
@Serializable
data class Components( data class Components(
val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf() val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf()
) )

View File

@ -1,9 +1,13 @@
package io.bkbn.kompendium.oas.info package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class Contact( data class Contact(
var name: String, var name: String,
@Serializable(with = UriSerializer::class)
var url: URI? = null, var url: URI? = null,
var email: String? = null // TODO Enforce email? var email: String? = null // TODO Enforce email?
) )

View File

@ -1,11 +1,15 @@
package io.bkbn.kompendium.oas.info package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class Info( data class Info(
var title: String? = null, var title: String? = null,
var version: String? = null, var version: String? = null,
var description: String? = null, var description: String? = null,
@Serializable(with = UriSerializer::class)
var termsOfService: URI? = null, var termsOfService: URI? = null,
var contact: Contact? = null, var contact: Contact? = null,
var license: License? = null var license: License? = null

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.info package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class License( data class License(
var name: String, var name: String,
@Serializable(with = UriSerializer::class)
var url: URI? = null var url: URI? = null
) )

View File

@ -2,7 +2,9 @@ package io.bkbn.kompendium.oas.path
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class Path( data class Path(
var get: PathOperation? = null, var get: PathOperation? = null,
var put: PathOperation? = null, var put: PathOperation? = null,

View File

@ -6,7 +6,9 @@ import io.bkbn.kompendium.oas.payload.Payload
import io.bkbn.kompendium.oas.payload.Request import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class PathOperation( data class PathOperation(
var tags: Set<String> = emptySet(), var tags: Set<String> = emptySet(),
var summary: String? = null, var summary: String? = null,
@ -14,9 +16,9 @@ data class PathOperation(
var externalDocs: ExternalDocumentation? = null, var externalDocs: ExternalDocumentation? = null,
var operationId: String? = null, var operationId: String? = null,
var parameters: List<Parameter>? = null, var parameters: List<Parameter>? = null,
var requestBody: Request<*>? = null, var requestBody: Request? = null,
// TODO How to enforce `default` requirement 🧐 // TODO How to enforce `default` requirement 🧐
var responses: Map<Int, Response<*>>? = null, var responses: Map<Int, Response>? = null,
var callbacks: Map<String, Payload>? = null, // todo what is this? var callbacks: Map<String, Payload>? = null, // todo what is this?
var deprecated: Boolean = false, var deprecated: Boolean = false,
var security: List<Map<String, List<String>>>? = null, var security: List<Map<String, List<String>>>? = null,

View File

@ -1,10 +1,14 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.oas.schema.ComponentSchema import io.bkbn.kompendium.oas.schema.ComponentSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
data class MediaType<T>( @Serializable
data class MediaType(
val schema: ComponentSchema, val schema: ComponentSchema,
val examples: Map<String, Example<T>>? = null val examples: Map<String, Example>? = null
) { ) {
data class Example<T>(val value: T) @Serializable
data class Example(val value: @Contextual Any)
} }

View File

@ -1,7 +1,10 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.oas.schema.ComponentSchema import io.bkbn.kompendium.oas.schema.ComponentSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class Parameter( data class Parameter(
val name: String, val name: String,
val `in`: String, // TODO Enum? "query", "header", "path" or "cookie" val `in`: String, // TODO Enum? "query", "header", "path" or "cookie"
@ -14,5 +17,6 @@ data class Parameter(
val explode: Boolean? = null, val explode: Boolean? = null,
val examples: Map<String, Example>? = null val examples: Map<String, Example>? = null
) { ) {
data class Example(val value: Any) @Serializable
data class Example(val value: @Contextual Any)
} }

View File

@ -1,7 +1,10 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
data class Request<T>( import kotlinx.serialization.Serializable
@Serializable
data class Request(
val description: String?, val description: String?,
val content: Map<String, MediaType<T>>, val content: Map<String, MediaType>,
val required: Boolean = false val required: Boolean = false
) : Payload ) : Payload

View File

@ -1,8 +1,11 @@
package io.bkbn.kompendium.oas.payload package io.bkbn.kompendium.oas.payload
data class Response<T>( import kotlinx.serialization.Serializable
@Serializable
data class Response(
val description: String? = null, val description: String? = null,
val headers: Map<String, Payload>? = null, val headers: Map<String, Payload>? = null,
val content: Map<String, MediaType<T>>? = null, val content: Map<String, MediaType>? = null,
val links: Map<String, Payload>? = null val links: Map<String, Payload>? = null
) : Payload ) : Payload

View File

@ -1,3 +1,6 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Serializable
@Serializable
data class AnyOfSchema(val anyOf: List<ComponentSchema>, override val description: String? = null) : ComponentSchema data class AnyOfSchema(val anyOf: List<ComponentSchema>, override val description: String? = null) : ComponentSchema

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ArraySchema( data class ArraySchema(
val items: ComponentSchema, val items: ComponentSchema,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints

View File

@ -1,5 +1,10 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonClassDiscriminator
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("component_type") // todo figure out a way to filter this
sealed interface ComponentSchema { sealed interface ComponentSchema {
val description: String? val description: String?
get() = null get() = null

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class DictionarySchema( data class DictionarySchema(
val additionalProperties: ComponentSchema, val additionalProperties: ComponentSchema,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null override val nullable: Boolean? = null
) : TypedSchema { ) : TypedSchema {

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class EnumSchema( data class EnumSchema(
val `enum`: Set<String>, val `enum`: Set<String>,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null override val nullable: Boolean? = null
) : TypedSchema { ) : TypedSchema {

View File

@ -1,15 +1,23 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import io.bkbn.kompendium.oas.serialization.NumberSerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class FormattedSchema( data class FormattedSchema(
val format: String, val format: String,
override val type: String, override val type: String,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// Constraints // Constraints
@Serializable(with = NumberSerializer::class)
val minimum: Number? = null, val minimum: Number? = null,
@Serializable(with = NumberSerializer::class)
val maximum: Number? = null, val maximum: Number? = null,
val exclusiveMinimum: Boolean? = null, val exclusiveMinimum: Boolean? = null,
val exclusiveMaximum: Boolean? = null, val exclusiveMaximum: Boolean? = null,
@Serializable(with = NumberSerializer::class)
val multipleOf: Number? = null, val multipleOf: Number? = null,
) : TypedSchema ) : TypedSchema

View File

@ -1,5 +1,9 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class FreeFormSchema( data class FreeFormSchema(
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints
@ -8,5 +12,5 @@ data class FreeFormSchema(
) : TypedSchema { ) : TypedSchema {
val additionalProperties: Boolean = true val additionalProperties: Boolean = true
override val type: String = "object" override val type: String = "object"
override val default: Any? = null override val default: @Contextual Any? = null
} }

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ObjectSchema( data class ObjectSchema(
val properties: Map<String, ComponentSchema>, val properties: Map<String, ComponentSchema>,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// constraints // constraints

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class SimpleSchema( data class SimpleSchema(
override val type: String, override val type: String,
override val default: Any? = null, override val default: @Contextual Any? = null,
override val description: String? = null, override val description: String? = null,
override val nullable: Boolean? = null, override val nullable: Boolean? = null,
// Constraints // Constraints

View File

@ -1,11 +1,13 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
import java.util.Locale import java.util.Locale
// TODO... is there even an official ktor api auth mechanism?? // TODO... is there even an official ktor api auth mechanism??
@Serializable
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
class ApiKeyAuth(val `in`: ApiKeyLocation, name: String) : SecuritySchema { class ApiKeyAuth(val `in`: ApiKeyLocation, val name: String) : SecuritySchema {
val type: String = "apiKey" val type: String = "apiKey"
enum class ApiKeyLocation { enum class ApiKeyLocation {

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
class BasicAuth : SecuritySchema { class BasicAuth : SecuritySchema {
val type: String = "http" val type: String = "http"
val scheme: String = "basic" val scheme: String = "basic"

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
data class BearerAuth(val bearerFormat: String? = null): SecuritySchema { data class BearerAuth(val bearerFormat: String? = null): SecuritySchema {
val type: String = "http" val type: String = "http"
val scheme: String = "bearer" val scheme: String = "bearer"

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(val description: String? = null, val flows: Flows) : SecuritySchema { data class OAuth(val description: String? = null, val flows: Flows) : SecuritySchema {
val type: String = "oauth2" val type: String = "oauth2"
@Serializable
data class Flows( data class Flows(
val implicit: Implicit? = null, val implicit: Implicit? = null,
val authorizationCode: AuthorizationCode? = null, val authorizationCode: AuthorizationCode? = null,
@ -21,12 +25,14 @@ data class OAuth(val description: String? = null, val flows: Flows) : SecuritySc
get() = emptyMap() get() = emptyMap()
} }
@Serializable
data class Implicit( data class Implicit(
override val authorizationUrl: String, override val authorizationUrl: String,
override val refreshUrl: String? = null, override val refreshUrl: String? = null,
override val scopes: Map<String, String> = emptyMap() override val scopes: Map<String, String> = emptyMap()
) : Flow ) : Flow
@Serializable
data class AuthorizationCode( data class AuthorizationCode(
override val authorizationUrl: String, override val authorizationUrl: String,
override val tokenUrl: String? = null, override val tokenUrl: String? = null,
@ -34,12 +40,14 @@ data class OAuth(val description: String? = null, val flows: Flows) : SecuritySc
override val scopes: Map<String, String> = emptyMap() override val scopes: Map<String, String> = emptyMap()
) : Flow ) : Flow
@Serializable
data class Password( data class Password(
override val tokenUrl: String? = null, override val tokenUrl: String? = null,
override val refreshUrl: String? = null, override val refreshUrl: String? = null,
override val scopes: Map<String, String> = emptyMap() override val scopes: Map<String, String> = emptyMap()
) : Flow ) : Flow
@Serializable
data class ClientCredential( data class ClientCredential(
override val tokenUrl: String? = null, override val tokenUrl: String? = null,
override val refreshUrl: String? = null, override val refreshUrl: String? = null,

View File

@ -1,3 +1,8 @@
package io.bkbn.kompendium.oas.security package io.bkbn.kompendium.oas.security
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonClassDiscriminator
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("schema_type") // todo figure out a way to filter this
sealed interface SecuritySchema sealed interface SecuritySchema

View File

@ -0,0 +1,27 @@
package io.bkbn.kompendium.oas.serialization
import kotlin.reflect.KClass
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializer
class AnySerializer<T : Any> : KSerializer<T> {
override fun serialize(encoder: Encoder, value: T) {
serialize(encoder, value, value::class as KClass<T>)
}
override fun deserialize(decoder: Decoder): T {
error("Abandon all hope ye who enter 💀")
}
override val descriptor: SerialDescriptor
get() = TODO("Not yet implemented")
@OptIn(InternalSerializationApi::class)
fun serialize(encoder: Encoder, obj: T, clazz: KClass<T>) {
clazz.serializer().serialize(encoder, obj)
}
}

View File

@ -0,0 +1,42 @@
package io.bkbn.kompendium.oas.serialization
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ArraySchema
import io.bkbn.kompendium.oas.schema.ComponentSchema
import io.bkbn.kompendium.oas.schema.DictionarySchema
import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema
import io.bkbn.kompendium.oas.security.ApiKeyAuth
import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.security.OAuth
import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
object KompendiumSerializersModule {
val module = SerializersModule {
polymorphic(ComponentSchema::class) {
subclass(SimpleSchema::class, SimpleSchema.serializer())
subclass(FormattedSchema::class, FormattedSchema.serializer())
subclass(ObjectSchema::class, ObjectSchema.serializer())
subclass(AnyOfSchema::class, AnyOfSchema.serializer())
subclass(ArraySchema::class, ArraySchema.serializer())
subclass(DictionarySchema::class, DictionarySchema.serializer())
subclass(EnumSchema::class, EnumSchema.serializer())
subclass(FreeFormSchema::class, FreeFormSchema.serializer())
}
polymorphic(SecuritySchema::class) {
subclass(ApiKeyAuth::class, ApiKeyAuth.serializer())
subclass(BasicAuth::class, BasicAuth.serializer())
subclass(BearerAuth::class, BearerAuth.serializer())
subclass(OAuth::class, OAuth.serializer())
}
contextual(Any::class, AnySerializer())
}
}

View File

@ -0,0 +1,24 @@
package io.bkbn.kompendium.oas.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
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
object NumberSerializer : KSerializer<Number> {
override fun deserialize(decoder: Decoder): Number = try {
decoder.decodeDouble()
} catch (_: SerializationException) {
decoder.decodeInt()
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Number", PrimitiveKind.DOUBLE)
override fun serialize(encoder: Encoder, value: Number) {
encoder.encodeString(value.toString())
}
}

View File

@ -0,0 +1,20 @@
package io.bkbn.kompendium.oas.serialization
import kotlinx.serialization.KSerializer
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
import java.net.URI
object UriSerializer : KSerializer<URI> {
override fun deserialize(decoder: Decoder): URI = URI.create(decoder.decodeString())
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: URI) {
encoder.encodeString(value.toString())
}
}

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.server package io.bkbn.kompendium.oas.server
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
@Serializable
data class Server( data class Server(
@Serializable(with = UriSerializer::class)
val url: URI, val url: URI,
val description: String? = null, val description: String? = null,
var variables: Map<String, ServerVariable>? = null var variables: Map<String, ServerVariable>? = null

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.server package io.bkbn.kompendium.oas.server
import kotlinx.serialization.Serializable
@Serializable
data class ServerVariable( data class ServerVariable(
val `enum`: Set<String>, // todo enforce not empty val `enum`: Set<String>, // todo enforce not empty
val default: String, val default: String,

View File

@ -18,10 +18,23 @@ dependencies {
implementation(group = "io.ktor", name = "ktor-auth", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-auth", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-auth-jwt", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-auth-jwt", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-serialization", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-serialization", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-jackson", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-gson", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-locations", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-locations", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion) implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion)
// Logging
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.1.0")
implementation("org.apache.logging.log4j:log4j-api:2.17.0")
implementation("org.apache.logging.log4j:log4j-core:2.17.0")
implementation("org.slf4j:slf4j-api:1.7.32")
implementation("org.slf4j:slf4j-simple:1.7.32")
implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1") implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1")
implementation(group = "joda-time", name = "joda-time", version = "2.10.13") implementation(group = "joda-time", name = "joda-time", version = "2.10.13")
} }
repositories {
mavenCentral()
}

View File

@ -7,12 +7,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
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.server.Server
import io.bkbn.kompendium.playground.AuthPlaygroundToC.simpleAuthenticatedGet import io.bkbn.kompendium.playground.AuthPlaygroundToC.simpleAuthenticatedGet
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -27,7 +23,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -44,10 +39,10 @@ fun main() {
// Application Module // Application Module
private fun Application.mainModule() { private fun Application.mainModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
install(Kompendium) { install(Kompendium) {
spec = AuthMetadata.spec spec = Util.baseSpec
} }
install(Authentication) { install(Authentication) {
// We can leverage the security config name to prevent typos // We can leverage the security config name to prevent typos
@ -73,36 +68,6 @@ private fun Application.mainModule() {
} }
} }
object AuthMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple API with documented Authentication",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
// This is where we define the available security configurations for our app // This is where we define the available security configurations for our app
object SecurityConfigurations { object SecurityConfigurations {
val basic = object : BasicAuthConfiguration { val basic = object : BasicAuthConfiguration {

View File

@ -1,5 +1,7 @@
package io.bkbn.kompendium.playground package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.SerializedName
import io.bkbn.kompendium.annotations.Field import io.bkbn.kompendium.annotations.Field
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
@ -13,18 +15,15 @@ import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
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.server.Server
import io.bkbn.kompendium.playground.BasicModels.BasicParameters import io.bkbn.kompendium.playground.BasicModels.BasicParameters
import io.bkbn.kompendium.playground.BasicModels.BasicResponse
import io.bkbn.kompendium.playground.BasicModels.BasicRequest import io.bkbn.kompendium.playground.BasicModels.BasicRequest
import io.bkbn.kompendium.playground.BasicModels.BasicResponse
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleDeleteRequest import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleDeleteRequest
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExample import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExample
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExampleWithParameters import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExampleWithParameters
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simplePostRequest import io.bkbn.kompendium.playground.BasicPlaygroundToC.simplePostRequest
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -37,8 +36,9 @@ import io.ktor.routing.routing
import io.ktor.serialization.json import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
/** /**
@ -57,11 +57,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = BasicMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {
@ -161,38 +161,6 @@ object BasicPlaygroundToC {
) )
} }
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object BasicMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object BasicModels { object BasicModels {
@Serializable @Serializable
data class BasicResponse(val c: String) data class BasicResponse(val c: String)
@ -207,6 +175,9 @@ object BasicModels {
@Serializable @Serializable
data class BasicRequest( data class BasicRequest(
@JsonProperty("best_field")
@SerializedName("best_field")
@SerialName("best_field")
@Field(description = "This is a super important field!!", name = "best_field") @Field(description = "This is a super important field!!", name = "best_field")
val d: Boolean val d: Boolean
) )

View File

@ -23,16 +23,12 @@ import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
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.server.Server
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedParams import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedParams
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedRequest import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedRequest
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedResponse import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedResponse
import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedGet import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedGet
import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedPost import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedPost
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -45,7 +41,6 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import java.net.URI
fun main() { fun main() {
embeddedServer( embeddedServer(
@ -59,11 +54,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = ConstrainedMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {
@ -104,36 +99,6 @@ object ConstrainedPlaygroundToC {
) )
} }
object ConstrainedMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object ConstrainedModels { object ConstrainedModels {
@Serializable @Serializable
data class ConstrainedResponse( data class ConstrainedResponse(

View File

@ -6,12 +6,8 @@ import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
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.server.Server
import io.bkbn.kompendium.playground.ExceptionPlaygroundToC.simpleGetExample import io.bkbn.kompendium.playground.ExceptionPlaygroundToC.simpleGetExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -25,7 +21,6 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
import java.time.LocalDateTime import java.time.LocalDateTime
// Application Entrypoint // Application Entrypoint
@ -41,11 +36,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = ExceptionMetadata.spec spec = Util.baseSpec
} }
install(StatusPages) { install(StatusPages) {
exception<ExceptionModels.BadUserException> { exception<ExceptionModels.BadUserException> {
@ -70,36 +65,6 @@ private fun Application.mainModule() {
} }
} }
object ExceptionMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with notarized exceptions",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
// This is a table of contents to hold all the metadata for our various API endpoints // This is a table of contents to hold all the metadata for our various API endpoints
object ExceptionPlaygroundToC { object ExceptionPlaygroundToC {
private val simpleException = ExceptionInfo<ExceptionModels.ExceptionResponse>( private val simpleException = ExceptionInfo<ExceptionModels.ExceptionResponse>(

View File

@ -5,12 +5,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
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.server.Server
import io.bkbn.kompendium.playground.GenericPlaygroundToC.simpleGenericGet import io.bkbn.kompendium.playground.GenericPlaygroundToC.simpleGenericGet
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -22,7 +18,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -40,11 +35,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = GenericMetadata.spec spec = Util.baseSpec
} }
routing { routing {
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
@ -70,38 +65,6 @@ object GenericPlaygroundToC {
) )
} }
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object GenericMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with Generic Data",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object GenericModels { object GenericModels {
@Serializable @Serializable
data class Foosy<T, K>(val test: T, val otherThing: List<K>) data class Foosy<T, K>(val test: T, val otherThing: List<K>)

View File

@ -0,0 +1,49 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.gson.gson
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc()
route("/create") {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
when (request.d) {
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!"))
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc()
route("/create") {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
when (request.d) {
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!"))
}
}
}
}
}

View File

@ -7,14 +7,10 @@ import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet
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.server.Server
import io.bkbn.kompendium.playground.LocationsToC.ohBoiUCrazy import io.bkbn.kompendium.playground.LocationsToC.ohBoiUCrazy
import io.bkbn.kompendium.playground.LocationsToC.testLocation import io.bkbn.kompendium.playground.LocationsToC.testLocation
import io.bkbn.kompendium.playground.LocationsToC.testNestLocation import io.bkbn.kompendium.playground.LocationsToC.testNestLocation
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -28,7 +24,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -44,10 +39,10 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
install(Kompendium) { install(Kompendium) {
spec = LocationMetadata.spec spec = Util.baseSpec
} }
install(Locations) install(Locations)
routing { routing {
@ -118,38 +113,6 @@ data class TestLocations(
} }
} }
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object LocationMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo Leveraging Ktor Locations",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object LocationModels { object LocationModels {
@Serializable @Serializable
data class ExampleResponse(val c: String) data class ExampleResponse(val c: String)

View File

@ -5,12 +5,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
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.server.Server
import io.bkbn.kompendium.playground.PolymorphicPlaygroundToC.polymorphicExample import io.bkbn.kompendium.playground.PolymorphicPlaygroundToC.polymorphicExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -22,7 +18,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI
/** /**
* Application entrypoint. Run this and head on over to `localhost:8081/docs` * Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -39,11 +34,11 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = PolymorphicMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {
@ -70,36 +65,6 @@ object PolymorphicPlaygroundToC {
) )
} }
object PolymorphicMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with Polymorphic Models",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object PolymorphicModels { object PolymorphicModels {
sealed interface SlammaJamma sealed interface SlammaJamma

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedGet import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.playground.util.Util
import io.bkbn.kompendium.swagger.swaggerUI import io.bkbn.kompendium.swagger.swaggerUI
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
@ -32,12 +33,12 @@ fun main() {
private fun Application.mainModule() { private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation // Installs Simple JSON Content Negotiation
install(ContentNegotiation) { install(ContentNegotiation) {
json() json(json = Util.kotlinxConfig)
} }
install(Webjars) install(Webjars)
// Installs the Kompendium Plugin and sets up baseline server metadata // Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) { install(Kompendium) {
spec = BasicMetadata.spec spec = Util.baseSpec
} }
// Configures the routes for our API // Configures the routes for our API
routing { routing {

View File

@ -0,0 +1,48 @@
package io.bkbn.kompendium.playground.util
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 kotlinx.serialization.json.Json
import java.net.URI
object Util {
val kotlinxConfig = Json {
classDiscriminator = "class"
serializersModule = KompendiumSerializersModule.module
prettyPrint = true
explicitNulls = false
encodeDefaults = true
}
val baseSpec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}