Compare commits

...

7 Commits

Author SHA1 Message Date
7b39e448bf chore: prep for 3.8.0 release 2022-11-09 08:13:42 -05:00
3c57c8f5e4 feat: NotarizedResource plugin 2022-11-09 08:11:42 -05:00
01b6b59cf5 feat: support partial authentication (#372) (#375) 2022-11-08 19:59:35 +00:00
2c1c8fbcdc chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.7.21 (#377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ryan Brink <5607577+unredundant@users.noreply.github.com>
2022-11-08 14:56:33 -05:00
0dc2b9538f chore: delete autoupdate action
this thing sucks meant to delete it a while ago
2022-11-08 14:42:30 -05:00
1ca7abaf0d fix(deps): update kotlin monorepo to v1.7.21 (#374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 18:59:49 +00:00
a5376cfa82 feat: allow media type overrides (#369) 2022-11-05 16:45:06 -04:00
32 changed files with 1695 additions and 839 deletions

View File

@ -1,12 +0,0 @@
name: autoupdate
on:
push:
branches: [ main ]
jobs:
autoupdate:
name: autoupdate
runs-on: ubuntu-latest
steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'

View File

@ -12,6 +12,22 @@
## Released ## Released
## [3.8.0] - November 9th, 2022
### Added
- Add support for NotarizedResource plugin scoped to route
### Changed
- Support registering same path with different authentication and methods
## [3.7.0] - November 5th, 2022
### Added
- Allow users to override media type in request and response
## [3.6.0] - November 5th, 2022 ## [3.6.0] - November 5th, 2022
### Changed ### Changed

View File

@ -1,6 +1,6 @@
plugins { plugins {
kotlin("jvm") version "1.7.20" apply false kotlin("jvm") version "1.7.21" apply false
kotlin("plugin.serialization") version "1.7.20" apply false kotlin("plugin.serialization") version "1.7.21" apply false
id("io.bkbn.sourdough.library.jvm") version "0.12.0" apply false id("io.bkbn.sourdough.library.jvm") version "0.12.0" apply false
id("io.bkbn.sourdough.application.jvm") version "0.12.0" apply false id("io.bkbn.sourdough.application.jvm") version "0.12.0" apply false
id("io.bkbn.sourdough.root") version "0.12.0" id("io.bkbn.sourdough.root") version "0.12.0"

View File

@ -7,7 +7,8 @@ import kotlin.reflect.typeOf
class RequestInfo private constructor( class RequestInfo private constructor(
val requestType: KType, val requestType: KType,
val description: String, val description: String,
val examples: Map<String, MediaType.Example>? val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>
) { ) {
companion object { companion object {
@ -22,6 +23,7 @@ class RequestInfo private constructor(
private var requestType: KType? = null private var requestType: KType? = null
private var description: String? = null private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null
fun requestType(t: KType) = apply { fun requestType(t: KType) = apply {
this.requestType = t this.requestType = t
@ -35,10 +37,15 @@ class RequestInfo private constructor(
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
} }
fun mediaTypes(vararg m: String) = apply {
this.mediaTypes = m.toSet()
}
fun build() = RequestInfo( fun build() = RequestInfo(
requestType = requestType ?: error("Request type must be present"), requestType = requestType ?: error("Request type must be present"),
description = description ?: error("Description must be present"), description = description ?: error("Description must be present"),
examples = examples examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
) )
} }
} }

View File

@ -9,7 +9,8 @@ class ResponseInfo private constructor(
val responseCode: HttpStatusCode, val responseCode: HttpStatusCode,
val responseType: KType, val responseType: KType,
val description: String, val description: String,
val examples: Map<String, MediaType.Example>? val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>
) { ) {
companion object { companion object {
@ -25,6 +26,7 @@ class ResponseInfo private constructor(
private var responseType: KType? = null private var responseType: KType? = null
private var description: String? = null private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null
fun responseCode(code: HttpStatusCode) = apply { fun responseCode(code: HttpStatusCode) = apply {
this.responseCode = code this.responseCode = code
@ -42,11 +44,16 @@ class ResponseInfo private constructor(
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
} }
fun mediaTypes(vararg m: String) = apply {
this.mediaTypes = m.toSet()
}
fun build() = ResponseInfo( fun build() = ResponseInfo(
responseCode = responseCode ?: error("You must provide a response code in order to build a Response!"), 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!"), 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!"), description = description ?: error("You must provide a description in order to build a Response!"),
examples = examples examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
) )
} }
} }

View File

@ -10,16 +10,16 @@ import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.PutInfo import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.util.Helpers.addToSpec import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.SpecConfig import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.path.Path import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.server.application.ApplicationCallPipeline import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.Hook import io.ktor.server.application.Hook
import io.ktor.server.application.PluginBuilder
import io.ktor.server.application.createRouteScopedPlugin import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
object NotarizedRoute { object NotarizedRoute {
class Config : SpecConfig { class Config : SpecConfig {
override var tags: Set<String> = emptySet() override var tags: Set<String> = emptySet()
override var parameters: List<Parameter> = emptyList() override var parameters: List<Parameter> = emptyList()
@ -31,7 +31,6 @@ object NotarizedRoute {
override var head: HeadInfo? = null override var head: HeadInfo? = null
override var options: OptionsInfo? = null override var options: OptionsInfo? = null
override var security: Map<String, List<String>>? = null override var security: Map<String, List<String>>? = null
internal var path: Path? = null
} }
private object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> { private object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> {
@ -51,62 +50,37 @@ object NotarizedRoute {
val spec = application.attributes[KompendiumAttributes.openApiSpec] val spec = application.attributes[KompendiumAttributes.openApiSpec]
val routePath = route.calculateRoutePath() val routePath = route.calculateRoutePath()
val authMethods = route.collectAuthMethods() val authMethods = route.collectAuthMethods()
pluginConfig.path?.addDefaultAuthMethods(authMethods)
require(spec.paths[routePath] == null) { addToSpec(spec, routePath, authMethods)
"""
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
?: error("This indicates a bug in Kompendium. Please file a GitHub issue!")
} }
val spec = application.attributes[KompendiumAttributes.openApiSpec]
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
val path = Path()
path.parameters = pluginConfig.parameters
pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader)
pluginConfig.path = path
} }
private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "") fun <T : SpecConfig> PluginBuilder<T>.addToSpec(
private fun Route.collectAuthMethods() = toString() spec: OpenApiSpec,
fullPath: String,
authMethods: List<String>
) {
val path = spec.paths[fullPath] ?: Path()
path.parameters = path.parameters?.plus(pluginConfig.parameters) ?: pluginConfig.parameters
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
spec.paths[fullPath] = path
}
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
fun Route.collectAuthMethods() = toString()
.split("/") .split("/")
.filter { it.contains(Regex("\\(authenticate .*\\)")) } .filter { it.contains(Regex("\\(authenticate .*\\)")) }
.map { it.replace("(authenticate ", "").replace(")", "") } .map { it.replace("(authenticate ", "").replace(")", "") }
.map { it.split(", ") } .map { it.split(", ") }
.flatten() .flatten()
private fun Path.addDefaultAuthMethods(methods: List<String>) {
get?.addDefaultAuthMethods(methods)
put?.addDefaultAuthMethods(methods)
post?.addDefaultAuthMethods(methods)
delete?.addDefaultAuthMethods(methods)
options?.addDefaultAuthMethods(methods)
head?.addDefaultAuthMethods(methods)
patch?.addDefaultAuthMethods(methods)
trace?.addDefaultAuthMethods(methods)
}
private fun PathOperation.addDefaultAuthMethods(methods: List<String>) {
methods.forEach { m ->
if (security == null || security?.all { s -> !s.containsKey(m) } == true) {
if (security == null) {
security = mutableListOf(mapOf(m to emptyList()))
} else {
security?.add(mapOf(m to emptyList()))
}
}
}
}
} }

View File

@ -24,11 +24,31 @@ import io.bkbn.kompendium.oas.payload.MediaType
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 kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KType import kotlin.reflect.KType
object Helpers { object Helpers {
fun MethodInfo.addToSpec(path: Path, spec: OpenApiSpec, config: SpecConfig, schemaConfigurator: SchemaConfigurator) { private fun PathOperation.addDefaultAuthMethods(methods: List<String>) {
methods.forEach { m ->
if (security == null || security?.all { s -> !s.containsKey(m) } == true) {
if (security == null) {
security = mutableListOf(mapOf(m to emptyList()))
} else {
security?.add(mapOf(m to emptyList()))
}
}
}
}
fun MethodInfo.addToSpec(
path: Path,
spec: OpenApiSpec,
config: SpecConfig,
schemaConfigurator: SchemaConfigurator,
routePath: String,
authMethods: List<String> = emptyList()
) {
SchemaGenerator.fromTypeOrUnit( SchemaGenerator.fromTypeOrUnit(
this.response.responseType, this.response.responseType,
spec.components.schemas, schemaConfigurator spec.components.schemas, schemaConfigurator
@ -57,15 +77,25 @@ object Helpers {
} }
val operations = this.toPathOperation(config) val operations = this.toPathOperation(config)
operations.addDefaultAuthMethods(authMethods)
when (this) { fun setOperation(
is DeleteInfo -> path.delete = operations property: KMutableProperty1<Path, PathOperation?>
is GetInfo -> path.get = operations ) {
is HeadInfo -> path.head = operations require(property.get(path) == null) {
is PatchInfo -> path.patch = operations "A route has already been registered for path: $routePath and method: ${property.name.uppercase()}"
is PostInfo -> path.post = operations }
is PutInfo -> path.put = operations property.set(path, operations)
is OptionsInfo -> path.options = operations }
return when (this) {
is DeleteInfo -> setOperation(Path::delete)
is GetInfo -> setOperation(Path::get)
is HeadInfo -> setOperation(Path::head)
is PatchInfo -> setOperation(Path::patch)
is PostInfo -> setOperation(Path::post)
is PutInfo -> setOperation(Path::put)
is OptionsInfo -> setOperation(Path::options)
} }
} }
@ -84,7 +114,7 @@ object Helpers {
requestBody = when (this) { requestBody = when (this) {
is MethodInfoWithRequest -> Request( is MethodInfoWithRequest -> Request(
description = this.request.description, description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples), content = this.request.requestType.toReferenceContent(this.request.examples, this.request.mediaTypes),
required = true required = true
) )
@ -93,7 +123,7 @@ object Helpers {
responses = mapOf( responses = mapOf(
this.response.responseCode.value to Response( this.response.responseCode.value to Response(
description = this.response.description, description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples) content = this.response.responseType.toReferenceContent(this.response.examples, this.response.mediaTypes)
) )
).plus(this.errors.toResponseMap()) ).plus(this.errors.toResponseMap())
) )
@ -101,21 +131,24 @@ object Helpers {
private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error -> private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response( error.responseCode.value to Response(
description = error.description, description = error.description,
content = error.responseType.toReferenceContent(error.examples) content = error.responseType.toReferenceContent(error.examples, error.mediaTypes)
) )
} }
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? = private fun KType.toReferenceContent(
examples: Map<String, MediaType.Example>?,
mediaTypes: Set<String>
): Map<String, MediaType>? =
when (this.classifier as KClass<*>) { when (this.classifier as KClass<*>) {
Unit::class -> null Unit::class -> null
else -> mapOf( else -> mediaTypes.associateWith {
"application/json" to MediaType( MediaType(
schema = if (this.isMarkedNullable) OneOfDefinition( schema = if (this.isMarkedNullable) OneOfDefinition(
NullableDefinition(), NullableDefinition(),
ReferenceDefinition(this.getReferenceSlug()) ReferenceDefinition(this.getReferenceSlug())
) else ReferenceDefinition(this.getReferenceSlug()), ) else ReferenceDefinition(this.getReferenceSlug()),
examples = examples examples = examples
) )
) }
} }
} }

View File

@ -2,57 +2,60 @@ package io.bkbn.kompendium.core
import dev.forst.ktor.apikey.apiKey import dev.forst.ktor.apikey.apiKey
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
import io.bkbn.kompendium.core.util.TestModules.complexRequest import io.bkbn.kompendium.core.util.complexRequest
import io.bkbn.kompendium.core.util.TestModules.customAuthConfig import io.bkbn.kompendium.core.util.customAuthConfig
import io.bkbn.kompendium.core.util.TestModules.customFieldNameResponse import io.bkbn.kompendium.core.util.customFieldNameResponse
import io.bkbn.kompendium.core.util.TestModules.dateTimeString import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.core.util.TestModules.defaultAuthConfig import io.bkbn.kompendium.core.util.defaultAuthConfig
import io.bkbn.kompendium.core.util.TestModules.defaultField import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.TestModules.defaultParameter import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.TestModules.exampleParams import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.TestModules.genericException import io.bkbn.kompendium.core.util.genericException
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.TestModules.gnarlyGenericResponse import io.bkbn.kompendium.core.util.gnarlyGenericResponse
import io.bkbn.kompendium.core.util.TestModules.headerParameter import io.bkbn.kompendium.core.util.headerParameter
import io.bkbn.kompendium.core.util.TestModules.ignoredFieldsResponse import io.bkbn.kompendium.core.util.ignoredFieldsResponse
import io.bkbn.kompendium.core.util.TestModules.multipleAuthStrategies import io.bkbn.kompendium.core.util.multipleAuthStrategies
import io.bkbn.kompendium.core.util.TestModules.multipleExceptions import io.bkbn.kompendium.core.util.multipleExceptions
import io.bkbn.kompendium.core.util.TestModules.nestedGenericCollection import io.bkbn.kompendium.core.util.nestedGenericCollection
import io.bkbn.kompendium.core.util.TestModules.nestedGenericMultipleParamsCollection import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse import io.bkbn.kompendium.core.util.nestedGenericResponse
import io.bkbn.kompendium.core.util.TestModules.nestedTypeName import io.bkbn.kompendium.core.util.nestedTypeName
import io.bkbn.kompendium.core.util.TestModules.nestedUnderRoot import io.bkbn.kompendium.core.util.nonRequiredParam
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam import io.bkbn.kompendium.core.util.nonRequiredParams
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParams import io.bkbn.kompendium.core.util.notarizedDelete
import io.bkbn.kompendium.core.util.TestModules.notarizedDelete import io.bkbn.kompendium.core.util.notarizedGet
import io.bkbn.kompendium.core.util.TestModules.notarizedGet import io.bkbn.kompendium.core.util.notarizedHead
import io.bkbn.kompendium.core.util.TestModules.notarizedHead import io.bkbn.kompendium.core.util.notarizedOptions
import io.bkbn.kompendium.core.util.TestModules.notarizedOptions import io.bkbn.kompendium.core.util.notarizedPatch
import io.bkbn.kompendium.core.util.TestModules.notarizedPatch import io.bkbn.kompendium.core.util.notarizedPost
import io.bkbn.kompendium.core.util.TestModules.notarizedPost import io.bkbn.kompendium.core.util.notarizedPut
import io.bkbn.kompendium.core.util.TestModules.notarizedPut import io.bkbn.kompendium.core.util.nullableEnumField
import io.bkbn.kompendium.core.util.TestModules.nullableEnumField import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.TestModules.nullableField import io.bkbn.kompendium.core.util.nullableNestedObject
import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject import io.bkbn.kompendium.core.util.nullableReference
import io.bkbn.kompendium.core.util.TestModules.nullableReference import io.bkbn.kompendium.core.util.overrideMediaTypes
import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicException import io.bkbn.kompendium.core.util.polymorphicException
import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse import io.bkbn.kompendium.core.util.polymorphicMapResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicResponse import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.primitives import io.bkbn.kompendium.core.util.primitives
import io.bkbn.kompendium.core.util.TestModules.reqRespExamples import io.bkbn.kompendium.core.util.reqRespExamples
import io.bkbn.kompendium.core.util.TestModules.requiredParams import io.bkbn.kompendium.core.util.requiredParams
import io.bkbn.kompendium.core.util.TestModules.returnsList import io.bkbn.kompendium.core.util.returnsList
import io.bkbn.kompendium.core.util.TestModules.rootRoute import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth
import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse import io.bkbn.kompendium.core.util.samePathSameMethod
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.TestModules.simpleRecursive import io.bkbn.kompendium.core.util.simpleRecursive
import io.bkbn.kompendium.core.util.TestModules.singleException import io.bkbn.kompendium.core.util.singleException
import io.bkbn.kompendium.core.util.TestModules.topLevelNullable import io.bkbn.kompendium.core.util.topLevelNullable
import io.bkbn.kompendium.core.util.TestModules.trailingSlash import io.bkbn.kompendium.core.util.unbackedFieldsResponse
import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse import io.bkbn.kompendium.core.util.withOperationId
import io.bkbn.kompendium.core.util.TestModules.withOperationId import io.bkbn.kompendium.core.util.nestedUnderRoot
import io.bkbn.kompendium.core.util.rootRoute
import io.bkbn.kompendium.core.util.simplePathParsing
import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
import io.bkbn.kompendium.oas.component.Components import io.bkbn.kompendium.oas.component.Components
@ -74,8 +77,8 @@ import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.basic import io.ktor.server.auth.basic
import io.ktor.server.auth.jwt.jwt import io.ktor.server.auth.jwt.jwt
import io.ktor.server.auth.oauth import io.ktor.server.auth.oauth
import kotlin.reflect.typeOf
import java.time.Instant import java.time.Instant
import kotlin.reflect.typeOf
class KompendiumTest : DescribeSpec({ class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") { describe("Notarized Open API Metadata Tests") {
@ -112,6 +115,9 @@ class KompendiumTest : DescribeSpec({
it("Can notarize a route with non-required params") { it("Can notarize a route with non-required params") {
openApiTestAllSerializers("T0011__non_required_params.json") { nonRequiredParams() } openApiTestAllSerializers("T0011__non_required_params.json") { nonRequiredParams() }
} }
it("Can override media types") {
openApiTestAllSerializers("T0052__override_media_types.json") { overrideMediaTypes() }
}
} }
describe("Route Parsing") { describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") { it("Can parse a simple path and store it under the expected route") {
@ -247,12 +253,42 @@ class KompendiumTest : DescribeSpec({
it("Can handle top level nullable types") { it("Can handle top level nullable types") {
openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() } openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() }
} }
it("Can handle multiple registrations for different methods with the same path and different auth") {
openApiTestAllSerializers(
"T0053__same_path_different_methods_and_auth.json",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
)
)
}
) { samePathDifferentMethodsAndAuth() }
}
} }
describe("Error Handling") { describe("Error Handling") {
it("Throws a clear exception when an unidentified type is encountered") { it("Throws a clear exception when an unidentified type is encountered") {
val exception = shouldThrow<UnknownSchemaException> { openApiTestAllSerializers("") { dateTimeString() } } val exception = shouldThrow<UnknownSchemaException> { openApiTestAllSerializers("") { dateTimeString() } }
exception.message should startWith("An unknown type was encountered: class java.time.Instant") exception.message should startWith("An unknown type was encountered: class java.time.Instant")
} }
it("Throws an exception when same method for same path has been previously registered") {
val exception = shouldThrow<IllegalArgumentException> {
openApiTestAllSerializers("") {
samePathSameMethod()
}
}
exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET")
}
} }
describe("Constraints") { describe("Constraints") {
// TODO Assess strategies here // TODO Assess strategies here

View File

@ -0,0 +1,51 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.auth.authenticate
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
fun Routing.defaultAuthConfig() {
authenticate("basic") {
route(rootPath) {
basicGetGenerator<TestResponse>()
}
}
}
fun Routing.customAuthConfig() {
authenticate("auth-oauth-google") {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
security = mapOf(
"auth-oauth-google" to listOf("read:pets")
)
}
}
}
}
}
fun Routing.multipleAuthStrategies() {
authenticate("jwt", "api-key") {
route(rootPath) {
basicGetGenerator<TestResponse>()
}
}
}

View File

@ -0,0 +1,10 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.SerialNameObject
import io.bkbn.kompendium.core.fixtures.TransientObject
import io.bkbn.kompendium.core.fixtures.UnbackedObject
import io.ktor.server.routing.Routing
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbackedObject>()
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()

View File

@ -0,0 +1,16 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.server.routing.Routing
fun Routing.defaultParameter() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING.withDefault("IDK")
)
)
)

View File

@ -0,0 +1,16 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.util.TestModules.defaultPath
import io.ktor.server.auth.authenticate
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
fun Routing.samePathSameMethod() {
route(defaultPath) {
basicGetGenerator<TestResponse>()
authenticate {
basicGetGenerator<TestResponse>()
}
}
}

View File

@ -0,0 +1,57 @@
package io.bkbn.kompendium.core.util
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.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultRequestDescription
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
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.install
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
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")
)
)
)
)

View File

@ -0,0 +1,105 @@
package io.bkbn.kompendium.core.util
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.TestResponse
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
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>>()
}
}
}
}
}

View File

@ -0,0 +1,80 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ColumnSchema
import io.bkbn.kompendium.core.fixtures.DateTimeString
import io.bkbn.kompendium.core.fixtures.ManyThings
import io.bkbn.kompendium.core.fixtures.Nested
import io.bkbn.kompendium.core.fixtures.NullableEnum
import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultParams
import io.bkbn.kompendium.core.util.TestModules.defaultPath
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultRequestDescription
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
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.install
import io.ktor.server.auth.authenticate
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
fun Routing.nullableEnumField() = basicGetGenerator<NullableEnum>()
fun Routing.nullableReference() = basicGetGenerator<ManyThings>()
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.nestedTypeName() = basicGetGenerator<Nested.Response>()
fun Routing.topLevelNullable() = basicGetGenerator<TestResponse?>()
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
fun Routing.samePathDifferentMethodsAndAuth() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
authenticate("basic") {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
description(defaultRequestDescription)
requestType<TestSimpleRequest>()
}
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
}
}
}
}
}
}

View File

@ -0,0 +1,301 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
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.core.util.TestModules.defaultParams
import io.bkbn.kompendium.core.util.TestModules.defaultPath
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
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
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.overrideMediaTypes() {
route("/media_types") {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
mediaTypes("multipart/form-data", "application/json")
requestType<TestRequest>()
description("A cool request")
}
response {
mediaTypes("application/xml")
responseType<TestResponse>()
description("A good response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}

View File

@ -0,0 +1,22 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.Barzo
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.Flibbity
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.Foosy
import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.Page
import io.ktor.server.routing.Routing
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.gnarlyGenericResponse() = basicGetGenerator<Foosy<Barzo<Int>, String>>()
fun Routing.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>()
fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()

View File

@ -0,0 +1,32 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.DefaultField
import io.bkbn.kompendium.core.fixtures.NullableField
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.server.routing.Routing
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>()

View File

@ -0,0 +1,102 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultParams
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
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.install
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
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>()
}
}
}
}
}
}

View File

@ -1,66 +1,29 @@
package io.bkbn.kompendium.core.util package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.Barzo
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.Foosy
import io.bkbn.kompendium.core.fixtures.Gibbity
import io.bkbn.kompendium.core.fixtures.ManyThings
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.Nested
import io.bkbn.kompendium.core.fixtures.NullableEnum
import io.bkbn.kompendium.core.fixtures.NullableField
import io.bkbn.kompendium.core.fixtures.Page
import io.bkbn.kompendium.core.fixtures.ProfileUpdateRequest
import io.bkbn.kompendium.core.fixtures.SerialNameObject
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.fixtures.TransientObject
import io.bkbn.kompendium.core.fixtures.UnbackedObject
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo 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.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPathDescription
import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
import io.bkbn.kompendium.core.util.TestModules.rootPath
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.auth.authenticate
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.routing.Routing 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 import io.ktor.server.routing.route
object TestModules { 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( const val defaultPath = "/test/{a}"
const val rootPath = "/"
const val defaultResponseDescription = "A Successful Endeavor"
const val defaultRequestDescription = "You gotta send it"
const val defaultPathSummary = "Great Summary!"
const val defaultPathDescription = "testing more"
val defaultParams = listOf(
Parameter( Parameter(
name = "a", name = "a",
`in` = Parameter.Location.path, `in` = Parameter.Location.path,
@ -72,612 +35,31 @@ object TestModules {
schema = TypeDefinition.INT schema = TypeDefinition.INT
) )
) )
}
fun Routing.notarizedGet() { internal inline fun <reified T> Routing.basicGetGenerator(
route(defaultPath) { params: List<Parameter> = emptyList(),
install(NotarizedRoute()) { operationId: String? = null
parameters = defaultParams ) {
get = GetInfo.builder { route(rootPath) {
response { basicGetGenerator<T>(params, operationId)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description(defaultResponseDescription)
}
summary(defaultPathSummary)
description(defaultPathDescription)
}
}
get {
call.respondText { "hey dude ‼️ congrats on the get request" }
}
}
} }
}
fun Routing.notarizedPost() { internal inline fun <reified T> Route.basicGetGenerator(
route(defaultPath) { params: List<Parameter> = emptyList(),
install(NotarizedRoute()) { operationId: String? = null
parameters = defaultParams ) {
post = PostInfo.builder { install(NotarizedRoute()) {
summary(defaultPathSummary) get = GetInfo.builder {
description(defaultPathDescription) summary(defaultPathSummary)
request { description(defaultPathDescription)
requestType<TestSimpleRequest>() operationId?.let { operationId(it) }
description("A Test request") parameters = params
} response {
response { description(defaultResponseDescription)
responseCode(HttpStatusCode.Created) responseCode(HttpStatusCode.OK)
responseType<TestCreatedResponse>() responseType<T>()
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.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbackedObject>()
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
fun Routing.polymorphicCollectionResponse() = basicGetGenerator<List<FlibbityGibbit>>()
fun Routing.polymorphicMapResponse() = basicGetGenerator<Map<String, FlibbityGibbit>>()
fun Routing.simpleGenericResponse() = basicGetGenerator<Gibbity<String>>()
fun Routing.gnarlyGenericResponse() = basicGetGenerator<Foosy<Barzo<Int>, String>>()
fun Routing.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>()
fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
fun Routing.nullableEnumField() = basicGetGenerator<NullableEnum>()
fun Routing.nullableReference() = basicGetGenerator<ManyThings>()
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.nestedTypeName() = basicGetGenerator<Nested.Response>()
fun Routing.topLevelNullable() = basicGetGenerator<TestResponse?>()
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
fun Routing.defaultAuthConfig() {
authenticate("basic") {
route(rootPath) {
basicGetGenerator<TestResponse>()
}
}
}
fun Routing.customAuthConfig() {
authenticate("auth-oauth-google") {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
security = mapOf(
"auth-oauth-google" to listOf("read:pets")
)
}
}
}
}
}
fun Routing.multipleAuthStrategies() {
authenticate("jwt", "api-key") {
route(rootPath) {
basicGetGenerator<TestResponse>()
}
}
}
private inline fun <reified T> Routing.basicGetGenerator(
params: List<Parameter> = emptyList(),
operationId: String? = null
) {
route(rootPath) {
basicGetGenerator<T>(params, operationId)
}
}
private inline fun <reified T> Route.basicGetGenerator(
params: List<Parameter> = emptyList(),
operationId: String? = null
) {
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

@ -0,0 +1,123 @@
{
"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": {
"/media_types": {
"put": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A cool request",
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A good response",
"content": {
"application/xml": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"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": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,164 @@
{
"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/{a}": {
"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
},
"put": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "You gotta send it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestSimpleRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false,
"security": [
{
"basic": []
}
]
},
"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": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"TestSimpleRequest": {
"type": "object",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "number",
"format": "int32"
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {
"basic": {
"type": "http",
"scheme": "basic"
}
}
},
"security": [],
"tags": []
}

View File

@ -4,6 +4,12 @@ You can read more about it [here](https://ktor.io/docs/type-safe-routing.html).
Kompendium supports Ktor-Resources through an ancillary module `kompendium-resources` Kompendium supports Ktor-Resources through an ancillary module `kompendium-resources`
{% hint style="warning" %}
The resources module contains _two_ plugins: `KompendiumResources` and `KompendiumResource`. You will find more
information on both below, but in a nutshell, the former is an application level plugin intended to define your entire
application, while the latter is a route level approach should you wish to split out your route definitions.
{% endhint %}
## Adding the Artifact ## Adding the Artifact
Prior to documenting your resources, you will need to add the artifact to your gradle build file. Prior to documenting your resources, you will need to add the artifact to your gradle build file.
@ -14,9 +20,11 @@ dependencies {
} }
``` ```
## Installing Plugin ## NotarizedResources
Once you have installed the dependency, you can install the plugin. The `NotarizedResources` plugin is an _application_ level plugin, and **must** be install after both the `NotarizedApplication` plugin and the Ktor `Resources` plugin. The `NotarizedResources` plugin is an _application_ level plugin, and **must** be installed after both the
`NotarizedApplication` plugin and the Ktor `Resources` plugin. It is intended to be used to document your entire
application in a single block.
```kotlin ```kotlin
private fun Application.mainModule() { private fun Application.mainModule() {
@ -54,7 +62,64 @@ private fun Application.mainModule() {
} }
``` ```
Here, the `resources` property is a map of `KClass<*>` to `ResourceMetadata` instance describing that resource. This metadata is functionally identical to how a standard `NotarizedRoute` is defined. Here, the `resources` property is a map of `KClass<*>` to `ResourceMetadata` instance describing that resource. This
metadata is functionally identical to how a standard `NotarizedRoute` is defined.
> ⚠️ If you try to map a class that is not annotated with the ktor `@Resource` annotation, you will get a runtime {% hint style="danger" %}
> exception! If you try to map a class that is not annotated with the ktor `@Resource` annotation, you will get a runtime exception!
{% endhint %}
## NotarizedResource
If you prefer a route-based approach similar to `NotarizedRoute`, you can use the `NotarizedResource<MyResourceType>()`
plugin instead of `NotarizedResources`. It will combine paths from any parent route with the route defined in the
resource, exactly as Ktor itself does:
```kotlin
@Serializable
@Resource("/list/{name}/page/{page}")
data class Listing(val name: String, val page: Int)
private fun Application.mainModule() {
install(Resources)
route("/api") {
listingDocumentation()
get<Listing> { listing ->
call.respondText("Listing ${listing.name}, page ${listing.page}")
}
}
}
private fun Route.listingDocumentation() {
install(NotarizedResource<Listing>()) {
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
)
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
}
}
```
In this case, the generated path will be `/api/list/{name}/page/{page}`, combining the route prefix with the path in the
resource.
{% hint style="danger" %}
If you try to map a class that is not annotated with the ktor `@Resource` annotation, you will get a runtime exception!
{% endhint %}

View File

@ -165,3 +165,42 @@ get = GetInfo.builder {
} }
} }
``` ```
## Media Types
By default, Kompendium will set the only media type to "application/json". If you would like to override the media type
for a specific request or response (including errors), you can do so with the `mediaTypes` method
```kotlin
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
response {
mediaTypes("application/xml")
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
```
## Partial Authentication
One might want to have a public GET endpoint but a protected PUT endpoint. This can be achieved by registering two
separate notarized routes. Note that you will get an error if you try to register the same method twice, as each path
can only have one registration per method. Example:
```kotlin
route("/user/{id}") {
get = GetInfo.builder {
// ...
}
// ...
authenticate {
put = PutInfo.builder {
// ...
}
// ...
}
}
```

View File

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

View File

@ -20,7 +20,7 @@ dependencies {
// Versions // Versions
val detektVersion: String by project val detektVersion: String by project
implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.20") implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.21")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
// Formatting // Formatting

View File

@ -51,17 +51,17 @@ object NotarizedLocations {
val spec = application.attributes[KompendiumAttributes.openApiSpec] val spec = application.attributes[KompendiumAttributes.openApiSpec]
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
pluginConfig.locations.forEach { (k, v) -> pluginConfig.locations.forEach { (k, v) ->
val path = Path()
path.parameters = v.parameters
v.get?.addToSpec(path, spec, v, serializableReader)
v.delete?.addToSpec(path, spec, v, serializableReader)
v.head?.addToSpec(path, spec, v, serializableReader)
v.options?.addToSpec(path, spec, v, serializableReader)
v.post?.addToSpec(path, spec, v, serializableReader)
v.put?.addToSpec(path, spec, v, serializableReader)
v.patch?.addToSpec(path, spec, v, serializableReader)
val location = k.getLocationFromClass() val location = k.getLocationFromClass()
val path = spec.paths[location] ?: Path()
path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters
v.get?.addToSpec(path, spec, v, serializableReader, location)
v.delete?.addToSpec(path, spec, v, serializableReader, location)
v.head?.addToSpec(path, spec, v, serializableReader, location)
v.options?.addToSpec(path, spec, v, serializableReader, location)
v.post?.addToSpec(path, spec, v, serializableReader, location)
v.put?.addToSpec(path, spec, v, serializableReader, location)
v.patch?.addToSpec(path, spec, v, serializableReader, location)
spec.paths[location] = path spec.paths[location] = path
} }
} }

View File

@ -0,0 +1,21 @@
package io.bkbn.kompendium.resources
import io.ktor.resources.Resource
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
fun KClass<*>.getResourcePathFromClass(): String {
val resource = findAnnotation<Resource>()
?: error("Cannot notarize a resource without annotating with @Resource")
val path = resource.path
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Resource>() }
return if (parent == null) {
path
} else {
parent.getResourcePathFromClass() + path
}
}

View File

@ -0,0 +1,35 @@
package io.bkbn.kompendium.resources
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.plugin.NotarizedRoute.addToSpec
import io.bkbn.kompendium.core.plugin.NotarizedRoute.calculateRoutePath
import io.bkbn.kompendium.core.plugin.NotarizedRoute.collectAuthMethods
import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.Hook
import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.routing.Route
object NotarizedResource {
object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> {
override fun install(pipeline: ApplicationCallPipeline, handler: (ApplicationCallPipeline) -> Unit) {
handler(pipeline)
}
}
inline operator fun <reified T> invoke() = createRouteScopedPlugin(
name = "NotarizedResource<${T::class.qualifiedName}>",
createConfiguration = NotarizedRoute::Config
) {
on(InstallHook) {
val route = it as? Route ?: return@on
val spec = application.attributes[KompendiumAttributes.openApiSpec]
val routePath = route.calculateRoutePath()
val authMethods = route.collectAuthMethods()
val resourcePath = T::class.getResourcePathFromClass()
val fullPath = "$routePath$resourcePath"
addToSpec(spec, fullPath, authMethods)
}
}
}

View File

@ -12,12 +12,8 @@ import io.bkbn.kompendium.core.util.Helpers.addToSpec
import io.bkbn.kompendium.core.util.SpecConfig import io.bkbn.kompendium.core.util.SpecConfig
import io.bkbn.kompendium.oas.path.Path import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.resources.Resource
import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.createApplicationPlugin
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
object NotarizedResources { object NotarizedResources {
@ -45,34 +41,18 @@ object NotarizedResources {
val spec = application.attributes[KompendiumAttributes.openApiSpec] val spec = application.attributes[KompendiumAttributes.openApiSpec]
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator] val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
pluginConfig.resources.forEach { (k, v) -> pluginConfig.resources.forEach { (k, v) ->
val path = Path() val resource = k.getResourcePathFromClass()
path.parameters = v.parameters val path = spec.paths[resource] ?: Path()
v.get?.addToSpec(path, spec, v, serializableReader) path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters
v.delete?.addToSpec(path, spec, v, serializableReader) v.get?.addToSpec(path, spec, v, serializableReader, resource)
v.head?.addToSpec(path, spec, v, serializableReader) v.delete?.addToSpec(path, spec, v, serializableReader, resource)
v.options?.addToSpec(path, spec, v, serializableReader) v.head?.addToSpec(path, spec, v, serializableReader, resource)
v.post?.addToSpec(path, spec, v, serializableReader) v.options?.addToSpec(path, spec, v, serializableReader, resource)
v.put?.addToSpec(path, spec, v, serializableReader) v.post?.addToSpec(path, spec, v, serializableReader, resource)
v.patch?.addToSpec(path, spec, v, serializableReader) v.put?.addToSpec(path, spec, v, serializableReader, resource)
v.patch?.addToSpec(path, spec, v, serializableReader, resource)
val resource = k.getResourcesFromClass()
spec.paths[resource] = path spec.paths[resource] = path
} }
} }
private fun KClass<*>.getResourcesFromClass(): String {
// todo if parent
val resource = findAnnotation<Resource>()
?: error("Cannot notarize a resource without annotating with @Resource")
val path = resource.path
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Resource>() }
return if (parent == null) {
path
} else {
parent.getResourcesFromClass() + path
}
}
} }

View File

@ -14,9 +14,11 @@ import io.ktor.server.application.install
import io.ktor.server.resources.Resources import io.ktor.server.resources.Resources
import io.ktor.server.resources.get import io.ktor.server.resources.get
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
class KompendiumResourcesTest : DescribeSpec({ class KompendiumResourcesTest : DescribeSpec({
describe("Resource Tests") { describe("NotarizedResources Tests") {
it("Can notarize a simple resource") { it("Can notarize a simple resource") {
openApiTestAllSerializers( openApiTestAllSerializers(
snapshotName = "T0001__simple_resource.json", snapshotName = "T0001__simple_resource.json",
@ -117,4 +119,72 @@ class KompendiumResourcesTest : DescribeSpec({
} }
} }
} }
describe("NotarizedResource Tests") {
it("Can notarize resources in route") {
openApiTestAllSerializers(
snapshotName = "T0003__resources_in_route.json",
applicationSetup = {
install(Resources)
}
) {
route("/api") {
typeEditDocumentation()
get<Type.Edit> { edit ->
call.respondText("Listing ${edit.parent.name}")
}
typeOtherDocumentation()
get<Type.Other> { other ->
call.respondText("Listing ${other.parent.name}, page ${other.page}")
}
}
}
}
}
}) })
private fun Route.typeOtherDocumentation() {
install(NotarizedResource<Type.Other>()) {
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
),
Parameter(
name = "page",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT
)
)
get = GetInfo.builder {
summary("Other")
description("example resource")
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("does great things")
}
}
}
}
private fun Route.typeEditDocumentation() {
install(NotarizedResource<Type.Edit>()) {
parameters = listOf(
Parameter(
name = "name",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary("Edit")
description("example resource")
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("does great things")
}
}
}
}

View File

@ -0,0 +1,124 @@
{
"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": {
"/api/type/{name}/edit": {
"get": {
"tags": [],
"summary": "Edit",
"description": "example resource",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
]
},
"/api/type/{name}/other/{page}": {
"get": {
"tags": [],
"summary": "Other",
"description": "example resource",
"parameters": [],
"responses": {
"200": {
"description": "does great things",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "page",
"in": "path",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}