feat: type enrichment (#408)
This commit is contained in:
@ -12,6 +12,12 @@
|
|||||||
|
|
||||||
## Released
|
## Released
|
||||||
|
|
||||||
|
## [3.10.0] - January 4th, 2023
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for type enrichments! `deprecated` and `description` to start
|
||||||
|
|
||||||
## [3.9.0] - November 15th, 2022
|
## [3.9.0] - November 15th, 2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.7.22" apply false
|
kotlin("jvm") version "1.8.0" apply false
|
||||||
kotlin("plugin.serialization") version "1.7.22" apply false
|
kotlin("plugin.serialization") version "1.8.0" 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"
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
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>
|
val mediaTypes: Set<String>
|
||||||
@ -21,6 +23,7 @@ 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
|
private var mediaTypes: Set<String>? = null
|
||||||
@ -29,7 +32,14 @@ class RequestInfo private constructor(
|
|||||||
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 }
|
||||||
|
|
||||||
@ -44,6 +54,7 @@ class RequestInfo private constructor(
|
|||||||
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"),
|
||||||
|
typeEnrichment = typeEnrichment,
|
||||||
examples = examples,
|
examples = examples,
|
||||||
mediaTypes = mediaTypes ?: setOf("application/json")
|
mediaTypes = mediaTypes ?: setOf("application/json")
|
||||||
)
|
)
|
||||||
|
@ -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,6 +9,7 @@ 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>
|
val mediaTypes: Set<String>
|
||||||
@ -24,6 +26,7 @@ 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
|
private var mediaTypes: Set<String>? = null
|
||||||
@ -36,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 }
|
||||||
|
|
||||||
@ -52,6 +62,7 @@ class ResponseInfo private constructor(
|
|||||||
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!"),
|
||||||
|
typeEnrichment = typeEnrichment,
|
||||||
examples = examples,
|
examples = examples,
|
||||||
mediaTypes = mediaTypes ?: setOf("application/json")
|
mediaTypes = mediaTypes ?: setOf("application/json")
|
||||||
)
|
)
|
||||||
|
@ -10,13 +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.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.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
|
||||||
@ -50,26 +51,34 @@ object Helpers {
|
|||||||
authMethods: List<String> = emptyList()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,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, this.request.mediaTypes),
|
content = this.request.requestType.toReferenceContent(
|
||||||
|
examples = this.request.examples,
|
||||||
|
mediaTypes = this.request.mediaTypes,
|
||||||
|
enrichment = this.request.typeEnrichment
|
||||||
|
),
|
||||||
required = true
|
required = true
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -123,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, this.response.mediaTypes)
|
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())
|
||||||
)
|
)
|
||||||
@ -131,22 +148,31 @@ 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, error.mediaTypes)
|
content = error.responseType.toReferenceContent(
|
||||||
|
examples = error.examples,
|
||||||
|
mediaTypes = error.mediaTypes,
|
||||||
|
enrichment = error.typeEnrichment
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KType.toReferenceContent(
|
private fun KType.toReferenceContent(
|
||||||
examples: Map<String, MediaType.Example>?,
|
examples: Map<String, MediaType.Example>?,
|
||||||
mediaTypes: Set<String>
|
mediaTypes: Set<String>,
|
||||||
|
enrichment: TypeEnrichment<*>?
|
||||||
): Map<String, MediaType>? =
|
): Map<String, MediaType>? =
|
||||||
when (this.classifier as KClass<*>) {
|
when (this.classifier as KClass<*>) {
|
||||||
Unit::class -> null
|
Unit::class -> null
|
||||||
else -> mediaTypes.associateWith {
|
else -> mediaTypes.associateWith {
|
||||||
MediaType(
|
MediaType(
|
||||||
schema = if (this.isMarkedNullable) OneOfDefinition(
|
schema = if (this.isMarkedNullable) {
|
||||||
|
OneOfDefinition(
|
||||||
NullableDefinition(),
|
NullableDefinition(),
|
||||||
ReferenceDefinition(this.getReferenceSlug())
|
ReferenceDefinition(this.getReferenceSlug(enrichment))
|
||||||
) else ReferenceDefinition(this.getReferenceSlug()),
|
)
|
||||||
|
} else {
|
||||||
|
ReferenceDefinition(this.getReferenceSlug(enrichment))
|
||||||
|
},
|
||||||
examples = examples
|
examples = examples
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ import io.bkbn.kompendium.core.util.dateTimeString
|
|||||||
import io.bkbn.kompendium.core.util.defaultAuthConfig
|
import io.bkbn.kompendium.core.util.defaultAuthConfig
|
||||||
import io.bkbn.kompendium.core.util.defaultField
|
import io.bkbn.kompendium.core.util.defaultField
|
||||||
import io.bkbn.kompendium.core.util.defaultParameter
|
import io.bkbn.kompendium.core.util.defaultParameter
|
||||||
|
import io.bkbn.kompendium.core.util.enrichedComplexGenericType
|
||||||
|
import io.bkbn.kompendium.core.util.enrichedNestedCollection
|
||||||
|
import io.bkbn.kompendium.core.util.enrichedSimpleRequest
|
||||||
|
import io.bkbn.kompendium.core.util.enrichedSimpleResponse
|
||||||
import io.bkbn.kompendium.core.util.exampleParams
|
import io.bkbn.kompendium.core.util.exampleParams
|
||||||
import io.bkbn.kompendium.core.util.genericException
|
import io.bkbn.kompendium.core.util.genericException
|
||||||
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
|
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
|
||||||
@ -297,7 +301,17 @@ class KompendiumTest : DescribeSpec({
|
|||||||
}
|
}
|
||||||
it("Throws an exception when same method for same path has been previously registered") {
|
it("Throws an exception when same method for same path has been previously registered") {
|
||||||
val exception = shouldThrow<IllegalArgumentException> {
|
val exception = shouldThrow<IllegalArgumentException> {
|
||||||
openApiTestAllSerializers("") {
|
openApiTestAllSerializers(
|
||||||
|
snapshotName = "",
|
||||||
|
applicationSetup = {
|
||||||
|
install(Authentication) {
|
||||||
|
basic("basic") {
|
||||||
|
realm = "Ktor Server"
|
||||||
|
validate { UserIdPrincipal("Placeholder") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
samePathSameMethod()
|
samePathSameMethod()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,4 +428,18 @@ 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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
137
core/src/test/kotlin/io/bkbn/kompendium/core/util/Enrichment.kt
Normal file
137
core/src/test/kotlin/io/bkbn/kompendium/core/util/Enrichment.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ import io.ktor.server.routing.route
|
|||||||
fun Routing.samePathSameMethod() {
|
fun Routing.samePathSameMethod() {
|
||||||
route(defaultPath) {
|
route(defaultPath) {
|
||||||
basicGetGenerator<TestResponse>()
|
basicGetGenerator<TestResponse>()
|
||||||
authenticate {
|
authenticate("basic") {
|
||||||
basicGetGenerator<TestResponse>()
|
basicGetGenerator<TestResponse>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
core/src/test/resources/T0055__enriched_simple_request.json
Normal file
126
core/src/test/resources/T0055__enriched_simple_request.json
Normal 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": []
|
||||||
|
}
|
168
core/src/test/resources/T0056__enriched_nested_collection.json
Normal file
168
core/src/test/resources/T0056__enriched_nested_collection.json
Normal 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": []
|
||||||
|
}
|
@ -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": []
|
||||||
|
}
|
73
core/src/test/resources/T0058__enriched_simple_response.json
Normal file
73
core/src/test/resources/T0058__enriched_simple_response.json
Normal 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": []
|
||||||
|
}
|
@ -204,3 +204,89 @@ route("/user/{id}") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Enrichment
|
||||||
|
|
||||||
|
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 `enrich` function on the `NotarizedRoute` builder. Enrichments
|
||||||
|
can be added to any request or response.
|
||||||
|
|
||||||
|
```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 %}
|
||||||
|
|
||||||
|
At the moment, the only available enrichments are the following
|
||||||
|
|
||||||
|
- description -> Provides a reader friendly description of the field in the object
|
||||||
|
- deprecated -> Indicates that the field is deprecated and should not be used
|
||||||
|
|
||||||
|
### 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
34
enrichment/build.gradle.kts
Normal file
34
enrichment/build.gradle.kts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package io.bkbn.kompendium.enrichment
|
||||||
|
|
||||||
|
sealed interface Enrichment
|
@ -0,0 +1,7 @@
|
|||||||
|
package io.bkbn.kompendium.enrichment
|
||||||
|
|
||||||
|
class PropertyEnrichment : Enrichment {
|
||||||
|
var deprecated: Boolean? = null
|
||||||
|
var description: String? = null
|
||||||
|
var typeEnrichment: TypeEnrichment<*>? = null
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=3.9.0
|
project.version=3.10.0
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
@ -8,6 +8,6 @@ org.gradle.vfs.verbose=true
|
|||||||
org.gradle.jvmargs=-Xmx2000m
|
org.gradle.jvmargs=-Xmx2000m
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
ktorVersion=2.1.3
|
ktorVersion=2.2.1
|
||||||
kotestVersion=5.5.4
|
kotestVersion=5.5.4
|
||||||
detektVersion=1.21.0
|
detektVersion=1.21.0
|
||||||
|
@ -20,6 +20,9 @@ dependencies {
|
|||||||
// Versions
|
// Versions
|
||||||
val detektVersion: String by project
|
val detektVersion: String by project
|
||||||
|
|
||||||
|
// Kompendium
|
||||||
|
api(projects.kompendiumEnrichment)
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.0")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
||||||
|
|
||||||
|
@ -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, cache)
|
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) {
|
||||||
|
@ -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
|
||||||
|
@ -4,7 +4,9 @@ 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,
|
||||||
) : JsonSchema {
|
) : JsonSchema {
|
||||||
val type: String = "array"
|
val type: String = "array"
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,7 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class EnumDefinition(
|
data class EnumDefinition(
|
||||||
val type: String,
|
val type: String,
|
||||||
val enum: Set<String>
|
val enum: Set<String>,
|
||||||
|
override val deprecated: Boolean? = null,
|
||||||
|
override val description: String? = null,
|
||||||
) : JsonSchema
|
) : JsonSchema
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -7,10 +7,11 @@ 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,
|
||||||
) : JsonSchema {
|
) : JsonSchema {
|
||||||
|
|
||||||
fun withDefault(default: Any): TypeDefinition = this.copy(default = default)
|
fun withDefault(default: Any): TypeDefinition = this.copy(default = default)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
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.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 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<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
|
fun handle(
|
||||||
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
|
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()
|
||||||
return EnumDefinition(type = "string", enum = options)
|
return EnumDefinition(type = "string", enum = options)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -1,16 +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.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
|
||||||
@ -26,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
|
||||||
@ -90,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 {
|
||||||
@ -98,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)
|
||||||
|
.let {
|
||||||
if (it.isOrContainsObjectOrEnumDef()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[constructedType.getSimpleSlug()] = it
|
cache[constructedType.getSlug(propEnrichment)] = it
|
||||||
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
ReferenceDefinition(prop.returnType.getReferenceSlug(propEnrichment))
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@ -112,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.isOrContainsObjectOrEnumDef()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[type.getSimpleSlug()] = it
|
cache[type.getSlug(propEnrichment)] = it
|
||||||
ReferenceDefinition(type.getReferenceSlug())
|
ReferenceDefinition(type.getReferenceSlug(propEnrichment))
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@ -129,12 +145,13 @@ 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.isOrContainsObjectOrEnumDef()) {
|
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
|
||||||
}
|
}
|
||||||
@ -149,4 +166,15 @@ object SimpleObjectHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()}"
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema
|
|||||||
|
|
||||||
import io.bkbn.kompendium.core.fixtures.ComplexRequest
|
import io.bkbn.kompendium.core.fixtures.ComplexRequest
|
||||||
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
|
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
|
||||||
|
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
|
||||||
import io.bkbn.kompendium.core.fixtures.ObjectWithEnum
|
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
|
||||||
@ -11,12 +12,14 @@ 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.UnbackedObject
|
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") {
|
||||||
@ -88,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")
|
||||||
@ -97,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 {
|
||||||
@ -107,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)
|
||||||
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
@ -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
|
||||||
|
@ -50,7 +50,6 @@ private fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
redoc(pageTitle = "Simple API Docs")
|
redoc(pageTitle = "Simple API Docs")
|
||||||
|
|
||||||
route("/{id}") {
|
route("/{id}") {
|
||||||
idDocumentation()
|
idDocumentation()
|
||||||
get {
|
get {
|
||||||
|
@ -0,0 +1,154 @@
|
|||||||
|
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.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 {
|
||||||
|
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 {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,19 @@ import io.ktor.server.locations.Location
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExampleRequest(
|
||||||
|
val thingA: String,
|
||||||
|
val thingB: Int,
|
||||||
|
val thingC: InnerRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InnerRequest(
|
||||||
|
val d: Float,
|
||||||
|
val e: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ExampleResponse(val isReal: Boolean)
|
data class ExampleResponse(val isReal: Boolean)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
rootProject.name = "kompendium"
|
rootProject.name = "kompendium"
|
||||||
|
|
||||||
include("core")
|
include("core")
|
||||||
|
include("enrichment")
|
||||||
include("oas")
|
include("oas")
|
||||||
include("playground")
|
include("playground")
|
||||||
include("locations")
|
include("locations")
|
||||||
|
Reference in New Issue
Block a user