Compare commits

...

49 Commits

Author SHA1 Message Date
8cc229667e chore: prep for 3.12.0 release 2023-03-14 16:52:16 -04:00
54d12de67a feat: reintroduce swagger compatability (#426) 2023-03-14 20:43:06 +00:00
9b93c887a2 chore(deps): update plugin io.github.gradle-nexus.publish-plugin to v1.2.0 (#424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 06:04:16 +00:00
19d828956b fix(deps): update dependency org.apache.logging.log4j:log4j-core to v2.20.0 (#423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 18:05:45 +00:00
eabe90acfc fix(deps): update dependency org.apache.logging.log4j:log4j-api to v2.20.0 (#422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 17:58:41 +00:00
7c2a2a9c9d fix(deps): update dependency com.google.protobuf:protobuf-java to v3.22.0 (#421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-17 05:27:43 +00:00
15cdfb229d fix(deps): update kotestversion to v5.5.5 (#420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-05 21:56:57 +00:00
5a40f37f81 fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.10 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 05:51:57 +00:00
64f2516f19 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.8.10 (#416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 00:22:16 +00:00
2e5e39d3b2 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.8.10 (#415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-02 19:52:53 +00:00
5342cf00d1 fix(deps): update ktor to v2.2.3 (#414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-01 04:08:51 +00:00
8c0b658033 fix: drop logback (#413) 2023-01-31 08:37:57 -05:00
b7b1171685 fix(deps): update dependency dev.forst:ktor-api-key to v2.2.2 (#410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-17 17:26:25 +00:00
1f730869a8 chore: fix incorrect line in documentation 2023-01-07 17:16:05 -05:00
377a60614e feat: constraints (#409) 2023-01-05 14:44:24 +00:00
e34bea1769 fix(deps): update ktor (#397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-04 21:49:06 -05:00
91bf93a866 feat: type enrichment (#408) 2023-01-04 21:32:31 -05:00
73fb8b137f fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.8.0 (#405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-28 22:11:08 +00:00
d3d6e79329 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.21.12 (#402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 10:02:49 +00:00
f2b7d924e0 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.6 (#401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-13 10:38:53 +00:00
f62010b4e7 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.6 (#400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-13 07:39:57 +00:00
5bd1534270 fix(deps): update dependency com.google.protobuf:protobuf-java to v3.21.11 (#398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 22:19:05 +00:00
4c151ffeea fix(deps): update dependency joda-time:joda-time to v2.12.2 (#396)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 18:21:42 +00:00
08a0d2e47c fix(deps): update dependency com.google.protobuf:protobuf-java to v3.21.10 (#395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 09:58:10 +00:00
f19476d3a3 chore: bump gradle version 2022-11-30 12:48:46 -05:00
8ede53fd5c fix(deps): update dependency org.jetbrains.kotlin:kotlin-reflect to v1.7.22 (#394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-28 21:50:54 +00:00
31a9f44ec1 chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.7.22 (#393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-28 15:31:11 +00:00
0eb2f126b3 chore(deps): update plugin org.jetbrains.kotlin.jvm to v1.7.22 (#392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-28 15:24:18 +00:00
0d658bd6a8 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.5 (#390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-25 19:01:28 +00:00
e924671c2b fix(deps): update dependency org.slf4j:slf4j-api to v2.0.5 (#389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-25 18:54:39 +00:00
04996631b9 fix(deps): update dependency ch.qos.logback:logback-classic to v1.4.5 (#387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-19 06:38:07 +00:00
82d80873c8 fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.4 (#386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-18 09:50:16 +00:00
487eaba741 fix(deps): update dependency org.slf4j:slf4j-api to v2.0.4 (#385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-18 06:42:30 +00:00
b935cc1e1d chore: prep for 3.9.0 release 2022-11-15 20:49:58 -05:00
9f2d2dd4e3 fix: do not include rootPath in serialized route path 2022-11-16 01:46:29 +00:00
20f8a4f08f feat: protobuf java converters (#382) 2022-11-15 15:50:26 -05:00
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
134 changed files with 12786 additions and 1055 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,57 @@
## Released ## Released
## [3.12.0] - March 14th, 2023
### Added
- Add support for swagger documentation
## [3.11.0] - January 5th, 2023
### Added
- Support for type constraints.
## [3.10.0] - January 4th, 2023
### Added
- Support for type enrichments! `deprecated` and `description` to start
## [3.9.0] - November 15th, 2022
### Added
- `protobuf-java-converter` module for converting generated protobuf objects to `JsonSchema` representations
### Changed
- Application `rootPath` is no longer prefixed to serialized route path when `NotarizedRoute` is resolved
## [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,12 +1,12 @@
plugins { plugins {
kotlin("jvm") version "1.7.20" apply false kotlin("jvm") version "1.8.10" apply false
kotlin("plugin.serialization") version "1.7.20" apply false kotlin("plugin.serialization") version "1.8.10" 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"
id("com.github.jakemarsden.git-hooks") version "0.0.2" id("com.github.jakemarsden.git-hooks") version "0.0.2"
id("org.jetbrains.kotlinx.kover") version "0.6.1" id("org.jetbrains.kotlinx.kover") version "0.6.1"
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("io.github.gradle-nexus.publish-plugin") version "1.2.0"
} }
gitHooks { gitHooks {

View File

@ -33,7 +33,6 @@ dependencies {
implementation("io.ktor:ktor-server-html-builder:$ktorVersion") implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("ch.qos.logback:logback-classic:1.4.4")
// Formatting // Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
@ -58,7 +57,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.2.2")
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
} }

View File

@ -1,13 +1,16 @@
package io.bkbn.kompendium.core.metadata package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.oas.payload.MediaType import io.bkbn.kompendium.oas.payload.MediaType
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
class RequestInfo private constructor( class RequestInfo private constructor(
val requestType: KType, val requestType: KType,
val typeEnrichment: TypeEnrichment<*>?,
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 {
@ -20,14 +23,23 @@ class RequestInfo private constructor(
class Builder { class Builder {
private var requestType: KType? = null private var requestType: KType? = null
private var typeEnrichment: TypeEnrichment<*>? = 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
} }
inline fun <reified T> requestType() = apply { requestType(typeOf<T>()) } fun enrichment(t: TypeEnrichment<*>) = apply {
this.typeEnrichment = t
}
inline fun <reified T> requestType(enrichment: TypeEnrichment<T>? = null) = apply {
requestType(typeOf<T>())
enrichment?.let { enrichment(it) }
}
fun description(s: String) = apply { this.description = s } fun description(s: String) = apply { this.description = s }
@ -35,10 +47,16 @@ 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 typeEnrichment = typeEnrichment,
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
) )
} }
} }

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.core.metadata package io.bkbn.kompendium.core.metadata
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.oas.payload.MediaType import io.bkbn.kompendium.oas.payload.MediaType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import kotlin.reflect.KType import kotlin.reflect.KType
@ -8,8 +9,10 @@ import kotlin.reflect.typeOf
class ResponseInfo private constructor( class ResponseInfo private constructor(
val responseCode: HttpStatusCode, val responseCode: HttpStatusCode,
val responseType: KType, val responseType: KType,
val typeEnrichment: TypeEnrichment<*>?,
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 {
@ -23,8 +26,10 @@ class ResponseInfo private constructor(
class Builder { class Builder {
private var responseCode: HttpStatusCode? = null private var responseCode: HttpStatusCode? = null
private var responseType: KType? = null private var responseType: KType? = null
private var typeEnrichment: TypeEnrichment<*>? = 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
@ -34,7 +39,14 @@ class ResponseInfo private constructor(
this.responseType = t this.responseType = t
} }
inline fun <reified T> responseType() = apply { responseType(typeOf<T>()) } fun enrichment(t: TypeEnrichment<*>) = apply {
this.typeEnrichment = t
}
inline fun <reified T> responseType(enrichment: TypeEnrichment<T>? = null) = apply {
responseType(typeOf<T>())
enrichment?.let { enrichment(it) }
}
fun description(s: String) = apply { this.description = s } fun description(s: String) = apply { this.description = s }
@ -42,11 +54,17 @@ 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 typeEnrichment = typeEnrichment,
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
) )
} }
} }

View File

@ -10,16 +10,17 @@ 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
import io.ktor.server.routing.application
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 +32,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> {
@ -44,69 +44,53 @@ object NotarizedRoute {
name = "NotarizedRoute", name = "NotarizedRoute",
createConfiguration = ::Config createConfiguration = ::Config
) { ) {
// This is required in order to introspect the route path and authentication // This is required in order to introspect the route path and authentication
on(InstallHook) { on(InstallHook) {
val route = it as? Route ?: return@on val route = it as? Route ?: return@on
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()
private fun Route.collectAuthMethods() = toString() .let {
application.environment.rootPath.takeIf { root -> root.isNotEmpty() }
?.let { root ->
val sanitizedRoute = if (root.startsWith("/")) root else "/$root"
it.replace(sanitizedRoute, "")
}
?: it
}
.replace(Regex("/\\(.+\\)"), "")
fun Route.collectAuthMethods() = toString()
.split("/") .split("/")
.filter { it.contains(Regex("\\(authenticate .*\\)")) } .filter { it.contains(Regex("\\(authenticate .*\\)")) }
.map { it.replace("(authenticate ", "").replace(")", "") } .map { it.replace("(authenticate ", "").replace(")", "") }
.map { it.split(", ") } .map { it.split(", ") }
.flatten() .flatten()
private fun Path.addDefaultAuthMethods(methods: List<String>) {
get?.addDefaultAuthMethods(methods)
put?.addDefaultAuthMethods(methods)
post?.addDefaultAuthMethods(methods)
delete?.addDefaultAuthMethods(methods)
options?.addDefaultAuthMethods(methods)
head?.addDefaultAuthMethods(methods)
patch?.addDefaultAuthMethods(methods)
trace?.addDefaultAuthMethods(methods)
}
private fun PathOperation.addDefaultAuthMethods(methods: List<String>) {
methods.forEach { m ->
if (security == null || security?.all { s -> !s.containsKey(m) } == true) {
if (security == null) {
security = mutableListOf(mapOf(m to emptyList()))
} else {
security?.add(mapOf(m to emptyList()))
}
}
}
}
} }

View File

@ -18,7 +18,7 @@ import kotlinx.html.unsafe
/** /**
* Provides an out-of-the-box route to view docs using ReDoc on the specified [path]. * Provides an out-of-the-box route to view docs using ReDoc on the specified [path].
* @param pageTitle Webpage title you wish to be displayed on your docs * @param pageTitle Webpage title you wish to be displayed on your docs
* @param route path to docs resource * @param path path to docs resource
* @param specUrl url to point ReDoc to the OpenAPI json document * @param specUrl url to point ReDoc to the OpenAPI json document
*/ */
fun Route.redoc(pageTitle: String = "Docs", path: String = "/docs", specUrl: String = "/openapi.json") { fun Route.redoc(pageTitle: String = "Docs", path: String = "/docs", specUrl: String = "/openapi.json") {

View File

@ -0,0 +1,91 @@
package io.bkbn.kompendium.core.routes
import io.ktor.server.application.call
import io.ktor.server.html.respondHtml
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.html.body
import kotlinx.html.div
import kotlinx.html.head
import kotlinx.html.id
import kotlinx.html.link
import kotlinx.html.meta
import kotlinx.html.script
import kotlinx.html.title
import kotlinx.html.unsafe
/**
* Provides an out-of-the-box route to view docs using Swagger
* @see <a href="https://swagger.io/specification/">Swagger OpenApi Specification</a>
* for the latest supported open api version.
* @param pageTitle Webpage title you wish to be displayed on your docs
* @param path path to docs resource
* @param specUrl url to point Swagger to the OpenAPI json document
* @param swaggerVersion version of swagger-ui distribution
*/
fun Route.swagger(
pageTitle: String = "Docs",
path: String = "/swagger-ui",
specUrl: String = "/openapi.json",
swaggerVersion: String? = null
) {
val swaggerVersionSuffix = if (swaggerVersion == null) "" else "@$swaggerVersion"
route(path) {
get {
call.respondHtml {
head {
title {
+pageTitle
}
meta {
charset = "utf-8"
}
meta {
name = "viewport"
content = "width=device-width, initial-scale=1"
}
link {
href = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui.css"
rel = "stylesheet"
}
}
body {
div {
id = "swagger-ui"
}
script {
src = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui-standalone-preset.js"
}
script {
src = "https://unpkg.com/swagger-ui-dist$swaggerVersionSuffix/swagger-ui-bundle.js"
}
unsafe {
+"""
<script>
window.onload = function () {
// Build a system
const ui = SwaggerUIBundle({
url: "$specUrl",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
})
window.ui = ui
}
</script>
""".trimIndent()
}
}
}
}
}
}

View File

@ -10,11 +10,14 @@ import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo 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.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.getSlug
import io.bkbn.kompendium.oas.OpenApiSpec 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.path.PathOperation
@ -22,32 +25,60 @@ 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, type = this.response.responseType,
spec.components.schemas, schemaConfigurator cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = this.response.typeEnrichment,
)?.let { schema -> )?.let { schema ->
spec.components.schemas[this.response.responseType.getSimpleSlug()] = schema spec.components.schemas[this.response.responseType.getSlug(this.response.typeEnrichment)] = schema
} }
errors.forEach { error -> errors.forEach { error ->
SchemaGenerator.fromTypeOrUnit(error.responseType, spec.components.schemas, schemaConfigurator)?.let { schema -> SchemaGenerator.fromTypeOrUnit(
spec.components.schemas[error.responseType.getSimpleSlug()] = schema type = error.responseType,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = error.typeEnrichment,
)?.let { schema ->
spec.components.schemas[error.responseType.getSlug(error.typeEnrichment)] = schema
} }
} }
when (this) { when (this) {
is MethodInfoWithRequest -> { is MethodInfoWithRequest -> {
SchemaGenerator.fromTypeOrUnit( SchemaGenerator.fromTypeOrUnit(
this.request.requestType, type = this.request.requestType,
spec.components.schemas, cache = spec.components.schemas,
schemaConfigurator schemaConfigurator = schemaConfigurator,
enrichment = this.request.typeEnrichment,
)?.let { schema -> )?.let { schema ->
spec.components.schemas[this.request.requestType.getSimpleSlug()] = schema spec.components.schemas[this.request.requestType.getSlug(this.request.typeEnrichment)] = schema
} }
} }
@ -55,15 +86,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 +123,11 @@ 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(
examples = this.request.examples,
mediaTypes = this.request.mediaTypes,
enrichment = this.request.typeEnrichment
),
required = true required = true
) )
@ -91,7 +136,11 @@ 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(
examples = this.response.examples,
mediaTypes = this.response.mediaTypes,
enrichment = this.response.typeEnrichment
)
) )
).plus(this.errors.toResponseMap()) ).plus(this.errors.toResponseMap())
) )
@ -99,18 +148,33 @@ 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(
examples = error.examples,
mediaTypes = error.mediaTypes,
enrichment = error.typeEnrichment
)
) )
} }
private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? = private fun KType.toReferenceContent(
examples: Map<String, MediaType.Example>?,
mediaTypes: Set<String>,
enrichment: TypeEnrichment<*>?
): 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(enrichment))
)
} else {
ReferenceDefinition(this.getReferenceSlug(enrichment))
},
examples = examples examples = examples
) )
) }
} }
} }

View File

@ -2,56 +2,70 @@ 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.arrayConstraints
import io.bkbn.kompendium.core.util.TestModules.customAuthConfig import io.bkbn.kompendium.core.util.complexRequest
import io.bkbn.kompendium.core.util.TestModules.customFieldNameResponse import io.bkbn.kompendium.core.util.customAuthConfig
import io.bkbn.kompendium.core.util.TestModules.dateTimeString import io.bkbn.kompendium.core.util.customFieldNameResponse
import io.bkbn.kompendium.core.util.TestModules.defaultAuthConfig import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.core.util.TestModules.defaultField import io.bkbn.kompendium.core.util.defaultAuthConfig
import io.bkbn.kompendium.core.util.TestModules.defaultParameter import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.TestModules.exampleParams import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.TestModules.genericException import io.bkbn.kompendium.core.util.doubleConstraints
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponse import io.bkbn.kompendium.core.util.enrichedComplexGenericType
import io.bkbn.kompendium.core.util.TestModules.genericPolymorphicResponseMultipleImpls import io.bkbn.kompendium.core.util.enrichedNestedCollection
import io.bkbn.kompendium.core.util.TestModules.gnarlyGenericResponse import io.bkbn.kompendium.core.util.enrichedSimpleRequest
import io.bkbn.kompendium.core.util.TestModules.headerParameter import io.bkbn.kompendium.core.util.enrichedSimpleResponse
import io.bkbn.kompendium.core.util.TestModules.ignoredFieldsResponse import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.TestModules.multipleAuthStrategies import io.bkbn.kompendium.core.util.genericException
import io.bkbn.kompendium.core.util.TestModules.multipleExceptions import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.nestedGenericCollection import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.TestModules.nestedGenericMultipleParamsCollection import io.bkbn.kompendium.core.util.gnarlyGenericResponse
import io.bkbn.kompendium.core.util.TestModules.nestedGenericResponse import io.bkbn.kompendium.core.util.headerParameter
import io.bkbn.kompendium.core.util.TestModules.nestedTypeName import io.bkbn.kompendium.core.util.ignoredFieldsResponse
import io.bkbn.kompendium.core.util.TestModules.nestedUnderRoot import io.bkbn.kompendium.core.util.intConstraints
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParam import io.bkbn.kompendium.core.util.multipleAuthStrategies
import io.bkbn.kompendium.core.util.TestModules.nonRequiredParams import io.bkbn.kompendium.core.util.multipleExceptions
import io.bkbn.kompendium.core.util.TestModules.notarizedDelete import io.bkbn.kompendium.core.util.nestedGenericCollection
import io.bkbn.kompendium.core.util.TestModules.notarizedGet import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.TestModules.notarizedHead import io.bkbn.kompendium.core.util.nestedGenericResponse
import io.bkbn.kompendium.core.util.TestModules.notarizedOptions import io.bkbn.kompendium.core.util.nestedTypeName
import io.bkbn.kompendium.core.util.TestModules.notarizedPatch import io.bkbn.kompendium.core.util.nestedUnderRoot
import io.bkbn.kompendium.core.util.TestModules.notarizedPost import io.bkbn.kompendium.core.util.nonRequiredParam
import io.bkbn.kompendium.core.util.TestModules.notarizedPut import io.bkbn.kompendium.core.util.nonRequiredParams
import io.bkbn.kompendium.core.util.TestModules.nullableEnumField import io.bkbn.kompendium.core.util.notarizedDelete
import io.bkbn.kompendium.core.util.TestModules.nullableField import io.bkbn.kompendium.core.util.notarizedGet
import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject import io.bkbn.kompendium.core.util.notarizedHead
import io.bkbn.kompendium.core.util.TestModules.nullableReference import io.bkbn.kompendium.core.util.notarizedOptions
import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse import io.bkbn.kompendium.core.util.notarizedPatch
import io.bkbn.kompendium.core.util.TestModules.polymorphicException import io.bkbn.kompendium.core.util.notarizedPost
import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse import io.bkbn.kompendium.core.util.notarizedPut
import io.bkbn.kompendium.core.util.TestModules.polymorphicResponse import io.bkbn.kompendium.core.util.nullableEnumField
import io.bkbn.kompendium.core.util.TestModules.primitives import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.TestModules.reqRespExamples import io.bkbn.kompendium.core.util.nullableNestedObject
import io.bkbn.kompendium.core.util.TestModules.requiredParams import io.bkbn.kompendium.core.util.nullableReference
import io.bkbn.kompendium.core.util.TestModules.returnsList import io.bkbn.kompendium.core.util.overrideMediaTypes
import io.bkbn.kompendium.core.util.TestModules.rootRoute import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse import io.bkbn.kompendium.core.util.polymorphicException
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing import io.bkbn.kompendium.core.util.polymorphicMapResponse
import io.bkbn.kompendium.core.util.TestModules.simpleRecursive import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.TestModules.singleException import io.bkbn.kompendium.core.util.primitives
import io.bkbn.kompendium.core.util.TestModules.trailingSlash import io.bkbn.kompendium.core.util.reqRespExamples
import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse import io.bkbn.kompendium.core.util.requiredParams
import io.bkbn.kompendium.core.util.TestModules.withOperationId import io.bkbn.kompendium.core.util.returnsList
import io.bkbn.kompendium.core.util.rootRoute
import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth
import io.bkbn.kompendium.core.util.samePathSameMethod
import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.simplePathParsing
import io.bkbn.kompendium.core.util.simpleRecursive
import io.bkbn.kompendium.core.util.singleException
import io.bkbn.kompendium.core.util.stringConstraints
import io.bkbn.kompendium.core.util.stringContentEncodingConstraints
import io.bkbn.kompendium.core.util.stringPatternConstraints
import io.bkbn.kompendium.core.util.topLevelNullable
import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
import io.bkbn.kompendium.core.util.withOperationId
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 +87,9 @@ 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.net.URI
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 +126,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,15 +261,68 @@ 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() }
}
it("Can generate paths without application root-path") {
openApiTestAllSerializers(
"T0054__app_with_rootpath.json",
applicationEnvironmentBuilder = {
rootPath = "/example"
},
specOverrides = {
copy(
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
)
}
) { notarizedGet() }
}
} }
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(
snapshotName = "",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
) {
samePathSameMethod()
}
}
exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET")
} }
describe("Constraints") {
// TODO Assess strategies here
} }
describe("Formats") { describe("Formats") {
it("Can set a format for a simple type schema") { it("Can set a format for a simple type schema") {
@ -360,4 +431,40 @@ class KompendiumTest : DescribeSpec({
) { multipleAuthStrategies() } ) { multipleAuthStrategies() }
} }
} }
describe("Enrichment") {
it("Can enrich a simple request") {
openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() }
}
it("Can enrich a simple response") {
openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() }
}
it("Can enrich a nested collection") {
openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() }
}
it("Can enrich a complex generic type") {
openApiTestAllSerializers("T0057__enriched_complex_generic_type.json") { enrichedComplexGenericType() }
}
}
describe("Constraints") {
it("Can apply constraints to an int field") {
openApiTestAllSerializers("T0059__int_constraints.json") { intConstraints() }
}
it("Can apply constraints to a double field") {
openApiTestAllSerializers("T0060__double_constraints.json") { doubleConstraints() }
}
it("Can apply a min and max length to a string field") {
openApiTestAllSerializers("T0061__string_min_max_constraints.json") { stringConstraints() }
}
it("Can apply a pattern to a string field") {
openApiTestAllSerializers("T0062__string_pattern_constraints.json") { stringPatternConstraints() }
}
it("Can apply a content encoding and media type to a string field") {
openApiTestAllSerializers("T0063__string_content_encoding_constraints.json") {
stringContentEncodingConstraints()
}
}
it("Can apply constraints to an array field") {
openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() }
}
}
}) })

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,160 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.DoubleResponse
import io.bkbn.kompendium.core.fixtures.Page
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestNested
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPath
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.intConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get an int")
description("Get an int")
response {
responseCode(HttpStatusCode.OK)
description("An int")
responseType(
enrichment = TypeEnrichment("example") {
TestCreatedResponse::id {
minimum = 2
maximum = 100
multipleOf = 2
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.doubleConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a double")
description("Get a double")
response {
responseCode(HttpStatusCode.OK)
description("A double")
responseType(
enrichment = TypeEnrichment("example") {
DoubleResponse::payload {
minimum = 2.0
maximum = 100.0
multipleOf = 2.0
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.stringConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a string")
description("Get a string with constraints")
response {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
TestNested::nesty {
maxLength = 10
minLength = 2
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.stringPatternConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a string")
description("This is a description")
response {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
TestNested::nesty {
pattern = "[a-z]+"
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.stringContentEncodingConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get a string")
description("This is a description")
response {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
TestNested::nesty {
contentEncoding = "base64"
contentMediaType = "image/png"
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}
fun Routing.arrayConstraints() {
route(defaultPath) {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary("Get an array")
description("Get an array of strings")
response {
responseCode(HttpStatusCode.OK)
description("An array")
responseType(
enrichment = TypeEnrichment("example") {
Page<String>::content {
minItems = 2
maxItems = 10
uniqueItems = true
}
}
)
responseCode(HttpStatusCode.OK)
}
}
}
}
}

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,137 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
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.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.enrichedSimpleResponse() {
route("/enriched") {
install(NotarizedRoute()) {
get = GetInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
response {
responseType(
enrichment = TypeEnrichment("simple") {
TestResponse::c {
description = "A simple description"
}
}
)
description("A good response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}
fun Routing.enrichedSimpleRequest() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
TestSimpleRequest::a {
description = "A simple description"
}
TestSimpleRequest::b {
deprecated = true
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
fun Routing.enrichedNestedCollection() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
ComplexRequest::tables {
description = "A nested item"
typeEnrichment = TypeEnrichment("nested") {
NestedComplexItem::name {
description = "A nested description"
}
}
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
fun Routing.enrichedComplexGenericType() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
MultiNestedGenerics<String, ComplexRequest>::content {
description = "Getting pretty crazy"
typeEnrichment = TypeEnrichment("nested") {
ComplexRequest::tables {
description = "A nested item"
typeEnrichment = TypeEnrichment("nested") {
NestedComplexItem::name {
description = "A nested description"
}
}
}
}
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}

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("basic") {
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

@ -0,0 +1,92 @@
{
"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/example",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com/example",
"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
},
"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"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,126 @@
{
"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": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestSimpleRequest-simple"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"TestSimpleRequest-simple": {
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "A simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,168 @@
{
"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": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ComplexRequest-simple"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"ComplexRequest-simple": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-nested"
},
"description": "A nested item",
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
},
"NestedComplexItem-nested": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string",
"description": "A nested description"
}
},
"required": [
"alias",
"name"
]
},
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"$ref": "#/components/schemas/SimpleEnum"
}
},
"required": [
"enumeration"
]
},
"SimpleEnum": {
"type": "string",
"enum": [
"ONE",
"TWO"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,183 @@
{
"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": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiNestedGenerics-String-ComplexRequest-simple"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"MultiNestedGenerics-String-ComplexRequest-simple": {
"type": "object",
"properties": {
"content": {
"additionalProperties": {
"$ref": "#/components/schemas/ComplexRequest-nested"
},
"description": "Getting pretty crazy",
"type": "object"
}
},
"required": [
"content"
]
},
"ComplexRequest-nested": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-nested"
},
"description": "A nested item",
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
},
"NestedComplexItem-nested": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string",
"description": "A nested description"
}
},
"required": [
"alias",
"name"
]
},
"CrazyItem": {
"type": "object",
"properties": {
"enumeration": {
"$ref": "#/components/schemas/SimpleEnum"
}
},
"required": [
"enumeration"
]
},
"SimpleEnum": {
"type": "string",
"enum": [
"ONE",
"TWO"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,73 @@
{
"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": {
"/enriched": {
"get": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"responses": {
"201": {
"description": "A good response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestResponse-simple"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse-simple": {
"type": "object",
"properties": {
"c": {
"type": "string",
"description": "A simple description"
}
},
"required": [
"c"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,80 @@
{
"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": "Get an int",
"description": "Get an int",
"parameters": [],
"responses": {
"200": {
"description": "An int",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse-example": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32",
"multipleOf": 2,
"maximum": 100,
"minimum": 2
}
},
"required": [
"c",
"id"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,76 @@
{
"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": "Get a double",
"description": "Get a double",
"parameters": [],
"responses": {
"200": {
"description": "A double",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DoubleResponse-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"DoubleResponse-example": {
"type": "object",
"properties": {
"payload": {
"type": "number",
"format": "double",
"multipleOf": 2.0,
"maximum": 100.0,
"minimum": 2.0
}
},
"required": [
"payload"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,74 @@
{
"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": "Get a string",
"description": "Get a string with constraints",
"parameters": [],
"responses": {
"200": {
"description": "A string",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestNested-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestNested-example": {
"type": "object",
"properties": {
"nesty": {
"type": "string",
"maxLength": 10,
"minLength": 2
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,73 @@
{
"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": "Get a string",
"description": "This is a description",
"parameters": [],
"responses": {
"200": {
"description": "A string",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestNested-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestNested-example": {
"type": "object",
"properties": {
"nesty": {
"type": "string",
"pattern": "[a-z]+"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,74 @@
{
"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": "Get a string",
"description": "This is a description",
"parameters": [],
"responses": {
"200": {
"description": "A string",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestNested-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestNested-example": {
"type": "object",
"properties": {
"nesty": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "image/png"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,103 @@
{
"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": "Get an array",
"description": "Get an array of strings",
"parameters": [],
"responses": {
"200": {
"description": "An array",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Page-String-example"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"Page-String-example": {
"type": "object",
"properties": {
"content": {
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 2,
"uniqueItems": true,
"type": "array"
},
"number": {
"type": "number",
"format": "int32"
},
"numberOfElements": {
"type": "number",
"format": "int32"
},
"size": {
"type": "number",
"format": "int32"
},
"totalElements": {
"type": "number",
"format": "int64"
},
"totalPages": {
"type": "number",
"format": "int32"
}
},
"required": [
"content",
"number",
"numberOfElements",
"size",
"totalElements",
"totalPages"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec import io.bkbn.kompendium.core.fixtures.TestSpecs.defaultSpec
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
@ -21,13 +22,14 @@ import io.ktor.serialization.gson.gson
import io.ktor.serialization.jackson.jackson import io.ktor.serialization.jackson.jackson
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication import io.ktor.server.testing.testApplication
import kotlin.reflect.KType
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import kotlinx.serialization.json.Json
import kotlin.reflect.KType
object TestHelpers { object TestHelpers {
private const val OPEN_API_ENDPOINT = "/openapi.json" private const val OPEN_API_ENDPOINT = "/openapi.json"
@ -43,8 +45,8 @@ object TestHelpers {
* exists as expected, and that the content matches the expected blob found in the specified file * exists as expected, and that the content matches the expected blob found in the specified file
* @param snapshotName The snapshot file to retrieve from the resources folder * @param snapshotName The snapshot file to retrieve from the resources folder
*/ */
private suspend fun ApplicationTestBuilder.compareOpenAPISpec(snapshotName: String) { private suspend fun ApplicationTestBuilder.compareOpenAPISpec(rootPath: String, snapshotName: String) {
val response = client.get(OPEN_API_ENDPOINT) val response = client.get("$rootPath$OPEN_API_ENDPOINT")
response shouldHaveStatus HttpStatusCode.OK response shouldHaveStatus HttpStatusCode.OK
response.bodyAsText() shouldNot beBlank() response.bodyAsText() shouldNot beBlank()
response.bodyAsText() shouldEqualJson getFileSnapshot(snapshotName) response.bodyAsText() shouldEqualJson getFileSnapshot(snapshotName)
@ -61,11 +63,36 @@ object TestHelpers {
customTypes: Map<KType, JsonSchema> = emptyMap(), customTypes: Map<KType, JsonSchema> = emptyMap(),
applicationSetup: Application.() -> Unit = { }, applicationSetup: Application.() -> Unit = { },
specOverrides: OpenApiSpec.() -> OpenApiSpec = { this }, specOverrides: OpenApiSpec.() -> OpenApiSpec = { this },
applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
routeUnderTest: Routing.() -> Unit routeUnderTest: Routing.() -> Unit
) { ) {
openApiTest(snapshotName, SupportedSerializer.KOTLINX, routeUnderTest, applicationSetup, specOverrides, customTypes) openApiTest(
openApiTest(snapshotName, SupportedSerializer.JACKSON, routeUnderTest, applicationSetup, specOverrides, customTypes) snapshotName,
openApiTest(snapshotName, SupportedSerializer.GSON, routeUnderTest, applicationSetup, specOverrides, customTypes) SupportedSerializer.KOTLINX,
routeUnderTest,
applicationSetup,
specOverrides,
customTypes,
applicationEnvironmentBuilder
)
openApiTest(
snapshotName,
SupportedSerializer.JACKSON,
routeUnderTest,
applicationSetup,
specOverrides,
customTypes,
applicationEnvironmentBuilder
)
openApiTest(
snapshotName,
SupportedSerializer.GSON,
routeUnderTest,
applicationSetup,
specOverrides,
customTypes,
applicationEnvironmentBuilder
)
} }
private fun openApiTest( private fun openApiTest(
@ -74,8 +101,10 @@ object TestHelpers {
routeUnderTest: Routing.() -> Unit, routeUnderTest: Routing.() -> Unit,
applicationSetup: Application.() -> Unit, applicationSetup: Application.() -> Unit,
specOverrides: OpenApiSpec.() -> OpenApiSpec, specOverrides: OpenApiSpec.() -> OpenApiSpec,
typeOverrides: Map<KType, JsonSchema> = emptyMap() typeOverrides: Map<KType, JsonSchema> = emptyMap(),
applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {}
) = testApplication { ) = testApplication {
environment(applicationBuilder)
install(NotarizedApplication()) { install(NotarizedApplication()) {
customTypes = typeOverrides customTypes = typeOverrides
spec = defaultSpec().specOverrides() spec = defaultSpec().specOverrides()
@ -102,9 +131,11 @@ object TestHelpers {
} }
application(applicationSetup) application(applicationSetup)
routing { routing {
swagger()
redoc() redoc()
routeUnderTest() routeUnderTest()
} }
compareOpenAPISpec(snapshotName) val root = ApplicationEngineEnvironmentBuilder().apply(applicationBuilder).rootPath
compareOpenAPISpec(root, snapshotName)
} }
} }

View File

@ -12,6 +12,9 @@ import java.time.Instant
@Serializable @Serializable
data class TestNested(val nesty: String) data class TestNested(val nesty: String)
@Serializable
data class DoubleResponse(val payload: Double)
@Serializable @Serializable
data class TestRequest( data class TestRequest(
val fieldName: TestNested, val fieldName: TestNested,
@ -163,7 +166,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 +179,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

@ -1,6 +1,10 @@
# Summary # Summary
* [Introduction](index.md) * [Introduction](index.md)
* [Helpers](helpers/index.md)
* [Protobuf java converter](helpers/protobuf_java_converter.md)
* [Concepts](concepts/index.md)
* [Enrichment](concepts/enrichment.md)
* [Plugins](plugins/index.md) * [Plugins](plugins/index.md)
* [Notarized Application](plugins/notarized_application.md) * [Notarized Application](plugins/notarized_application.md)
* [Notarized Route](plugins/notarized_route.md) * [Notarized Route](plugins/notarized_route.md)

106
docs/concepts/enrichment.md Normal file
View File

@ -0,0 +1,106 @@
Kompendium allows users to enrich their data types with additional information. This can be done by defining a
`TypeEnrichment` object and passing it to the `enrichment` parameter of the relevant `requestType` or `responseType`.
```kotlin
data class SimpleData(val a: String, val b: Int? = null)
val myEnrichment = TypeEnrichment<SimpleData>(id = "simple-enrichment") {
SimpleData::a {
description = "This will update the field description"
}
SimpleData::b {
// Will indicate in the UI that the field will be removed soon
deprecated = true
}
}
// In your route documentation
fun Routing.enrichedSimpleRequest() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType<SimpleData>(enrichment = myEnrichment) // Simply attach the enrichment to the request
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
```
{% hint style="warning" %}
An enrichment must provide an `id` field that is unique to the data class that is being enriched. This is because
under the hood, Kompendium appends this id to the data class identifier in order to support multiple different
enrichments
on the same data class.
If you provide duplicate ids, all but the first enrichment will be ignored, as Kompendium will view that as a cache hit,
and skip analyzing the new enrichment.
{% endhint %}
### Nested Enrichments
Enrichments are portable and composable, meaning that we can take an enrichment for a child data class
and apply it inside a parent data class using the `typeEnrichment` property.
```kotlin
data class ParentData(val a: String, val b: ChildData)
data class ChildData(val c: String, val d: Int? = null)
val childEnrichment = TypeEnrichment<ChildData>(id = "child-enrichment") {
ChildData::c {
description = "This will update the field description of field c on child data"
}
ChildData::d {
description = "This will update the field description of field d on child data"
}
}
val parentEnrichment = TypeEnrichment<ParentData>(id = "parent-enrichment") {
ParentData::a {
description = "This will update the field description"
}
ParentData::b {
description = "This will update the field description of field b on parent data"
typeEnrichment = childEnrichment // Will apply the child enrichment to the internals of field b
}
}
```
## Available Enrichments
All enrichments support the following properties:
- description -> Provides a reader friendly description of the field in the object
- deprecated -> Indicates that the field is deprecated and should not be used
### String
- minLength -> The minimum length of the string
- maxLength -> The maximum length of the string
- pattern -> A regex pattern that the string must match
- contentEncoding -> The encoding of the string
- contentMediaType -> The media type of the string
### Numbers
- minimum -> The minimum value of the number
- maximum -> The maximum value of the number
- exclusiveMinimum -> Indicates that the minimum value is exclusive
- exclusiveMaximum -> Indicates that the maximum value is exclusive
- multipleOf -> Indicates that the number must be a multiple of the provided value
### Arrays
- minItems -> The minimum number of items in the array
- maxItems -> The maximum number of items in the array
- uniqueItems -> Indicates that the array must contain unique items

2
docs/concepts/index.md Normal file
View File

@ -0,0 +1,2 @@
Various concepts that are core to Kompendium but not necessarily exclusive
to any given module or plugin

7
docs/helpers/index.md Normal file
View File

@ -0,0 +1,7 @@
Helper modules help you to interact with Kompendium.
Some functionality is not possible or difficult to do with Kompendium by default. Modules in this folder help you to get
functionality that would otherwise be difficult.
The first one of which is [Protobuf java converter](protobuf_java_converter.md) which translates java protobuf classes
to `customTypes` entries.

View File

@ -0,0 +1,153 @@
The `Protobuf java converter` functions allow you to generate documentation from your generated Java classes.
Since Kompendium relies a lot on `KProperties` we have yet to find a way to connect this with our Java.
For now the documentation is generated for the `customTypes` in `NotarizedApplication`.
## Usage with Kotlinx
setup:
```kotlin
install(ContentNegotiation) {
json(Json {
encodeDefaults = false
// Combine the kompendium serializers with your custom java proto serializers
serializersModule =
KompendiumSerializersModule.module + SerializersModule { serializersModule = yourCustomProtoSerializers }
})
}
```
For one message and all its nested sub messages:
```kotlin
private fun Application.mainModule() {
// ...
install(NotarizedApplication()) {
spec = baseSpec
customTypes = MyJavaProto.getDefaultInstance().createCustomTypesForTypeAndSubTypes().toMap()
}
}
```
For multiple messages and their submesages:
```kotlin
private fun Application.mainModule() {
// ...
install(NotarizedApplication()) {
spec = baseSpec
customTypes = MyJavaProto.getDefaultInstance().createCustomTypesForTypeAndSubTypes()
.plus(AnotherJavaProto.getDefaultInstance().createCustomTypesForTypeAndSubTypes()).toMap()
}
}
```
### Example User
The protobuf that is used on our endpoint
```proto
message User {
string id = 1;
string email = 2;
string mobile_phone = 3;
string name = 4;
}
```
A custom serializer deserializer:
```kotlin
@OptIn(ExperimentalSerializationApi::class)
object UserSerializer : KSerializer<User> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("User") {
element("id", serialDescriptor<String>())
element("email", serialDescriptor<String>())
element("mobile_phone", serialDescriptor<String>())
element("name", serialDescriptor<String>())
}
override fun deserialize(decoder: Decoder): User {
return decoder.decodeStructure(descriptor) {
var id: String? = null
var email: String? = null
var mobilePhone: String? = null
var name: String? = null
loop@ while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break@loop
0 -> id = decodeStringElement(descriptor, index)
1 -> email = decodeStringElement(descriptor, index)
2 -> mobilePhone = decodeStringElement(descriptor, index)
3 -> name = decodeStringElement(descriptor, index)
else -> throw RuntimeException(
"Unexpected index field ${descriptor.getElementName(index)}"
)
}
}
// building the protobuf object
val user = User.newBuilder().apply {
id?.let { v -> this.id = v }
email?.let { v -> this.email = v }
mobilePhone?.let { v -> this.mobilePhone = v }
name?.let { v -> this.name = v }
}.build()
user
}
}
override fun serialize(encoder: Encoder, value: User) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.id)
encodeStringElement(descriptor, 1, value.email)
encodeStringElement(descriptor, 2, value.mobilePhone)
encodeStringElement(descriptor, 3, value.name)
}
}
}
```
Setting the content type:
```kotlin
install(ContentNegotiation) {
json(Json {
encodeDefaults = false
// Combine the kompendium serializers with your custom java proto serializers
serializersModule =
KompendiumSerializersModule.module + SerializersModule {
serializersModule = SerializersModule {
contextual(UserSerializer)
}
}
})
}
```
The installation of the noterized application:
```kotlin
install(NotarizedApplication()) {
spec = baseSpec
customTypes = User.getDefaultInstance().createCustomTypesForTypeAndSubTypes().toMap()
}
```
Route configuration as you normally would with one exception which is `createType()` to create kotlin type from a javaClass.
```kotlin
private fun Route.userDocumentation() {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary("My User API")
description("Create a user")
request {
requestType(User::class.createType())
description("My user creation object")
}
response {
responseCode(HttpStatusCode.OK)
responseType(CreateUserResponse::class.createType())
description("Returns simulation object")
}
canRespond {
responseCode(HttpStatusCode.NotFound)
responseType<String>()
description("Indicates that the user could not be found")
}
}
}
}
```

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

@ -0,0 +1,34 @@
plugins {
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm")
id("io.gitlab.arturbosch.detekt")
id("com.adarshr.test-logger")
id("maven-publish")
id("java-library")
id("signing")
id("org.jetbrains.kotlinx.kover")
}
sourdoughLibrary {
libraryName.set("Kompendium Type Enrichment")
libraryDescription.set("Utility library for creating portable type enrichments")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
// Versions
val detektVersion: String by project
// Formatting
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
testImplementation(testFixtures(projects.kompendiumCore))
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.enrichment
sealed interface Enrichment

View File

@ -0,0 +1,36 @@
package io.bkbn.kompendium.enrichment
/**
* Reference https://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof
*/
class PropertyEnrichment : Enrichment {
// Metadata
var deprecated: Boolean? = null
var description: String? = null
var typeEnrichment: TypeEnrichment<*>? = null
// Number and Integer Constraints
var multipleOf: Number? = null
var maximum: Number? = null
var exclusiveMaximum: Number? = null
var minimum: Number? = null
var exclusiveMinimum: Number? = null
// String constraints
var maxLength: Int? = null
var minLength: Int? = null
var pattern: String? = null
var contentEncoding: String? = null
var contentMediaType: String? = null
// TODO how to handle contentSchema?
// Array constraints
var maxItems: Int? = null
var minItems: Int? = null
var uniqueItems: Boolean? = null
// TODO How to handle contains, minContains, maxContains?
// Object constraints
var maxProperties: Int? = null
var minProperties: Int? = null
}

View File

@ -0,0 +1,25 @@
package io.bkbn.kompendium.enrichment
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
class TypeEnrichment<T>(val id: String) : Enrichment {
private val enrichments: MutableMap<KProperty1<*, *>, Enrichment> = mutableMapOf()
fun getEnrichmentForProperty(property: KProperty<*>): Enrichment? = enrichments[property]
operator fun <R> KProperty1<T, R>.invoke(init: PropertyEnrichment.() -> Unit) {
require(!enrichments.containsKey(this)) { "${this.name} has already been registered" }
val propertyEnrichment = PropertyEnrichment()
init.invoke(propertyEnrichment)
enrichments[this] = propertyEnrichment
}
companion object {
inline operator fun <reified T> invoke(id: String, init: TypeEnrichment<T>.() -> Unit): TypeEnrichment<T> {
val builder = TypeEnrichment<T>(id)
return builder.apply(init)
}
}
}

View File

@ -1,13 +1,14 @@
# Kompendium # Kompendium
project.version=3.5.0 project.version=3.12.0
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle
org.gradle.vfs.watch=true org.gradle.vfs.watch=true
org.gradle.vfs.verbose=true org.gradle.vfs.verbose=true
org.gradle.jvmargs=-Xmx2000m org.gradle.jvmargs=-Xmx2000m
org.gradle.parallel=true
# Dependencies # Dependencies
ktorVersion=2.1.3 ktorVersion=2.2.3
kotestVersion=5.5.3 kotestVersion=5.5.5
detektVersion=1.21.0 detektVersion=1.21.0

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

6
gradlew vendored
View File

@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

14
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

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

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.enrichment.TypeEnrichment
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
@ -9,28 +10,26 @@ import io.bkbn.kompendium.json.schema.handler.EnumHandler
import io.bkbn.kompendium.json.schema.handler.MapHandler import io.bkbn.kompendium.json.schema.handler.MapHandler
import io.bkbn.kompendium.json.schema.handler.SealedObjectHandler import io.bkbn.kompendium.json.schema.handler.SealedObjectHandler
import io.bkbn.kompendium.json.schema.handler.SimpleObjectHandler import io.bkbn.kompendium.json.schema.handler.SimpleObjectHandler
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import java.util.UUID
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.typeOf
import java.util.UUID
object SchemaGenerator { object SchemaGenerator {
inline fun <reified T : Any?> fromTypeToSchema(
cache: MutableMap<String, JsonSchema> = mutableMapOf(),
schemaConfigurator: SchemaConfigurator = SchemaConfigurator.Default()
) = fromTypeToSchema(typeOf<T>(), cache, schemaConfigurator)
fun fromTypeToSchema( fun fromTypeToSchema(
type: KType, type: KType,
cache: MutableMap<String, JsonSchema>, cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema { ): JsonSchema {
cache[type.getSimpleSlug()]?.let { val slug = type.getSlug(enrichment)
cache[slug]?.let {
return it return it
} }
return when (val clazz = type.classifier as KClass<*>) { return when (val clazz = type.classifier as KClass<*>) {
Unit::class -> error( Unit::class -> error(
""" """
@ -48,14 +47,14 @@ 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, enrichment)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator) clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator, enrichment)
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator) clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator, enrichment)
else -> { else -> {
if (clazz.isSealed) { if (clazz.isSealed) {
SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator) SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
} else { } else {
SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator) SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
} }
} }
} }
@ -65,11 +64,12 @@ object SchemaGenerator {
fun fromTypeOrUnit( fun fromTypeOrUnit(
type: KType, type: KType,
cache: MutableMap<String, JsonSchema> = mutableMapOf(), cache: MutableMap<String, JsonSchema> = mutableMapOf(),
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema? = ): JsonSchema? =
when (type.classifier as KClass<*>) { when (type.classifier as KClass<*>) {
Unit::class -> null Unit::class -> null
else -> fromTypeToSchema(type, cache, schemaConfigurator) else -> fromTypeToSchema(type, cache, schemaConfigurator, enrichment)
} }
private fun checkForNull(type: KType, schema: JsonSchema): JsonSchema = when (type.isMarkedNullable) { private fun checkForNull(type: KType, schema: JsonSchema): JsonSchema = when (type.isMarkedNullable) {

View File

@ -3,4 +3,8 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class AnyOfDefinition(val anyOf: Set<JsonSchema>) : JsonSchema data class AnyOfDefinition(
val anyOf: Set<JsonSchema>,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -4,7 +4,14 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ArrayDefinition( data class ArrayDefinition(
val items: JsonSchema val items: JsonSchema,
override val deprecated: Boolean? = null,
override val description: String? = null,
// Constraints
val maxItems: Int? = null,
val minItems: Int? = null,
val uniqueItems: Boolean? = null,
) : JsonSchema { ) : JsonSchema {
val type: String = "array" val type: String = "array"
} }

View File

@ -4,5 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class EnumDefinition( data class EnumDefinition(
val enum: Set<String> val type: String,
val enum: Set<String>,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema ) : JsonSchema

View File

@ -11,6 +11,9 @@ import kotlinx.serialization.encoding.Encoder
@Serializable(with = JsonSchema.Serializer::class) @Serializable(with = JsonSchema.Serializer::class)
sealed interface JsonSchema { sealed interface JsonSchema {
val description: String?
val deprecated: Boolean?
object Serializer : KSerializer<JsonSchema> { object Serializer : KSerializer<JsonSchema> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JsonSchema", PrimitiveKind.STRING) override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JsonSchema", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): JsonSchema { override fun deserialize(decoder: Decoder): JsonSchema {

View File

@ -4,7 +4,9 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class MapDefinition( data class MapDefinition(
val additionalProperties: JsonSchema val additionalProperties: JsonSchema,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema { ) : JsonSchema {
val type: String = "object" val type: String = "object"
} }

View File

@ -3,4 +3,8 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class NullableDefinition(val type: String = "null") : JsonSchema data class NullableDefinition(
val type: String = "null",
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -3,6 +3,10 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class OneOfDefinition(val oneOf: Set<JsonSchema>) : JsonSchema { data class OneOfDefinition(
val oneOf: Set<JsonSchema>,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema {
constructor(vararg types: JsonSchema) : this(types.toSet()) constructor(vararg types: JsonSchema) : this(types.toSet())
} }

View File

@ -3,4 +3,8 @@ package io.bkbn.kompendium.json.schema.definition
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ReferenceDefinition(val `$ref`: String) : JsonSchema data class ReferenceDefinition(
val `$ref`: String,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.definition package io.bkbn.kompendium.json.schema.definition
import io.bkbn.kompendium.json.schema.util.Serializers
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -7,10 +8,35 @@ import kotlinx.serialization.Serializable
data class TypeDefinition( data class TypeDefinition(
val type: String, val type: String,
val format: String? = null, val format: String? = null,
val description: String? = null,
val properties: Map<String, JsonSchema>? = null, val properties: Map<String, JsonSchema>? = null,
val required: Set<String>? = null, val required: Set<String>? = null,
@Contextual val default: Any? = null, @Contextual val default: Any? = null,
override val deprecated: Boolean? = null,
override val description: String? = null,
// Constraints
// Number
@Serializable(with = Serializers.Number::class)
val multipleOf: Number? = null,
@Serializable(with = Serializers.Number::class)
val maximum: Number? = null,
@Serializable(with = Serializers.Number::class)
val exclusiveMaximum: Number? = null,
@Serializable(with = Serializers.Number::class)
val minimum: Number? = null,
@Serializable(with = Serializers.Number::class)
val exclusiveMinimum: Number? = null,
// String
val maxLength: Int? = null,
val minLength: Int? = null,
val pattern: String? = null,
val contentEncoding: String? = null,
val contentMediaType: String? = null,
// Object
val maxProperties: Int? = null,
val minProperties: Int? = null,
) : JsonSchema { ) : JsonSchema {
fun withDefault(default: Any): TypeDefinition = this.copy(default = default) fun withDefault(default: Any): TypeDefinition = this.copy(default = default)

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.handler package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.ArrayDefinition import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
@ -9,17 +10,22 @@ 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.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KType import kotlin.reflect.KType
object CollectionHandler { object CollectionHandler {
fun handle(type: KType, cache: MutableMap<String, JsonSchema>, schemaConfigurator: SchemaConfigurator): JsonSchema { fun handle(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
val collectionType = type.arguments.first().type val collectionType = type.arguments.first().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!")
val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator).let { val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator, enrichment).let {
if (it is TypeDefinition && it.type == "object") { if (it is TypeDefinition && it.type == "object") {
cache[collectionType.getSimpleSlug()] = it cache[collectionType.getSlug(enrichment)] = it
ReferenceDefinition(collectionType.getReferenceSlug()) ReferenceDefinition(collectionType.getReferenceSlug(enrichment))
} else { } else {
it it
} }

View File

@ -1,19 +1,24 @@
package io.bkbn.kompendium.json.schema.handler package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.getSlug
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>,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
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

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.handler package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.JsonSchema import io.bkbn.kompendium.json.schema.definition.JsonSchema
@ -9,21 +10,26 @@ 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.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
object MapHandler { object MapHandler {
fun handle(type: KType, cache: MutableMap<String, JsonSchema>, schemaConfigurator: SchemaConfigurator): JsonSchema { fun handle(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
require(type.arguments.first().type?.classifier as KClass<*> == String::class) { require(type.arguments.first().type?.classifier as KClass<*> == String::class) {
"JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}" "JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}"
} }
val valueType = type.arguments[1].type ?: error("this indicates a bug in Kompendium, please open a GitHub issue") val valueType = type.arguments[1].type ?: error("this indicates a bug in Kompendium, please open a GitHub issue")
val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator).let { val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator, enrichment).let {
if (it is TypeDefinition && it.type == "object") { if (it is TypeDefinition && it.type == "object") {
cache[valueType.getSimpleSlug()] = it cache[valueType.getSlug(enrichment)] = it
ReferenceDefinition(valueType.getReferenceSlug()) ReferenceDefinition(valueType.getReferenceSlug(enrichment))
} else { } else {
it it
} }

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.json.schema.handler package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.AnyOfDefinition import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
@ -7,7 +8,7 @@ import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
@ -18,15 +19,17 @@ object SealedObjectHandler {
type: KType, type: KType,
clazz: KClass<*>, clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>, cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null,
): JsonSchema { ): JsonSchema {
val subclasses = clazz.sealedSubclasses val subclasses = clazz.sealedSubclasses
.map { it.createType(type.arguments) } .map { it.createType(type.arguments) }
.map { t -> .map { t ->
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator).let { js -> SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment).let { js ->
if (js is TypeDefinition && js.type == "object") { if (js is TypeDefinition && js.type == "object") {
cache[t.getSimpleSlug()] = js val slug = t.getSlug(enrichment)
ReferenceDefinition(t.getReferenceSlug()) cache[slug] = js
ReferenceDefinition(t.getReferenceSlug(enrichment))
} else { } else {
js js
} }

View File

@ -1,15 +1,21 @@
package io.bkbn.kompendium.json.schema.handler package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.PropertyEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
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.AnyOfDefinition
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
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.MapDefinition
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
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
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.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.getSlug
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
@ -25,26 +31,34 @@ object SimpleObjectHandler {
type: KType, type: KType,
clazz: KClass<*>, clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>, cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>?,
): JsonSchema { ): JsonSchema {
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug()) cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
val typeMap = clazz.typeParameters.zip(type.arguments).toMap() val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
val props = schemaConfigurator.serializableMemberProperties(clazz) val props = schemaConfigurator.serializableMemberProperties(clazz)
.filterNot { it.javaField == null } .filterNot { it.javaField == null }
.associate { prop -> .associate { prop ->
val propTypeEnrichment = when (val pe = enrichment?.getEnrichmentForProperty(prop)) {
is PropertyEnrichment -> pe
else -> null
}
val schema = when (prop.needsToInjectGenerics(typeMap)) { val schema = when (prop.needsToInjectGenerics(typeMap)) {
true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator) true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator, propTypeEnrichment)
false -> when (typeMap.containsKey(prop.returnType.classifier)) { false -> when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator) true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator, propTypeEnrichment)
false -> handleProperty(prop, cache, schemaConfigurator) false -> handleProperty(prop, cache, schemaConfigurator, propTypeEnrichment?.typeEnrichment)
} }
} }
val nullCheckSchema = when (prop.returnType.isMarkedNullable && !schema.isNullable()) { val enrichedSchema = propTypeEnrichment?.applyToSchema(schema) ?: schema
true -> OneOfDefinition(NullableDefinition(), schema)
false -> schema val nullCheckSchema = when (prop.returnType.isMarkedNullable && !enrichedSchema.isNullable()) {
true -> OneOfDefinition(NullableDefinition(), enrichedSchema)
false -> enrichedSchema
} }
schemaConfigurator.serializableName(prop) to nullCheckSchema schemaConfigurator.serializableName(prop) to nullCheckSchema
@ -71,16 +85,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(
@ -94,7 +103,8 @@ object SimpleObjectHandler {
typeMap: Map<KTypeParameter, KTypeProjection>, typeMap: Map<KTypeParameter, KTypeProjection>,
prop: KProperty<*>, prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>, cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
propEnrichment: PropertyEnrichment?
): JsonSchema { ): JsonSchema {
val propClass = prop.returnType.classifier as KClass<*> val propClass = prop.returnType.classifier as KClass<*>
val types = prop.returnType.arguments.map { val types = prop.returnType.arguments.map {
@ -102,10 +112,11 @@ object SimpleObjectHandler {
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first() typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
} }
val constructedType = propClass.createType(types) val constructedType = propClass.createType(types)
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let { return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator, propEnrichment?.typeEnrichment)
if (it.isOrContainsObjectDef()) { .let {
cache[constructedType.getSimpleSlug()] = it if (it.isOrContainsObjectOrEnumDef()) {
ReferenceDefinition(prop.returnType.getReferenceSlug()) cache[constructedType.getSlug(propEnrichment)] = it
ReferenceDefinition(prop.returnType.getReferenceSlug(propEnrichment))
} else { } else {
it it
} }
@ -116,14 +127,15 @@ object SimpleObjectHandler {
prop: KProperty<*>, prop: KProperty<*>,
typeMap: Map<KTypeParameter, KTypeProjection>, typeMap: Map<KTypeParameter, KTypeProjection>,
cache: MutableMap<String, JsonSchema>, cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
propEnrichment: PropertyEnrichment?
): JsonSchema { ): JsonSchema {
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, propEnrichment?.typeEnrichment).let {
if (it.isOrContainsObjectDef()) { if (it.isOrContainsObjectOrEnumDef()) {
cache[type.getSimpleSlug()] = it cache[type.getSlug(propEnrichment)] = it
ReferenceDefinition(type.getReferenceSlug()) ReferenceDefinition(type.getReferenceSlug(propEnrichment))
} else { } else {
it it
} }
@ -133,22 +145,57 @@ object SimpleObjectHandler {
private fun handleProperty( private fun handleProperty(
prop: KProperty<*>, prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>, cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator schemaConfigurator: SchemaConfigurator,
propEnrichment: TypeEnrichment<*>?
): JsonSchema = ): JsonSchema =
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let { SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator, propEnrichment).let {
if (it.isOrContainsObjectDef()) { if (it.isOrContainsObjectOrEnumDef()) {
cache[prop.returnType.getSimpleSlug()] = it cache[prop.returnType.getSlug(propEnrichment)] = it
ReferenceDefinition(prop.returnType.getReferenceSlug()) ReferenceDefinition(prop.returnType.getReferenceSlug(propEnrichment))
} else { } else {
it it
} }
} }
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 }
private fun PropertyEnrichment.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is AnyOfDefinition -> schema.copy(deprecated = deprecated, description = description)
is ArrayDefinition -> schema.copy(
deprecated = deprecated,
description = description,
minItems = minItems,
maxItems = maxItems,
uniqueItems = uniqueItems,
)
is EnumDefinition -> schema.copy(deprecated = deprecated, description = description)
is MapDefinition -> schema.copy(deprecated = deprecated, description = description)
is NullableDefinition -> schema.copy(deprecated = deprecated, description = description)
is OneOfDefinition -> schema.copy(deprecated = deprecated, description = description)
is ReferenceDefinition -> schema.copy(deprecated = deprecated, description = description)
is TypeDefinition -> schema.copy(
deprecated = deprecated,
description = description,
multipleOf = multipleOf,
maximum = maximum,
exclusiveMaximum = exclusiveMaximum,
minimum = minimum,
exclusiveMinimum = exclusiveMinimum,
maxLength = maxLength,
minLength = minLength,
pattern = pattern,
contentEncoding = contentEncoding,
contentMediaType = contentMediaType,
maxProperties = maxProperties,
minProperties = minProperties,
)
}
} }

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.json.schema.util package io.bkbn.kompendium.json.schema.util
import io.bkbn.kompendium.enrichment.Enrichment
import io.bkbn.kompendium.enrichment.PropertyEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
@ -7,12 +10,26 @@ object Helpers {
private const val COMPONENT_SLUG = "#/components/schemas" private const val COMPONENT_SLUG = "#/components/schemas"
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
is TypeEnrichment<*> -> getEnrichedSlug(enrichment)
is PropertyEnrichment -> error("Slugs should not be generated for field enrichments")
null -> getSimpleSlug()
}
fun KType.getSimpleSlug(): String = when { fun KType.getSimpleSlug(): String = when {
this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>) this.arguments.isNotEmpty() -> genericNameAdapter(this, classifier as KClass<*>)
else -> (classifier as KClass<*>).kompendiumSlug() ?: error("Could not determine simple name for $this") else -> (classifier as KClass<*>).kompendiumSlug() ?: error("Could not determine simple name for $this")
} }
fun KType.getReferenceSlug(): String = when { private fun KType.getEnrichedSlug(enrichment: TypeEnrichment<*>) = getSimpleSlug() + "-${enrichment.id}"
fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) {
is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}"
is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments")
null -> getSimpleReferenceSlug()
}
private fun KType.getSimpleReferenceSlug() = when {
arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}" arguments.isNotEmpty() -> "$COMPONENT_SLUG/${genericNameAdapter(this, classifier as KClass<*>)}"
else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).kompendiumSlug()}" else -> "$COMPONENT_SLUG/${(classifier as KClass<*>).kompendiumSlug()}"
} }

View File

@ -0,0 +1,43 @@
package io.bkbn.kompendium.json.schema.util
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.UUID
import kotlin.Number as KNumber
object Serializers {
object Uuid : KSerializer<UUID> {
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID {
return UUID.fromString(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}
object Number : KSerializer<KNumber> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Number", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): KNumber {
TODO("Not yet implemented")
}
override fun serialize(encoder: Encoder, value: KNumber) {
when (value) {
is Int -> encoder.encodeInt(value)
is Long -> encoder.encodeLong(value)
is Double -> encoder.encodeDouble(value)
is Float -> encoder.encodeFloat(value)
else -> throw IllegalArgumentException("Number is not a valid type")
}
}
}
}

View File

@ -2,6 +2,8 @@ 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.NestedComplexItem
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,13 +11,15 @@ 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.enrichment.TypeEnrichment
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
import io.kotest.core.spec.style.DescribeSpec import io.kotest.core.spec.style.DescribeSpec
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
import kotlin.reflect.typeOf
class SchemaGeneratorTest : DescribeSpec({ class SchemaGeneratorTest : DescribeSpec({
describe("Scalars") { describe("Scalars") {
@ -40,6 +44,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 +57,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 +68,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") {
@ -82,7 +91,13 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<Map<String, Int>>("T0012__scalar_map.json") jsonSchemaTest<Map<String, Int>>("T0012__scalar_map.json")
} }
it("Throws an error when map keys are not strings") { it("Throws an error when map keys are not strings") {
shouldThrow<IllegalArgumentException> { SchemaGenerator.fromTypeToSchema<Map<Int, Int>>() } shouldThrow<IllegalArgumentException> {
SchemaGenerator.fromTypeToSchema(
typeOf<Map<Int, Int>>(),
cache = mutableMapOf(),
schemaConfigurator = KotlinXSchemaConfigurator()
)
}
} }
it("Can generate the schema for a map of objects") { it("Can generate the schema for a map of objects") {
jsonSchemaTest<Map<String, TestResponse>>("T0013__object_map.json") jsonSchemaTest<Map<String, TestResponse>>("T0013__object_map.json")
@ -91,6 +106,36 @@ class SchemaGeneratorTest : DescribeSpec({
jsonSchemaTest<Map<String, Int>?>("T0014__nullable_map.json") jsonSchemaTest<Map<String, Int>?>("T0014__nullable_map.json")
} }
} }
describe("Enrichment") {
it("Can attach an enrichment to a simple type") {
jsonSchemaTest<TestSimpleRequest>(
snapshotName = "T0022__enriched_simple_object.json",
enrichment = TypeEnrichment("simple") {
TestSimpleRequest::a {
description = "This is a simple description"
}
TestSimpleRequest::b {
deprecated = true
}
}
)
}
it("Can properly assign a reference to a nested enrichment") {
jsonSchemaTest<ComplexRequest>(
snapshotName = "T0023__enriched_nested_reference.json",
enrichment = TypeEnrichment("example") {
ComplexRequest::tables {
description = "Collection of important items"
typeEnrichment = TypeEnrichment("table") {
NestedComplexItem::name {
description = "The name of the table"
}
}
}
}
)
}
}
}) { }) {
companion object { companion object {
private val json = Json { private val json = Json {
@ -101,11 +146,14 @@ class SchemaGeneratorTest : DescribeSpec({
private fun JsonSchema.serialize() = json.encodeToString(JsonSchema.serializer(), this) private fun JsonSchema.serialize() = json.encodeToString(JsonSchema.serializer(), this)
private inline fun <reified T> jsonSchemaTest(snapshotName: String) { private inline fun <reified T> jsonSchemaTest(snapshotName: String, enrichment: TypeEnrichment<*>? = null) {
// act // act
val schema = SchemaGenerator.fromTypeToSchema<T>(schemaConfigurator = KotlinXSchemaConfigurator()) val schema = SchemaGenerator.fromTypeToSchema(
type = typeOf<T>(),
// todo add cache assertions!!! cache = mutableMapOf(),
schemaConfigurator = KotlinXSchemaConfigurator(),
enrichment = enrichment,
)
// assert // assert
schema.serialize() shouldEqualJson getFileSnapshot(snapshotName) schema.serialize() shouldEqualJson getFileSnapshot(snapshotName)

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

@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "This is a simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
}

View File

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

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.2.3")
implementation("io.ktor:ktor-server-locations:2.1.2") implementation("io.ktor:ktor-server-locations:2.2.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

@ -21,6 +21,7 @@ dependencies {
val detektVersion: String by project val detektVersion: String by project
api(projects.kompendiumJsonSchema) api(projects.kompendiumJsonSchema)
api(projects.kompendiumEnrichment)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
// Formatting // Formatting

View File

@ -15,6 +15,7 @@ dependencies {
implementation(projects.kompendiumCore) implementation(projects.kompendiumCore)
implementation(projects.kompendiumLocations) implementation(projects.kompendiumLocations)
implementation(projects.kompendiumResources) implementation(projects.kompendiumResources)
implementation(projects.kompendiumProtobufJavaConverter)
// Ktor // Ktor
val ktorVersion: String by project val ktorVersion: String by project
@ -34,14 +35,14 @@ dependencies {
// Logging // Logging
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.2.0") implementation("org.apache.logging.log4j:log4j-api-kotlin:1.2.0")
implementation("org.apache.logging.log4j:log4j-api:2.19.0") implementation("org.apache.logging.log4j:log4j-api:2.20.0")
implementation("org.apache.logging.log4j:log4j-core:2.19.0") implementation("org.apache.logging.log4j:log4j-core:2.20.0")
implementation("org.slf4j:slf4j-api:2.0.3") implementation("org.slf4j:slf4j-api:2.0.6")
implementation("org.slf4j:slf4j-simple:2.0.3") implementation("org.slf4j:slf4j-simple:2.0.6")
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.2")
} }

View File

@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.component.Components import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
@ -68,6 +69,7 @@ private fun Application.mainModule() {
) )
} }
routing { routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
authenticate("basic") { authenticate("basic") {
route("/{id}") { route("/{id}") {

View File

@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
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
@ -49,8 +50,8 @@ private fun Application.mainModule() {
schemaConfigurator = KotlinXSchemaConfigurator() schemaConfigurator = KotlinXSchemaConfigurator()
} }
routing { routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
idDocumentation() idDocumentation()
get { get {

View File

@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
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.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
@ -51,6 +52,7 @@ private fun Application.mainModule() {
) )
} }
routing { routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {

View File

@ -0,0 +1,158 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.util.ExampleRequest
import io.bkbn.kompendium.playground.util.ExampleResponse
import io.bkbn.kompendium.playground.util.ExceptionResponse
import io.bkbn.kompendium.playground.util.InnerRequest
import io.bkbn.kompendium.playground.util.Util.baseSpec
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.*
import kotlinx.serialization.json.Json
fun main() {
embeddedServer(
CIO,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
json(Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
})
}
install(NotarizedApplication()) {
spec = baseSpec
// Adds support for @Transient and @SerialName
// If you are not using them this is not required.
schemaConfigurator = KotlinXSchemaConfigurator()
}
routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs")
enrichedDocumentation()
post {
call.respond(HttpStatusCode.OK, ExampleResponse(false))
}
}
}
private val testEnrichment = TypeEnrichment("testerino") {
ExampleRequest::thingA {
description = "This is a thing"
}
ExampleRequest::thingB {
description = "This is another thing"
}
ExampleRequest::thingC {
deprecated = true
description = "A good but old field"
typeEnrichment = TypeEnrichment("big-tings") {
InnerRequest::d {
exclusiveMaximum = 10.0
exclusiveMinimum = 1.1
description = "THE BIG D"
}
}
}
}
private val testResponseEnrichment = TypeEnrichment("testerino") {
ExampleResponse::isReal {
description = "Is this thing real or not?"
}
}
private fun Route.enrichedDocumentation() {
install(NotarizedRoute()) {
post = PostInfo.builder {
summary("Do a thing")
description("This is a thing")
request {
requestType(enrichment = testEnrichment)
description("This is the request")
}
response {
responseCode(HttpStatusCode.OK)
responseType(enrichment = testResponseEnrichment)
description("This is the response")
}
}
}
}
private fun Route.idDocumentation() {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
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 😱")
}
canRespond {
responseType<ExceptionResponse>()
responseCode(HttpStatusCode.NotFound)
description("Indicates that a user with this id does not exist")
}
}
}
}
private fun Route.profileDocumentation() {
install(NotarizedRoute()) {
parameters = listOf(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING
)
)
get = GetInfo.builder {
summary("Get a users profile")
description("A cool endpoint!")
response {
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Returns user profile information")
}
canRespond {
responseType<ExceptionResponse>()
responseCode(HttpStatusCode.NotFound)
description("Indicates that a user with this id does not exist")
}
}
}
}

View File

@ -4,6 +4,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
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.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
@ -50,6 +51,7 @@ private fun Application.mainModule() {
} }
} }
routing { routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {

View File

@ -6,6 +6,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.SchemaConfigurator
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
@ -48,6 +49,7 @@ private fun Application.mainModule() {
schemaConfigurator = GsonSchemaConfigurator() schemaConfigurator = GsonSchemaConfigurator()
} }
routing { routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {

View File

@ -5,6 +5,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.component.Components import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
@ -80,6 +81,7 @@ private fun Application.mainModule() {
} }
routing { routing {
authenticate("basic") { authenticate("basic") {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {
locationDocumentation() locationDocumentation()

View File

@ -8,6 +8,7 @@ import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.json.schema.SchemaConfigurator import io.bkbn.kompendium.json.schema.SchemaConfigurator
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
@ -51,6 +52,7 @@ private fun Application.mainModule() {
schemaConfigurator = JacksonSchemaConfigurator() schemaConfigurator = JacksonSchemaConfigurator()
} }
routing { routing {
swagger(pageTitle = "Simple API Docs")
redoc(pageTitle = "Simple API Docs") redoc(pageTitle = "Simple API Docs")
route("/{id}") { route("/{id}") {

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