Compare commits

..

13 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
2492661f1f chore: prep for release 2022-11-05 16:11:38 -04:00
a7b52ec114 feat: create schema reference for enum types (#368) 2022-11-05 16:09:06 -04:00
8ebab04a83 fix(deps): update kotestversion to v5.5.4 (#363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-04 06:00:41 +00:00
921f6f9691 fix(deps): update dependency dev.forst:ktor-api-key to v2.1.3 (#361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-31 22:25:14 +00:00
558b9fea62 fix(deps): update ktor to v2.1.3 (#360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-31 17:01:30 +00:00
752cb238d3 fix(deps): update dependency joda-time:joda-time to v2.12.1 (#359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-30 06:12:11 +00:00
53 changed files with 1919 additions and 945 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,29 @@
## 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
### Changed
- Schemas for types in nullable properties are no longer nullable themselves
- Enums are now generated as references, which makes it possible to generate types for them
## [3.5.0] - October 29th, 2022 ## [3.5.0] - October 29th, 2022
### Added ### Added

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

@ -58,7 +58,7 @@ dependencies {
testFixturesApi("io.ktor:ktor-client:$ktorVersion") testFixturesApi("io.ktor:ktor-client:$ktorVersion")
testFixturesApi("io.ktor:ktor-client-cio:$ktorVersion") testFixturesApi("io.ktor:ktor-client-cio:$ktorVersion")
testFixturesApi("dev.forst:ktor-api-key:2.1.2") testFixturesApi("dev.forst:ktor-api-key:2.1.3")
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
} }

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] fun <T : SpecConfig> PluginBuilder<T>.addToSpec(
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] val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
val path = Path() pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
path.parameters = pluginConfig.parameters 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)
pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader) spec.paths[fullPath] = path
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 Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
private fun Route.collectAuthMethods() = toString() 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

@ -12,6 +12,8 @@ import io.bkbn.kompendium.core.metadata.PutInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
@ -22,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
@ -55,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)
} }
} }
@ -82,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
) )
@ -91,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())
) )
@ -99,18 +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 = ReferenceDefinition(this.getReferenceSlug()), schema = if (this.isMarkedNullable) OneOfDefinition(
NullableDefinition(),
ReferenceDefinition(this.getReferenceSlug())
) else ReferenceDefinition(this.getReferenceSlug()),
examples = examples examples = examples
) )
) }
} }
} }

View File

