com.papsign.ktor.openapigen.route.OpenAPIRoute.kt Maven / Gradle / Ivy
package com.papsign.ktor.openapigen.route
import com.papsign.ktor.openapigen.classLogger
import com.papsign.ktor.openapigen.content.type.*
import com.papsign.ktor.openapigen.content.type.ktor.KtorContentProvider
import com.papsign.ktor.openapigen.exceptions.OpenAPINoParserException
import com.papsign.ktor.openapigen.exceptions.OpenAPINoSerializerException
import com.papsign.ktor.openapigen.modules.CachingModuleProvider
import com.papsign.ktor.openapigen.modules.OpenAPIModule
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.HandlerModule
import com.papsign.ktor.openapigen.openAPIGen
import com.papsign.ktor.openapigen.parameters.handlers.ParameterHandler
import com.papsign.ktor.openapigen.parameters.util.buildParameterHandler
import com.papsign.ktor.openapigen.route.response.Responder
import com.papsign.ktor.openapigen.validation.ValidationHandler
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.contentType
import io.ktor.server.routing.Route
import io.ktor.server.routing.accept
import io.ktor.server.routing.application
import io.ktor.server.routing.contentType
import io.ktor.util.pipeline.PipelineContext
import io.ktor.util.reflect.TypeInfo
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
import kotlin.reflect.KVariance
import kotlin.reflect.full.createType
abstract class OpenAPIRoute>(val ktorRoute: Route, val provider: CachingModuleProvider) {
private val log = classLogger()
abstract fun child(route: Route = this.ktorRoute): T
fun handle(
paramsType: KType,
responseType: KType,
bodyType: KType,
pass: suspend OpenAPIRoute<*>.(pipeline: PipelineContext, responder: Responder, P, B) -> Unit
) {
val parameterHandler = buildParameterHandler(paramsType)
provider.registerModule(parameterHandler, ParameterHandler::class.createType(listOf(KTypeProjection(KVariance.INVARIANT, paramsType))))
val apiGen = application.openAPIGen
provider.ofType().forEach {
it.configure(apiGen, provider)
}
val BHandler = ValidationHandler.build(bodyType)
val PHandler = ValidationHandler.build(paramsType)
ktorRoute.apply {
getAcceptMap(responseType).let {
if (it.isNotEmpty()) it else listOf(ContentType.Any to listOf(SelectedSerializer(KtorContentProvider)))
}.forEach { (acceptType, serializers) ->
val responder = ContentTypeResponder(serializers.getResponseSerializer(acceptType), acceptType)
accept(acceptType) {
if (bodyType.classifier == Unit::class) {
handle {
@Suppress("UNCHECKED_CAST")
val params: P = if (paramsType.classifier == Unit::class) Unit as P else parameterHandler.parse(call.parameters, call.request.headers)
@Suppress("UNCHECKED_CAST")
pass(this, responder, PHandler.handle(params), Unit as B)
}
} else {
getContentTypesMap(bodyType).forEach { (contentType, parsers) ->
contentType(contentType) {
handle {
val receive: B = parsers.getBodyParser(call.request.contentType()).parseBody(bodyType, this)
@Suppress("UNCHECKED_CAST")
val params: P = if (paramsType.classifier == Unit::class) Unit as P else parameterHandler.parse(call.parameters, call.request.headers)
pass(this, responder, PHandler.handle(params), BHandler.handle(receive))
}
}
}
}
}
}
}
}
fun List.getResponseSerializer(contentType: ContentType): ResponseSerializer {
if (size > 1) log.warn("Multiple equal serializers for Accept $contentType: ${map { it.module::class.simpleName }}, selecting first ${first().module::class.simpleName}")
return firstOrNull()?.module ?: throw OpenAPINoSerializerException(contentType)
}
fun List.getBodyParser(contentType: ContentType): BodyParser {
if (size > 1) log.warn("Multiple equal parsers for Content-Type $contentType: ${map { it.module::class.simpleName }}, selecting first ${first().module::class.simpleName}")
return firstOrNull()?.module ?: throw OpenAPINoParserException(contentType)
}
fun getContentTypesMap(type: KType) = mapContentTypes { module.getParseableContentTypes(type) }
fun getAcceptMap(type: KType) = mapContentTypes { module.getSerializableContentTypes(type) }
inline fun mapContentTypes(noinline fn: T.() -> List): List>> {
return provider.ofType().flatMap { parser ->
parser.fn().map { Pair(it, parser) }
}.groupBy { it.first }.mapValues { it.value.map { it.second } }.map { Pair(it.key, it.value) }.sortedBy {
val ct = it.first
when {
ct.contentSubtype != "*" -> 1000
ct.contentType != "*" -> 10000
else -> 100000
} - ct.parameters.size // edge case already, no need to sort by potential wildcards too, if you do this you are already looking for problems
}
}
}
val OpenAPIRoute<*>.application get() = ktorRoute.application