fix: locations inheritance (#135)

This commit is contained in:
Ryan Brink
2022-01-07 08:46:20 -05:00
committed by GitHub
parent da104d0a63
commit eb369dcdc8
11 changed files with 183 additions and 99 deletions

View File

@ -8,6 +8,7 @@
### Changed ### Changed
- Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization - Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization
- Fixed bug where overridden field names were not reflected in serialized object and required array - Fixed bug where overridden field names were not reflected in serialized object and required array
- Fixed bug where Ktor Location parents were not being scanned for parameters
### Remove ### Remove

View File

@ -0,0 +1,5 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.parser.IMethodParser
object DefaultMethodParser : IMethodParser

View File

@ -1,8 +1,9 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.DefaultMethodParser.calculateRoutePath
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo import io.bkbn.kompendium.core.DefaultMethodParser.parseMethodInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.HeadInfo import io.bkbn.kompendium.core.metadata.method.HeadInfo
@ -168,10 +169,4 @@ object Notarized {
feature.config.spec.paths[path]?.options = postProcess(baseInfo) feature.config.spec.paths[path]?.options = postProcess(baseInfo)
return method(HttpMethod.Options) { handle(body) } return method(HttpMethod.Options) { handle(body) }
} }
/**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything
* that matches the RegEx pattern `/\\(.+\\)`
*/
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
} }

View File