@ -2,56 +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.trailingSlash import io.bkbn.kompendium.core.util.topLevelNullable
import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse import io.bkbn.kompendium.core.util.unbackedFieldsResponse
import io.bkbn.kompendium.core.util.TestModules.withOperationId import io.bkbn.kompendium.core.util.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
@ -73,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") {
@ -111,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") {
@ -243,12 +250,45 @@ class KompendiumTest : DescribeSpec({
it("Can handle nested type names") { it("Can handle nested type names") {
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() } openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
} }
it("Can handle top level nullable types") {
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.UnbakcedObject
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,599 +35,21 @@ object TestModules {
schema = TypeDefinition.INT schema = TypeDefinition.INT
) )
) )
}
fun Routing.notarizedGet() { internal inline fun <reified T> Routing.basicGetGenerator(
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
get = GetInfo.builder {
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description(defaultResponseDescription)
}
summary(defaultPathSummary)
description(defaultPathDescription)
}
}
get {
call.respondText { "hey dude ‼️ congrats on the get request" }
}
}
}
fun Routing.notarizedPost() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<TestSimpleRequest>()
description("A Test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
post {
call.respondText { "hey dude ‼️ congrats on the post request" }
}
}
}
fun Routing.notarizedPut() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<TestSimpleRequest>()
description("A Test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
put {
call.respondText { "hey dude ‼️ congrats on the post request" }
}
}
}
fun Routing.notarizedDelete() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
delete = DeleteInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseCode(HttpStatusCode.NoContent)
responseType<Unit>()
description(defaultResponseDescription)
}
}
}
}
delete {
call.respond(HttpStatusCode.NoContent)
}
}
fun Routing.notarizedPatch() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
patch = PatchInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
description("A Test request")
requestType<TestSimpleRequest>()
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
patch {
call.respond(HttpStatusCode.Created) { TestCreatedResponse(123, "Nice!") }
}
}
}
fun Routing.notarizedHead() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
head = HeadInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description("great!")
responseCode(HttpStatusCode.Created)
responseType<Unit>()
}
}
}
head {
call.respond(HttpStatusCode.OK)
}
}
}
fun Routing.notarizedOptions() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
options = OptionsInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
description("nice")
}
}
}
options {
call.respond(HttpStatusCode.NoContent)
}
}
}
fun Routing.complexRequest() {
route(rootPath) {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<ComplexRequest>()
description("A Complex request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(defaultResponseDescription)
}
}
}
patch {
call.respond(HttpStatusCode.Created, TestCreatedResponse(123, "nice!"))
}
}
}
fun Routing.primitives() {
route(rootPath) {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
requestType<Int>()
description("A Test Request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<Boolean>()
description(defaultResponseDescription)
}
}
}
}
}
fun Routing.returnsList() {
route(defaultPath) {
install(NotarizedRoute()) {
parameters = defaultParams
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description("A Successful List-y Endeavor")
responseCode(HttpStatusCode.OK)
responseType<List<TestResponse>>()
}
}
}
}
}
fun Routing.nonRequiredParams() {
route("/optional") {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "notRequired",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
required = false,
),
Parameter(
name = "required",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
responseType<Unit>()
description("Empty")
responseCode(HttpStatusCode.NoContent)
}
}
}
}
}
fun Routing.simplePathParsing() {
route("/this") {
route("/is") {
route("/a") {
route("/complex") {
route("path") {
route("with/an/{id}") {
install(NotarizedRoute()) {
get = GetInfo.builder {
parameters = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
}
}
}
}
fun Routing.rootRoute() {
route(rootPath) {
install(NotarizedRoute()) {
parameters = listOf(defaultParams.last())
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
fun Routing.nestedUnderRoot() {
route("/") {
route("/testerino") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
fun Routing.trailingSlash() {
route("/test") {
route("/") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
}
}
}
}
}
fun Routing.singleException() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.BadRequest)
responseType<ExceptionResponse>()
}
}
}
}
}
fun Routing.multipleExceptions() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.BadRequest)
responseType<ExceptionResponse>()
}
canRespond {
description("Access Denied")
responseCode(HttpStatusCode.Forbidden)
responseType<ExceptionResponse>()
}
}
}
}
}
fun Routing.polymorphicException() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.InternalServerError)
responseType<FlibbityGibbit>()
}
}
}
}
}
fun Routing.genericException() {
route(rootPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
}
canRespond {
description("Bad Things Happened")
responseCode(HttpStatusCode.BadRequest)
responseType<Flibbity<String>>()
}
}
}
}
}
fun Routing.reqRespExamples() {
route(rootPath) {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
description(defaultRequestDescription)
requestType<TestRequest>()
examples(
"Testerina" to TestRequest(TestNested("asdf"), 1.5, emptyList())
)
}
response {
description(defaultResponseDescription)
responseCode(HttpStatusCode.OK)
responseType<TestResponse>()
examples(
"Testerino" to TestResponse("Heya")
)
}
}
}
}
}
fun Routing.exampleParams() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING,
examples = mapOf(
"foo" to Parameter.Example("testing")
)
)
)
)
fun Routing.defaultParameter() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING.withDefault("IDK")
)
)
)
fun Routing.requiredParams() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
)
fun Routing.nonRequiredParam() = basicGetGenerator<TestResponse>(
params = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
required = false
)
)
)
fun Routing.defaultField() = basicGetGenerator<DefaultField>()
fun Routing.nullableField() = basicGetGenerator<NullableField>()
fun Routing.polymorphicResponse() = basicGetGenerator<FlibbityGibbit>()
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbakcedObject>()
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.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(), params: List<Parameter> = emptyList(),
operationId: String? = null operationId: String? = null
) { ) {
route(rootPath) { route(rootPath) {
basicGetGenerator<T>(params, operationId) basicGetGenerator<T>(params, operationId)
} }
} }
private inline fun <reified T> Route.basicGetGenerator( internal inline fun <reified T> Route.basicGetGenerator(
params: List<Parameter> = emptyList(), params: List<Parameter> = emptyList(),
operationId: String? = null operationId: String? = null
) { ) {
install(NotarizedRoute()) { install(NotarizedRoute()) {
get = GetInfo.builder { get = GetInfo.builder {
summary(defaultPathSummary) summary(defaultPathSummary)
@ -678,5 +63,4 @@ object TestModules {
} }
} }
} }
}
} }

