Skip to content
This repository was archived by the owner on Dec 15, 2024. It is now read-only.

Commit d621b65

Browse files
committed
Support schemas
1 parent d604efe commit d621b65

File tree

7 files changed

+81
-0
lines changed

7 files changed

+81
-0
lines changed

kotest-extensions-ktor-openapi-model/src/main/kotlin/io/kotest/extensions/ktor/openapi/keys.kt

+6
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import io.ktor.server.application.ApplicationCall
44
import io.ktor.server.application.call
55
import io.ktor.util.AttributeKey
66
import io.ktor.util.pipeline.PipelineContext
7+
import kotlin.reflect.KClass
78

89
val DescriptionKey: AttributeKey<String> = AttributeKey("KotestOpenApiDescriptionKey")
910
val DeprecatedKey: AttributeKey<Boolean> = AttributeKey("KotestOpenApiDeprecatedKey")
11+
val SchemaKey: AttributeKey<KClass<*>> = AttributeKey("KotestOpenApiSchemaKey")
1012

1113
fun PipelineContext<*, ApplicationCall>.description(desc: String) {
1214
call.attributes.put(DescriptionKey, desc)
@@ -15,3 +17,7 @@ fun PipelineContext<*, ApplicationCall>.description(desc: String) {
1517
fun PipelineContext<*, ApplicationCall>.deprecated(deprecated: Boolean) {
1618
if (deprecated) call.attributes.put(DeprecatedKey, deprecated)
1719
}
20+
21+
inline fun <reified T : Any> PipelineContext<*, ApplicationCall>.schema() {
22+
call.attributes.put(SchemaKey, T::class)
23+
}

kotest-extensions-ktor-openapi-plugin/src/main/kotlin/io/kotest/extensions/ktor/openapi/OpenApiBuilder.kt

+6
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ class OpenApiBuilder(private val config: OpenApiConfig) {
4444
val item = PathItem()
4545
openapi.path(path, item)
4646

47+
// add all schemas at the top level
48+
traces.mapNotNull { it.schema }.forEach { schema ->
49+
openapi.components.addSchemas(schema.java.name, schema.toSchema())
50+
}
51+
4752
// each http method is an operation
4853
traces.groupBy { it.method }.forEach { (method, tracesByMethod) ->
4954

@@ -75,6 +80,7 @@ class OpenApiBuilder(private val config: OpenApiConfig) {
7580
if (contentType != null) {
7681

7782
val mediaType = MediaType()
83+
mediaType.schema = tracesByContentType.firstNotNullOfOrNull { it.schema }?.toSchema()
7884

7985
// for each content type that is the same, they are added as multiple examples
8086
// to the same MediaType in the response content

kotest-extensions-ktor-openapi-plugin/src/main/kotlin/io/kotest/extensions/ktor/openapi/Tracer.kt

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.kotest.extensions.ktor.openapi
33
import io.ktor.http.ContentType
44
import io.ktor.http.HttpMethod
55
import io.ktor.http.HttpStatusCode
6+
import kotlin.reflect.KClass
67

78
/**
89
* The tracer is used to collect individual route traces.
@@ -37,6 +38,7 @@ data class Trace(
3738
var responseBody: String? = null,
3839
var description: String?,
3940
var deprecated: Boolean = false,
41+
var schema: KClass<*>? = null,
4042
var pathParameterExamples: Map<String, String?>,
4143
) {
4244
companion object {

kotest-extensions-ktor-openapi-plugin/src/main/kotlin/io/kotest/extensions/ktor/openapi/plugin.kt

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ val KotestOpenApi = createApplicationPlugin("OpenApi", createConfiguration = ::O
6868
trace.status = call.response.status()
6969
trace.description = call.attributes.getOrNull(DescriptionKey)
7070
trace.deprecated = call.attributes.getOrNull(DeprecatedKey) ?: false
71+
trace.schema = call.attributes.getOrNull(SchemaKey)
7172
trace.pathParameterExamples = trace.pathParameters.associateWith { call.parameters[it] }
7273
this@createApplicationPlugin.pluginConfig.tracer.addTrace(trace)
7374
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.kotest.extensions.ktor.openapi
2+
3+
import io.swagger.v3.oas.models.media.Schema
4+
import kotlin.reflect.KClass
5+
import kotlin.reflect.full.memberProperties
6+
import kotlin.reflect.typeOf
7+
8+
object SwaggerSchemas {
9+
val string = Schema<String>().apply { type = "string" }
10+
val integer = Schema<String>().apply { type = "integer" }
11+
val number = Schema<String>().apply { type = "number" }
12+
val boolean = Schema<String>().apply { type = "boolean" }
13+
val obj = Schema<String>().apply { type = "object" }
14+
}
15+
16+
fun KClass<*>.toSchema(): Schema<Any> {
17+
require(isData)
18+
val schema = Schema<Any>()
19+
schema.name = this.java.name
20+
schema.type = "object"
21+
memberProperties.map { prop ->
22+
val propSchema = when (prop.returnType) {
23+
typeOf<String>() -> SwaggerSchemas.string
24+
typeOf<Int>() -> SwaggerSchemas.integer
25+
typeOf<Long>() -> SwaggerSchemas.integer
26+
typeOf<Float>() -> SwaggerSchemas.number
27+
typeOf<Double>() -> SwaggerSchemas.number
28+
typeOf<Boolean>() -> SwaggerSchemas.boolean
29+
else -> null
30+
}
31+
schema.addProperty(prop.name, propSchema)
32+
}
33+
return schema
34+
}

kotest-extensions-ktor-openapi-plugin/src/test/kotlin/io/kotest/extensions/ktor/openapi/OpenApiTest.kt

+7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class OpenApiTest : FunSpec() {
4141
route("/internal") {
4242
get("/foo1") {
4343
description("Returns the user identified by the foo param")
44+
schema<MyDataClass>()
4445
call.respond(HttpStatusCode.OK)
4546
}
4647
patch("/patchme") {
@@ -65,3 +66,9 @@ class OpenApiTest : FunSpec() {
6566
}
6667
}
6768
}
69+
70+
data class MyDataClass(
71+
val a: String,
72+
val b: Int,
73+
val c: Double,
74+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.kotest.extensions.ktor.openapi
2+
3+
import io.kotest.core.spec.style.FunSpec
4+
import io.kotest.matchers.shouldBe
5+
import io.swagger.v3.oas.models.media.Schema
6+
7+
class SchemaBuilderTest : FunSpec() {
8+
init {
9+
10+
test("basic property types") {
11+
data class Foo(val a: String, val b: Boolean, val c: Int, val d: Long, val e: Float, val f: Double)
12+
13+
val expected = Schema<Foo>()
14+
expected.type = "object"
15+
expected.addProperty("a", SwaggerSchemas.string)
16+
expected.addProperty("b", SwaggerSchemas.boolean)
17+
expected.addProperty("c", SwaggerSchemas.integer)
18+
expected.addProperty("d", SwaggerSchemas.integer)
19+
expected.addProperty("e", SwaggerSchemas.number)
20+
expected.addProperty("f", SwaggerSchemas.number)
21+
expected.name = Foo::class.java.name
22+
Foo::class.toSchema() shouldBe expected
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)