diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3c66b77..bafd296 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,6 +1,6 @@ name: Checks -on: [ push, pull_request ] +on: [ push, pull_request, workflow_dispatch ] permissions: contents: read diff --git a/gradle.properties b/gradle.properties index 99e7359..994f325 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ kotlin.code.style=official # project id projectGroupId=io.github.smiley4 projectArtifactIdBase=ktor-swagger-ui -projectVersion=3.0.1 +projectVersion=3.1.0 # publishing information projectNameBase=Ktor Swagger UI diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt index 4c47d4d..4240c3c 100644 --- a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt @@ -1,6 +1,7 @@ package io.github.smiley4.ktorswaggerui.examples import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor import io.github.smiley4.ktorswaggerui.dsl.routing.get import io.github.smiley4.ktorswaggerui.routing.openApiSpec import io.github.smiley4.ktorswaggerui.routing.swaggerUI @@ -12,6 +13,7 @@ import io.ktor.server.netty.Netty import io.ktor.server.response.respondText import io.ktor.server.routing.route import io.ktor.server.routing.routing +import kotlin.reflect.typeOf fun main() { embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) @@ -37,6 +39,15 @@ private fun Application.myModule() { ) } + // encoder used in "custom-encoder" example + encoder { type, example -> + // encode just the wrapped value for CustomEncoderData class + if (type is KTypeDescriptor && type.type == typeOf()) + (example as CustomEncoderData).number + // return the example unmodified to fall back to default encoder + else + example + } } } @@ -86,6 +97,19 @@ private fun Application.myModule() { call.respondText("...") } + get("custom-encoder", { + request { + body { + // The type is CustomEncoderData, but it's actually encoded as a plain number + // See configuration for encoder + example("Example 1") { + value = CustomEncoderData(123) + } + } + } + }) { + call.respondText("...") + } } } @@ -94,3 +118,7 @@ private fun Application.myModule() { private data class MyExampleClass( val someValue: String ) + +private data class CustomEncoderData( + val number: Int +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 6833c6a..d6d6885 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -30,6 +30,7 @@ import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl import io.github.smiley4.ktorswaggerui.routing.ApiSpec import io.ktor.server.application.Application @@ -93,7 +94,7 @@ private fun buildOpenApiSpec(pluginConfig: PluginConfigData, routes: List() private val componentExamples = mutableMapOf() @@ -18,11 +18,11 @@ class ExampleContextImpl : ExampleContext { */ fun addShared(config: ExampleConfigData) { config.sharedExamples.forEach { (_, exampleDescriptor) -> - val example = generateExample(exampleDescriptor) + val example = generateExample(exampleDescriptor, null) componentExamples[exampleDescriptor.name] = example } config.securityExamples.forEach { exampleDescriptor -> - val example = generateExample(exampleDescriptor) + val example = generateExample(exampleDescriptor, null) rootExamples[exampleDescriptor] = example } } @@ -32,8 +32,9 @@ class ExampleContextImpl : ExampleContext { * Collect and add all examples for the given routes */ fun add(routes: Collection) { - collectExampleDescriptors(routes).forEach { exampleDescriptor -> - rootExamples[exampleDescriptor] = generateExample(exampleDescriptor) + collectExampleDescriptors(routes).forEach { (exampleDescriptor, typeDescriptor) -> + val example = generateExample(exampleDescriptor, typeDescriptor) + rootExamples[exampleDescriptor] = example } } @@ -41,46 +42,49 @@ class ExampleContextImpl : ExampleContext { /** * Collect all [ExampleDescriptor]s from the given routes */ - private fun collectExampleDescriptors(routes: Collection): List { - val descriptors = mutableListOf() - routes - .filter { !it.documentation.hidden } - .forEach { route -> - route.documentation.request.also { request -> - request.parameters.forEach { parameter -> - parameter.example?.also { descriptors.add(it) } - } - request.body?.also { body -> - if (body is OpenApiSimpleBodyData) { - descriptors.addAll(body.examples) + private fun collectExampleDescriptors(routes: Collection): List> = + buildList { + routes + .filter { !it.documentation.hidden } + .forEach { route -> + route.documentation.request.also { request -> + request.parameters.forEach { parameter -> + parameter.example?.also { add(it to parameter.type) } + } + request.body?.also { body -> + if (body is OpenApiSimpleBodyData) { + addAll(body.examples.map { it to body.type }) + } } } - } - route.documentation.responses.forEach { response -> - response.body?.also { body -> - if (body is OpenApiSimpleBodyData) { - descriptors.addAll(body.examples) + route.documentation.responses.forEach { response -> + response.body?.also { body -> + if (body is OpenApiSimpleBodyData) { + addAll(body.examples.map { it to body.type }) + } } } } - } - return descriptors - } + } /** * Generate a swagger [Example] from the given [ExampleDescriptor] */ - private fun generateExample(exampleDescriptor: ExampleDescriptor): Example { + private fun generateExample(exampleDescriptor: ExampleDescriptor, type: TypeDescriptor?): Example { return when (exampleDescriptor) { is ValueExampleDescriptor -> Example().also { - it.value = exampleDescriptor.value + it.value = + if (encoder != null) encoder.invoke(type, exampleDescriptor.value) + else exampleDescriptor.value it.summary = exampleDescriptor.summary it.description = exampleDescriptor.description } + is RefExampleDescriptor -> Example().also { it.`$ref` = "#/components/examples/${exampleDescriptor.refName}" } + is SwaggerExampleDescriptor -> exampleDescriptor.example } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt index bd9b970..fc2e213 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt @@ -5,7 +5,7 @@ import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.github.smiley4.ktorswaggerui.data.OpenApiBaseBodyData import io.github.smiley4.ktorswaggerui.data.OpenApiMultipartBodyData import io.github.smiley4.ktorswaggerui.data.OpenApiSimpleBodyData -import io.ktor.http.ContentType +import io.ktor.http.* import io.swagger.v3.oas.models.media.Content import io.swagger.v3.oas.models.media.Encoding import io.swagger.v3.oas.models.media.MediaType diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt index 742b566..f93d88c 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt @@ -1,14 +1,22 @@ package io.github.smiley4.ktorswaggerui.data +/** + * Encoder to produce the final example value. + * Return the unmodified example to fall back to the default encoder. + */ +typealias ExampleEncoder = (type: TypeDescriptor?, example: Any?) -> Any? + class ExampleConfigData( val sharedExamples: Map, - val securityExamples: List + val securityExamples: List, + val exampleEncoder: ExampleEncoder? ) { companion object { val DEFAULT = ExampleConfigData( sharedExamples = emptyMap(), - securityExamples = emptyList() + securityExamples = emptyList(), + exampleEncoder = null ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt index 7c45b2a..151dbd9 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt @@ -6,12 +6,13 @@ import io.github.smiley4.ktorswaggerui.dsl.routes.ValueExampleDescriptorDsl import io.swagger.v3.oas.models.examples.Example /** - * Configuration for schemas + * Configuration for examples */ @OpenApiDslMarker class ExampleConfig { val sharedExamples = mutableMapOf() + var exampleEncoder: ExampleEncoder? = null fun example(example: ExampleDescriptor) { sharedExamples[example.name] = example @@ -32,6 +33,10 @@ class ExampleConfig { } ) + fun encoder(exampleEncoder: ExampleEncoder) { + this.exampleEncoder = exampleEncoder + } + fun build(securityConfig: SecurityData) = ExampleConfigData( sharedExamples = sharedExamples, securityExamples = securityConfig.defaultUnauthorizedResponse?.body?.let { @@ -39,7 +44,7 @@ class ExampleConfig { is OpenApiSimpleBodyData -> it.examples is OpenApiMultipartBodyData -> emptyList() } - } ?: emptyList() + } ?: emptyList(), + exampleEncoder = exampleEncoder ) - } diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt index b4a3fa3..4b23947 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt @@ -116,7 +116,7 @@ class OpenApiBuilderTest : StringSpec({ private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl): ExampleContext { val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return ExampleContextImpl().also { + return ExampleContextImpl(pluginConfigData.exampleConfig.exampleEncoder).also { it.addShared(pluginConfigData.exampleConfig) it.add(routes) } diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt index d08d78b..b674a3a 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt @@ -998,7 +998,7 @@ class OperationBuilderTest : StringSpec({ private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): ExampleContext { val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return ExampleContextImpl().also { + return ExampleContextImpl(pluginConfigData.exampleConfig.exampleEncoder).also { it.addShared(pluginConfigData.exampleConfig) it.add(routes) } diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt index ff5534d..d9c9d65 100644 --- a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt @@ -95,7 +95,7 @@ class PathsBuilderTest : StringSpec({ private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl): ExampleContext { val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return ExampleContextImpl().also { + return ExampleContextImpl(pluginConfigData.exampleConfig.exampleEncoder).also { it.addShared(pluginConfigData.exampleConfig) it.add(routes) }