View File

@ -124,15 +124,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"enumeration": { "enumeration": {
"enum": [ "$ref": "#/components/schemas/SimpleEnum"
"ONE",
"TWO"
]
} }
}, },
"required": [ "required": [
"enumeration" "enumeration"
] ]
},
"SimpleEnum": {
"type": "string",
"enum": [
"ONE",
"TWO"
]
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -91,11 +91,6 @@
"required": [] "required": []
}, },
"ProfileMetadataUpdateRequest": { "ProfileMetadataUpdateRequest": {
"oneOf": [
{
"type": "null"
},
{
"type": "object", "type": "object",
"properties": { "properties": {
"isPrivate": { "isPrivate": {
@ -121,8 +116,6 @@
}, },
"required": [] "required": []
} }
]
}
}, },
"securitySchemes": {} "securitySchemes": {}
}, },

View File

@ -62,15 +62,20 @@
"type": "null" "type": "null"
}, },
{ {
"enum": [ "$ref": "#/components/schemas/TestEnum"
"YES",
"NO"
]
} }
] ]
} }
}, },
"required": [] "required": []
},
"TestEnum":
{
"type": "string",
"enum": [
"YES",
"NO"
]
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -57,10 +57,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"enumeration": { "enumeration": {
"enum": [ "$ref": "#/components/schemas/SimpleEnum"
"ONE",
"TWO"
]
} }
}, },
"required": [ "required": [
@ -120,6 +117,13 @@
"required": [ "required": [
"content" "content"
] ]
},
"SimpleEnum": {
"type": "string",
"enum": [
"ONE",
"TWO"
]
} }
}, },
"securitySchemes": {} "securitySchemes": {}

View File

