From f42de3bc6c379cf9d4ea45cc86ea8e486237d44e Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 21 Jun 2024 21:05:54 +0200 Subject: [PATCH 1/4] set version 3.1.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 058fbbf3dd2d99f6784e26844d0b9b4141f2953f Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Sat, 22 Jun 2024 11:53:18 +0200 Subject: [PATCH 2/4] allow manual trigger of check gh-action --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From aec52a881087e2d60898b07fa28de40510346646 Mon Sep 17 00:00:00 2001 From: Matei-Paul Trandafir Date: Thu, 13 Jun 2024 16:41:14 +0300 Subject: [PATCH 3/4] add example encoder config option --- .../ktorswaggerui/examples/Examples.kt | 28 +++++++++++++++++++ .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 3 +- .../builder/example/ExampleContext.kt | 6 ++-- .../builder/example/ExampleContextImpl.kt | 26 ++++++++++------- .../builder/openapi/ContentBuilder.kt | 2 +- .../builder/openapi/ParameterBuilder.kt | 2 +- .../ktorswaggerui/data/ExampleConfigData.kt | 12 ++++++-- .../ktorswaggerui/dsl/config/ExampleConfig.kt | 11 ++++++-- .../builder/OpenApiBuilderTest.kt | 2 +- .../builder/OperationBuilderTest.kt | 2 +- .../ktorswaggerui/builder/PathsBuilderTest.kt | 2 +- 11 files changed, 73 insertions(+), 23 deletions(-) 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 rootExamples = mutableMapOf() private val componentExamples = mutableMapOf() @@ -18,12 +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) - rootExamples[exampleDescriptor] = example + rootExamples[exampleDescriptor] = exampleDescriptor } } @@ -33,7 +32,7 @@ class ExampleContextImpl : ExampleContext { */ fun add(routes: Collection) { collectExampleDescriptors(routes).forEach { exampleDescriptor -> - rootExamples[exampleDescriptor] = generateExample(exampleDescriptor) + rootExamples[exampleDescriptor] = exampleDescriptor } } @@ -71,22 +70,29 @@ class ExampleContextImpl : ExampleContext { /** * 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 } } - override fun getExample(descriptor: ExampleDescriptor): Example { - return rootExamples[descriptor] ?: throw NoSuchElementException("no root-example for given example-descriptor") + override fun getExample(descriptor: ExampleDescriptor, type: TypeDescriptor): Example { + return generateExample( + rootExamples[descriptor] ?: throw NoSuchElementException("no root-example for given example-descriptor"), + type + ) } override fun getComponentSection(): Map { 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..21e499d 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 @@ -53,7 +53,7 @@ class ContentBuilder( return MediaType().also { it.schema = schema body.examples.forEach { descriptor -> - it.addExamples(descriptor.name, exampleContext.getExample(descriptor)) + it.addExamples(descriptor.name, exampleContext.getExample(descriptor, body.type)) } } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt index 6f852c6..19e9680 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt @@ -28,7 +28,7 @@ class ParameterBuilder( it.deprecated = parameter.deprecated it.allowEmptyValue = parameter.allowEmptyValue it.explode = parameter.explode - it.example = parameter.example?.let { e -> exampleContext.getExample(e).value } // todo: example"S" ? + it.example = parameter.example?.let { e -> exampleContext.getExample(e, parameter.type).value } // todo: example"S" ? it.allowReserved = parameter.allowReserved it.schema = schemaContext.getSchema(parameter.type) it.style = parameter.style 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) } From be36ee0eec4c1288af32c38ff0f04b0068710564 Mon Sep 17 00:00:00 2001 From: Matei-Paul Trandafir Date: Thu, 27 Jun 2024 11:54:56 +0300 Subject: [PATCH 4/4] Collect types & generated examples on add rather than on get --- .../builder/example/ExampleContext.kt | 6 +- .../builder/example/ExampleContextImpl.kt | 56 +++++++++---------- .../builder/openapi/ContentBuilder.kt | 4 +- .../builder/openapi/ParameterBuilder.kt | 2 +- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt index 44160d4..216b98c 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt @@ -1,7 +1,6 @@ package io.github.smiley4.ktorswaggerui.builder.example import io.github.smiley4.ktorswaggerui.data.ExampleDescriptor -import io.github.smiley4.ktorswaggerui.data.TypeDescriptor import io.swagger.v3.oas.models.examples.Example /** @@ -10,10 +9,9 @@ import io.swagger.v3.oas.models.examples.Example interface ExampleContext { /** - * Get an [Example] (or a ref to an example) by its [ExampleDescriptor], - * and its type, which may be used to encode the example first. + * Get an [Example] (or a ref to an example) by its [ExampleDescriptor]. */ - fun getExample(descriptor: ExampleDescriptor, type: TypeDescriptor): Example + fun getExample(descriptor: ExampleDescriptor): Example /** diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt index 5da864e..f119dde 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt @@ -9,7 +9,7 @@ import io.swagger.v3.oas.models.examples.Example */ class ExampleContextImpl(private val encoder: ExampleEncoder?) : ExampleContext { - private val rootExamples = mutableMapOf() + private val rootExamples = mutableMapOf() private val componentExamples = mutableMapOf() @@ -22,7 +22,8 @@ class ExampleContextImpl(private val encoder: ExampleEncoder?) : ExampleContext componentExamples[exampleDescriptor.name] = example } config.securityExamples.forEach { exampleDescriptor -> - rootExamples[exampleDescriptor] = exampleDescriptor + val example = generateExample(exampleDescriptor, null) + rootExamples[exampleDescriptor] = example } } @@ -31,8 +32,9 @@ class ExampleContextImpl(private val encoder: ExampleEncoder?) : ExampleContext * Collect and add all examples for the given routes */ fun add(routes: Collection) { - collectExampleDescriptors(routes).forEach { exampleDescriptor -> - rootExamples[exampleDescriptor] = exampleDescriptor + collectExampleDescriptors(routes).forEach { (exampleDescriptor, typeDescriptor) -> + val example = generateExample(exampleDescriptor, typeDescriptor) + rootExamples[exampleDescriptor] = example } } @@ -40,31 +42,30 @@ class ExampleContextImpl(private val encoder: ExampleEncoder?) : 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 - } + } /** @@ -88,11 +89,8 @@ class ExampleContextImpl(private val encoder: ExampleEncoder?) : ExampleContext } } - override fun getExample(descriptor: ExampleDescriptor, type: TypeDescriptor): Example { - return generateExample( - rootExamples[descriptor] ?: throw NoSuchElementException("no root-example for given example-descriptor"), - type - ) + override fun getExample(descriptor: ExampleDescriptor): Example { + return rootExamples[descriptor] ?: throw NoSuchElementException("no root-example for given example-descriptor") } override fun getComponentSection(): Map { 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 21e499d..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 @@ -53,7 +53,7 @@ class ContentBuilder( return MediaType().also { it.schema = schema body.examples.forEach { descriptor -> - it.addExamples(descriptor.name, exampleContext.getExample(descriptor, body.type)) + it.addExamples(descriptor.name, exampleContext.getExample(descriptor)) } } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt index 19e9680..6f852c6 100644 --- a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt @@ -28,7 +28,7 @@ class ParameterBuilder( it.deprecated = parameter.deprecated it.allowEmptyValue = parameter.allowEmptyValue it.explode = parameter.explode - it.example = parameter.example?.let { e -> exampleContext.getExample(e, parameter.type).value } // todo: example"S" ? + it.example = parameter.example?.let { e -> exampleContext.getExample(e).value } // todo: example"S" ? it.allowReserved = parameter.allowReserved it.schema = schemaContext.getSchema(parameter.type) it.style = parameter.style