@ -1,7 +1,8 @@
package io.bkbn.kompendium.core package io.bkbn.kompendium.core.parser
import io.bkbn.kompendium.annotations.Param import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kontent.generateKontent import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Kontent
import io.bkbn.kompendium.core.metadata.ExceptionInfo import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.RequestInfo
@ -19,22 +20,20 @@ import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.schema.AnyOfSchema import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.ktor.routing.Route
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KParameter import kotlin.reflect.KParameter
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.primaryConstructor
import java.util.Locale import java.util.Locale
import java.util.UUID import java.util.UUID
/** interface IMethodParser {
* The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
*/
object MethodParser {
/** /**
* Generates the OpenAPI Path spec from provided metadata * Generates the OpenAPI Path spec from provided metadata
* @param info implementation of the [MethodInfo] sealed class * @param info implementation of the [MethodInfo] sealed class
@ -68,17 +67,17 @@ object MethodParser {
) else null ) else null
) )
private fun parseResponse( fun parseResponse(
responseType: KType, responseType: KType,
responseInfo: ResponseInfo<*>?, responseInfo: ResponseInfo<*>?,
feature: Kompendium feature: Kompendium
): Map<Int, Response> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty() ): Map<Int, Response> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
private fun parseExceptions( fun parseExceptions(
exceptionInfo: Set<ExceptionInfo<*>>, exceptionInfo: Set<ExceptionInfo<*>>,
feature: Kompendium, feature: Kompendium,
): Map<Int, Response> = exceptionInfo.associate { info -> ): Map<Int, Response> = exceptionInfo.associate { info ->
feature.config.cache = generateKontent(info.responseType, feature.config.cache) feature.config.cache = Kontent.generateKontent(info.responseType, feature.config.cache)
val response = Response( val response = Response(
description = info.description, description = info.description,
content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples) content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
@ -92,7 +91,7 @@ object MethodParser {
* @param requestInfo request metadata * @param requestInfo request metadata
* @return Will return a generated [Request] if requestInfo is not null * @return Will return a generated [Request] if requestInfo is not null
*/ */
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request? = fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request? =
when (requestInfo) { when (requestInfo) {
null -> null null -> null
else -> { else -> {
@ -111,7 +110,7 @@ object MethodParser {
* @param responseInfo response metadata * @param responseInfo response metadata
* @return Will return a generated [Pair] if responseInfo is not null * @return Will return a generated [Pair] if responseInfo is not null
*/ */
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response>? = fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response>? =
when (responseInfo) { when (responseInfo) {
null -> null null -> null
else -> { else -> {
@ -130,7 +129,7 @@ object MethodParser {
* @param examples Mapping of named examples of valid bodies. * @param examples Mapping of named examples of valid bodies.
* @return Named mapping of media types. * @return Named mapping of media types.
*/ */
private fun Kompendium.resolveContent( fun Kompendium.resolveContent(
type: KType, type: KType,
mediaTypes: List<String>, mediaTypes: List<String>,
examples: Map<String, Any> examples: Map<String, Any>
@ -163,29 +162,36 @@ object MethodParser {
* @return list of valid parameter specs as detailed by the [KType] members * @return list of valid parameter specs as detailed by the [KType] members
* @throws [IllegalStateException] if the class could not be parsed properly * @throws [IllegalStateException] if the class could not be parsed properly
*/ */
private fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> { fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
val clazz = classifier as KClass<*> val clazz = classifier as KClass<*>
return clazz.memberProperties.filter { prop -> return clazz.memberProperties
prop.findAnnotation<Param>() != null .filter { prop -> prop.hasAnnotation<Param>() }
}.map { prop -> .map { prop -> prop.toParameter(info, this, clazz, feature) }
val wrapperSchema = feature.config.cache[this.getSimpleSlug()]!! as ObjectSchema
val anny = prop.findAnnotation<Param>()
?: error("Field ${prop.name} is not annotated with KompendiumParam")
val schema = wrapperSchema.properties[prop.name]
?: error("Could not find component type for $prop")
val defaultValue = getDefaultParameterValue(clazz, prop)
Parameter(
name = prop.name,
`in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue),
description = schema.description,
required = !prop.returnType.isMarkedNullable && defaultValue == null,
examples = info.parameterExamples.mapToSpec(prop.name)
)
}
} }
private fun Set<ParameterExample>.mapToSpec(parameterName: String): Map<String, Parameter.Example>? { fun KProperty<*>.toParameter(
info: MethodInfo<*, *>,
parentType: KType,
parentClazz: KClass<*>,
feature: Kompendium
): Parameter {
val wrapperSchema = feature.config.cache[parentType.getSimpleSlug()]!! as ObjectSchema
val anny = this.findAnnotation<Param>()
?: error("Field $name is not annotated with KompendiumParam")
val schema = wrapperSchema.properties[name]
?: error("Could not find component type for $this")
val defaultValue = getDefaultParameterValue(parentClazz, this)
return Parameter(
name = name,
`in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue),
description = schema.description,
required = !returnType.isMarkedNullable && defaultValue == null,
examples = info.parameterExamples.mapToSpec(name)
)
}
fun Set<ParameterExample>.mapToSpec(parameterName: String): Map<String, Parameter.Example>? {
val filtered = filter { it.parameterName == parameterName } val filtered = filter { it.parameterName == parameterName }
return if (filtered.isEmpty()) { return if (filtered.isEmpty()) {
null null
@ -200,7 +206,7 @@ object MethodParser {
* @param prop the property in question * @param prop the property in question
* @return The default value if found * @return The default value if found
*/ */
private fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? { fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
val constructor = clazz.primaryConstructor val constructor = clazz.primaryConstructor
val parameterInQuestion = constructor val parameterInQuestion = constructor
?.parameters ?.parameters
@ -227,7 +233,7 @@ object MethodParser {
* @return value of the proper type to match param * @return value of the proper type to match param
* @throws [IllegalStateException] if parameter type is not one of the basic types supported below. * @throws [IllegalStateException] if parameter type is not one of the basic types supported below.
*/ */
private fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) { fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
String::class -> "test" String::class -> "test"
Boolean::class -> false Boolean::class -> false
Int::class -> 1 Int::class -> 1
@ -237,4 +243,10 @@ object MethodParser {
UUID::class -> UUID.randomUUID() UUID::class -> UUID.randomUUID()
else -> error("Unsupported Type") else -> error("Unsupported Type")
} }
/**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything
* that matches the RegEx pattern `/\\(.+\\)`
*/
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
} }

View File

@ -0,0 +1,84 @@
package io.bkbn.kompendium.locations
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.metadata.method.MethodInfo
import io.bkbn.kompendium.core.parser.IMethodParser
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.application.feature
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.routing.Route
import io.ktor.routing.application
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
@OptIn(KtorExperimentalLocationsAPI::class)
object LocationMethodParser : IMethodParser {
override fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
val clazzList = determineLocationParents(classifier!!)
return clazzList.associateWith { it.memberProperties }
.flatMap { (clazz, memberProperties) -> memberProperties.associateWith { clazz }.toList() }
.filter { (prop, _) -> prop.hasAnnotation<Param>() }
.map { (prop, clazz) -> prop.toParameter(info, clazz.createType(), clazz, feature) }
}
private fun determineLocationParents(classifier: KClassifier): List<KClass<*>> {
var clazz: KClass<*>? = classifier as KClass<*>
val clazzList = mutableListOf<KClass<*>>()
while (clazz != null) {
clazzList.add(clazz)
clazz = getLocationParent(clazz)
}
return clazzList
}
private fun getLocationParent(clazz: KClass<*>): KClass<*>? {
val parent = clazz.memberProperties
.find { (it.returnType.classifier as KAnnotatedElement).hasAnnotation<Location>() }
return parent?.returnType?.classifier as? KClass<*>
}
fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
val locationAnnotation = this.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val parent = this.java.declaringClass?.kotlin
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculateLocationPath(newSuffix)
}
}
inline fun <reified TParam : Any> processBaseInfo(
paramType: KType,
requestType: KType,
responseType: KType,
info: MethodInfo<*, *>,
route: Route
): LocationBaseInfo {
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = route.calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
val feature = route.application.feature(Kompendium)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
return LocationBaseInfo(baseInfo, feature, pathWithLocation)
}
data class LocationBaseInfo(
val op: PathOperation,
val feature: Kompendium,
val path: String
)
}

View File

@ -1,28 +1,19 @@
package io.bkbn.kompendium.locations package io.bkbn.kompendium.locations
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo
import io.bkbn.kompendium.core.Notarized.calculateRoutePath
import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation import io.bkbn.kompendium.oas.path.PathOperation
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.feature
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.handle import io.ktor.locations.handle
import io.ktor.locations.location import io.ktor.locations.location
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.application
import io.ktor.routing.method import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
/** /**
* This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access * This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access
@ -45,15 +36,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.get = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.get = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Get) { handle(body) } method(HttpMethod.Get) { handle(body) }
} }
@ -74,15 +58,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.post = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.post = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Post) { handle(body) } method(HttpMethod.Post) { handle(body) }
} }
@ -103,15 +80,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.put = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.put = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Put) { handle(body) } method(HttpMethod.Put) { handle(body) }
} }
@ -131,28 +101,10 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p }, postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType -> ): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>() val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" } lbi.feature.config.spec.paths[lbi.path]?.delete = postProcess(lbi.op)
val feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.delete = postProcess(baseInfo)
return location(TParam::class) { return location(TParam::class) {
method(HttpMethod.Delete) { handle(body) } method(HttpMethod.Delete) { handle(body) }
} }
} }
fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
val locationAnnotation = this.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val parent = this.java.declaringClass?.kotlin
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculateLocationPath(newSuffix)
}
}
} }

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"responses": { "responses": {

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"responses": { "responses": {

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"requestBody": { "requestBody": {

View File

@ -40,6 +40,15 @@
}, },
"required": true, "required": true,
"deprecated": false "deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
} }
], ],
"requestBody": { "requestBody": {

View File

@ -20,5 +20,4 @@ object NumberSerializer : KSerializer<Number> {
override fun serialize(encoder: Encoder, value: Number) { override fun serialize(encoder: Encoder, value: Number) {
encoder.encodeString(value.toString()) encoder.encodeString(value.toString())
} }
} }