@ -53,6 +53,14 @@
"webhooks": {}, "webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"ColumnMode": {
"type": "string",
"enum": [
"NULLABLE",
"REQUIRED",
"REPEATED"
]
},
"ColumnSchema": { "ColumnSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -60,11 +68,7 @@
"type": "string" "type": "string"
}, },
"mode": { "mode": {
"enum": [ "$ref": "#/components/schemas/ColumnMode"
"NULLABLE",
"REQUIRED",
"REPEATED"
]
}, },
"name": { "name": {
"type": "string" "type": "string"

View File

@ -39,7 +39,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/UnbakcedObject" "$ref": "#/components/schemas/UnbackedObject"
} }
} }
} }
@ -53,7 +53,7 @@
"webhooks": {}, "webhooks": {},
"components": { "components": {
"schemas": { "schemas": {
"UnbakcedObject": { "UnbackedObject": {
"type": "object", "type": "object",
"properties": { "properties": {
"backed": { "backed": {

View File

@ -0,0 +1,79 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"200": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/TestResponse"
}
]
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

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

@ -163,7 +163,7 @@ data class TransientObject(
) )
@Serializable @Serializable
data class UnbakcedObject( data class UnbackedObject(
val backed: String val backed: String
) { ) {
val unbacked: String get() = "unbacked" val unbacked: String get() = "unbacked"
@ -176,3 +176,14 @@ data class SerialNameObject(
@SerialName("snake_case_name") @SerialName("snake_case_name")
val camelCaseName: String val camelCaseName: String
) )
enum class Color {
RED,
GREEN,
BLUE
}
@Serializable
data class ObjectWithEnum(
val color: Color
)

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.5.0 project.version=3.8.0
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle
@ -9,5 +9,5 @@ org.gradle.jvmargs=-Xmx2000m
# Dependencies # Dependencies
ktorVersion=2.1.3 ktorVersion=2.1.3
kotestVersion=5.5.3 kotestVersion=5.5.4
detektVersion=1.21.0 detektVersion=1.21.0

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

@ -48,7 +48,7 @@ object SchemaGenerator {
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN) Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
UUID::class -> checkForNull(type, TypeDefinition.UUID) UUID::class -> checkForNull(type, TypeDefinition.UUID)
else -> when { else -> when {
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz) clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator) clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator)
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator) clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator)
else -> { else -> {

View File

@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class EnumDefinition( data class EnumDefinition(
val type: String,
val enum: Set<String> val enum: Set<String>
) : JsonSchema ) : JsonSchema

View File

@ -2,18 +2,17 @@ package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.definition.EnumDefinition import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
object EnumHandler { object EnumHandler {
fun handle(type: KType, clazz: KClass<*>): JsonSchema { fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
val options = clazz.java.enumConstants.map { it.toString() }.toSet() val options = clazz.java.enumConstants.map { it.toString() }.toSet()
val definition = EnumDefinition(enum = options) return EnumDefinition(type = "string", enum = options)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)
false -> definition
}
} }
} }

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
@ -71,16 +72,11 @@ object SimpleObjectHandler {
.map { schemaConfigurator.serializableName(it) } .map { schemaConfigurator.serializableName(it) }
.toSet() .toSet()
val definition = TypeDefinition( return TypeDefinition(
type = "object", type = "object",
properties = props, properties = props,
required = required required = required
) )
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)
false -> definition
}
} }
private fun KProperty<*>.needsToInjectGenerics( private fun KProperty<*>.needsToInjectGenerics(
@ -103,7 +99,7 @@ object SimpleObjectHandler {
} }
val constructedType = propClass.createType(types) val constructedType = propClass.createType(types)
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let { return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let {
if (it.isOrContainsObjectDef()) { if (it.isOrContainsObjectOrEnumDef()) {
cache[constructedType.getSimpleSlug()] = it cache[constructedType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug()) ReferenceDefinition(prop.returnType.getReferenceSlug())
} else { } else {
@ -121,7 +117,7 @@ object SimpleObjectHandler {
val type = typeMap[prop.returnType.classifier]?.type val type = typeMap[prop.returnType.classifier]?.type
?: error("This indicates a bug in Kompendium, please open a GitHub issue") ?: error("This indicates a bug in Kompendium, please open a GitHub issue")
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let { return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let {
if (it.isOrContainsObjectDef()) { if (it.isOrContainsObjectOrEnumDef()) {
cache[type.getSimpleSlug()] = it cache[type.getSimpleSlug()] = it
ReferenceDefinition(type.getReferenceSlug()) ReferenceDefinition(type.getReferenceSlug())
} else { } else {
@ -136,7 +132,7 @@ object SimpleObjectHandler {
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator
): JsonSchema = ): JsonSchema =
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let { SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let {
if (it.isOrContainsObjectDef()) { if (it.isOrContainsObjectOrEnumDef()) {
cache[prop.returnType.getSimpleSlug()] = it cache[prop.returnType.getSimpleSlug()] = it
ReferenceDefinition(prop.returnType.getReferenceSlug()) ReferenceDefinition(prop.returnType.getReferenceSlug())
} else { } else {
@ -144,10 +140,12 @@ object SimpleObjectHandler {
} }
} }
private fun JsonSchema.isOrContainsObjectDef(): Boolean { private fun JsonSchema.isOrContainsObjectOrEnumDef(): Boolean {
val isTypeDef = this is TypeDefinition && type == "object" val isTypeDef = this is TypeDefinition && type == "object"
val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" } val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" }
return isTypeDef || isTypeDefOneOf val isEnumDef = this is EnumDefinition
val isEnumDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is EnumDefinition }
return isTypeDef || isTypeDefOneOf || isEnumDef || isEnumDefOneOf
} }
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition } private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition }

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.core.fixtures.ComplexRequest import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
import io.bkbn.kompendium.core.fixtures.ObjectWithEnum
import io.bkbn.kompendium.core.fixtures.SerialNameObject import io.bkbn.kompendium.core.fixtures.SerialNameObject
import io.bkbn.kompendium.core.fixtures.SimpleEnum import io.bkbn.kompendium.core.fixtures.SimpleEnum
import io.bkbn.kompendium.core.fixtures.SlammaJamma import io.bkbn.kompendium.core.fixtures.SlammaJamma
@ -9,7 +10,7 @@ import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.core.fixtures.TestResponse import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
import io.bkbn.kompendium.core.fixtures.TransientObject import io.bkbn.kompendium.core.fixtures.TransientObject
import io.bkbn.kompendium.core.fixtures.UnbakcedObject import io.bkbn.kompendium.core.fixtures.UnbackedObject
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.kotest.assertions.json.shouldEqualJson import io.kotest.assertions.json.shouldEqualJson
import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrow
@ -40,6 +41,7 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<ComplexRequest>("T0005__complex_object.json") jsonSchemaTest<ComplexRequest>("T0005__complex_object.json")
} }
it("Can generate the schema for a nullable object") { it("Can generate the schema for a nullable object") {
// Same schema as a non-nullable type, since the nullability will be handled on the property
jsonSchemaTest<TestSimpleRequest?>("T0006__nullable_object.json") jsonSchemaTest<TestSimpleRequest?>("T0006__nullable_object.json")
} }
it("Can generate the schema for a polymorphic object") { it("Can generate the schema for a polymorphic object") {
@ -52,7 +54,7 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<TransientObject>("T0018__transient_object.json") jsonSchemaTest<TransientObject>("T0018__transient_object.json")
} }
it("Can generate the schema for object with unbacked property") { it("Can generate the schema for object with unbacked property") {
jsonSchemaTest<UnbakcedObject>("T0019__unbacked_object.json") jsonSchemaTest<UnbackedObject>("T0019__unbacked_object.json")
} }
it("Can generate the schema for object with SerialName annotation") { it("Can generate the schema for object with SerialName annotation") {
jsonSchemaTest<SerialNameObject>("T0020__serial_name_object.json") jsonSchemaTest<SerialNameObject>("T0020__serial_name_object.json")
@ -63,8 +65,12 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<SimpleEnum>("T0007__simple_enum.json") jsonSchemaTest<SimpleEnum>("T0007__simple_enum.json")
} }
it("Can generate the schema for a nullable enum") { it("Can generate the schema for a nullable enum") {
// Same schema as a non-nullable enum, since the nullability will be handled on the property
jsonSchemaTest<SimpleEnum?>("T0008__nullable_enum.json") jsonSchemaTest<SimpleEnum?>("T0008__nullable_enum.json")
} }
it("Can generate the schema for an object with an enum property") {
jsonSchemaTest<ObjectWithEnum>("T0021__object_with_enum.json")
}
} }
describe("Arrays") { describe("Arrays") {
it("Can generate the schema for an array of scalars") { it("Can generate the schema for an array of scalars") {

View File

@ -1,9 +1,4 @@
{ {
"oneOf": [
{
"type": "null"
},
{
"type": "object", "type": "object",
"properties": { "properties": {
"a": { "a": {
@ -18,6 +13,4 @@
"a", "a",
"b" "b"
] ]
}
]
} }

View File

@ -1,3 +1,4 @@
{ {
"enum": [ "ONE", "TWO" ] "enum": [ "ONE", "TWO" ],
"type": "string"
} }

View File

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

View File

@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"color": {
"$ref": "#/components/schemas/Color"
}
},
"required": [
"color"
]
}

View File

@ -22,8 +22,8 @@ dependencies {
// IMPLEMENTATION // IMPLEMENTATION
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation("io.ktor:ktor-server-core:2.1.2") implementation("io.ktor:ktor-server-core:2.1.3")
implementation("io.ktor:ktor-server-locations:2.1.2") implementation("io.ktor:ktor-server-locations:2.1.3")
// TESTING // TESTING

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

@ -43,5 +43,5 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation("joda-time:joda-time:2.12.0") implementation("joda-time:joda-time:2.12.1")
} }

View File

@ -22,8 +22,8 @@ dependencies {
// IMPLEMENTATION // IMPLEMENTATION
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation("io.ktor:ktor-server-core:2.1.2") implementation("io.ktor:ktor-server-core:2.1.3")
implementation("io.ktor:ktor-server-resources:2.1.2") implementation("io.ktor:ktor-server-resources:2.1.3")
// TESTING // TESTING

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": []
}