Skip to content

Commit

Permalink
add example encoder config option
Browse files Browse the repository at this point in the history
  • Loading branch information
Matei-Paul Trandafir committed Jun 27, 2024
1 parent 058fbbf commit aec52a8
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 23 deletions.
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
@@ -1,6 +1,7 @@
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

/**
Expand All @@ -9,9 +10,10 @@ 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],
* and its type, which may be used to encode the example first.
*/
fun getExample(descriptor: ExampleDescriptor): Example
fun getExample(descriptor: ExampleDescriptor, type: TypeDescriptor): Example


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ 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 rootExamples = mutableMapOf<ExampleDescriptor, ExampleDescriptor>()
private val componentExamples = mutableMapOf<String, Example>()


Expand All @@ -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
}
}

Expand All @@ -33,7 +32,7 @@ class ExampleContextImpl : ExampleContext {
*/
fun add(routes: Collection<RouteMeta>) {
collectExampleDescriptors(routes).forEach { exampleDescriptor ->
rootExamples[exampleDescriptor] = generateExample(exampleDescriptor)
rootExamples[exampleDescriptor] = exampleDescriptor
}
}

Expand Down Expand Up @@ -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<String, Example> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

0 comments on commit aec52a8

Please sign in to comment.