Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example encoder config option #107

Merged
merged 2 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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<CustomEncoderData>())
(example as CustomEncoderData).number
// return the example unmodified to fall back to default encoder
else
example
}
}
}

Expand Down Expand Up @@ -86,6 +97,19 @@ private fun Application.myModule() {
call.respondText("...")
}

get("custom-encoder", {
request {
body<CustomEncoderData> {
// 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("...")
}
}

}
Expand All @@ -94,3 +118,7 @@ private fun Application.myModule() {
private data class MyExampleClass(
val someValue: String
)

private data class CustomEncoderData(
val number: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,7 +94,7 @@ private fun buildOpenApiSpec(pluginConfig: PluginConfigData, routes: List<RouteM
it.addGlobal(pluginConfig.schemaConfig)
it.add(routes)
}
val exampleContext = ExampleContextImpl().also {
val exampleContext = ExampleContextImpl(pluginConfig.exampleConfig.exampleEncoder).also {
it.addShared(pluginConfig.exampleConfig)
it.add(routes)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.swagger.v3.oas.models.examples.Example
interface ExampleContext {

/**
* Get an [Example] (or a ref to an example) by its [ExampleDescriptor]
* Get an [Example] (or a ref to an example) by its [ExampleDescriptor].
*/
fun getExample(descriptor: ExampleDescriptor): Example

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.swagger.v3.oas.models.examples.Example
/**
* Implementation of an [ExampleContext].
*/
class ExampleContextImpl : ExampleContext {
class ExampleContextImpl(private val encoder: ExampleEncoder?) : ExampleContext {

private val rootExamples = mutableMapOf<ExampleDescriptor, Example>()
private val componentExamples = mutableMapOf<String, Example>()
Expand All @@ -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
}
}
Expand All @@ -32,55 +32,59 @@ class ExampleContextImpl : ExampleContext {
* Collect and add all examples for the given routes
*/
fun add(routes: Collection<RouteMeta>) {
collectExampleDescriptors(routes).forEach { exampleDescriptor ->
rootExamples[exampleDescriptor] = generateExample(exampleDescriptor)
collectExampleDescriptors(routes).forEach { (exampleDescriptor, typeDescriptor) ->
val example = generateExample(exampleDescriptor, typeDescriptor)
rootExamples[exampleDescriptor] = example
}
}


/**
* Collect all [ExampleDescriptor]s from the given routes
*/
private fun collectExampleDescriptors(routes: Collection<RouteMeta>): List<ExampleDescriptor> {
val descriptors = mutableListOf<ExampleDescriptor>()
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<RouteMeta>): List<Pair<ExampleDescriptor, TypeDescriptor>> =
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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ExampleDescriptor>,
val securityExamples: List<ExampleDescriptor>
val securityExamples: List<ExampleDescriptor>,
val exampleEncoder: ExampleEncoder?
) {

companion object {
val DEFAULT = ExampleConfigData(
sharedExamples = emptyMap(),
securityExamples = emptyList()
securityExamples = emptyList(),
exampleEncoder = null
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, ExampleDescriptor>()
var exampleEncoder: ExampleEncoder? = null

fun example(example: ExampleDescriptor) {
sharedExamples[example.name] = example
Expand All @@ -32,14 +33,18 @@ class ExampleConfig {
}
)

fun encoder(exampleEncoder: ExampleEncoder) {
this.exampleEncoder = exampleEncoder
}

fun build(securityConfig: SecurityData) = ExampleConfigData(
sharedExamples = sharedExamples,
securityExamples = securityConfig.defaultUnauthorizedResponse?.body?.let {
when (it) {
is OpenApiSimpleBodyData -> it.examples
is OpenApiMultipartBodyData -> emptyList()
}
} ?: emptyList()
} ?: emptyList(),
exampleEncoder = exampleEncoder
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class OpenApiBuilderTest : StringSpec({

private fun exampleContext(routes: List<RouteMeta>, 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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ class OperationBuilderTest : StringSpec({

private fun exampleContext(routes: List<RouteMeta>, 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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class PathsBuilderTest : StringSpec({

private fun exampleContext(routes: List<RouteMeta>, 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)
}
Expand Down
Loading