Skip to content

Commit

Permalink
Changed module provider to work with KTypes to allow for generic spec…
Browse files Browse the repository at this point in the history
…ialization

Changed module provider to guarantee insertion order is preserved, and last insertion is at the end
added status info
  • Loading branch information
Wicpar committed Jun 15, 2020
1 parent 6593fde commit b20a531
Show file tree
Hide file tree
Showing 26 changed files with 181 additions and 72 deletions.
3 changes: 2 additions & 1 deletion src/main/kotlin/com/papsign/ktor/openapigen/OpenAPIGen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.ktor.application.call
import io.ktor.request.path
import io.ktor.util.AttributeKey
import org.reflections.Reflections
import kotlin.reflect.full.starProjectedType

class OpenAPIGen(
config: Configuration,
Expand All @@ -37,7 +38,7 @@ class OpenAPIGen(
}
}
config.removeModules.forEach(globalModuleProvider::unRegisterModule)
config.addModules.forEach(globalModuleProvider::registerModule)
config.addModules.forEach { globalModuleProvider.registerModule(it, it::class.starProjectedType) }
}

class Configuration(val api: OpenAPIModel) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.papsign.ktor.openapigen

import com.papsign.ktor.openapigen.modules.OpenAPIModule
import kotlin.reflect.full.starProjectedType

/**
* implement this to automatically register an object as [OpenAPIModule] in the global context
* only works if the object is in a package declared in [OpenAPIGen.Configuration.scanPackagesForModules]
*/
interface OpenAPIGenModuleExtension: OpenAPIModule, OpenAPIGenExtension {
override fun onInit(gen: OpenAPIGen) {
gen.globalModuleProvider.registerModule(this)
gen.globalModuleProvider.registerModule(this, this::class.starProjectedType)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.papsign.ktor.openapigen.content.type.ktor

import com.papsign.ktor.openapigen.getKType
import com.papsign.ktor.openapigen.unitKType
import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.OpenAPIGenModuleExtension
Expand All @@ -12,7 +11,7 @@ import com.papsign.ktor.openapigen.content.type.ResponseSerializer
import com.papsign.ktor.openapigen.model.operation.MediaTypeModel
import com.papsign.ktor.openapigen.model.schema.SchemaModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProviderModule
import io.ktor.application.ApplicationCall
import io.ktor.application.call
Expand Down Expand Up @@ -58,7 +57,7 @@ object KtorContentProvider : ContentTypeProvider, BodyParser, ResponseSerializer
}
}
val contentTypes = initContentTypes(apiGen) ?: return null
val schemaBuilder = provider.ofClass<FinalSchemaBuilderProviderModule>().last().provide(apiGen, provider)
val schemaBuilder = provider.ofType<FinalSchemaBuilderProviderModule>().last().provide(apiGen, provider)
@Suppress("UNCHECKED_CAST")
val media = MediaTypeModel(schemaBuilder.build(type) as SchemaModel<T>, example)
return contentTypes.associateWith { media.copy() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.papsign.ktor.openapigen.model.operation.MediaTypeEncodingModel
import com.papsign.ktor.openapigen.model.operation.MediaTypeModel
import com.papsign.ktor.openapigen.model.schema.SchemaModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProviderModule
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
Expand Down Expand Up @@ -140,7 +140,7 @@ object MultipartFormDataContentProvider : BodyParser, OpenAPIGenModuleExtension
.mapValues { MediaTypeEncodingModel(it.value!!.contentType) }
}.toMap()
}
val schemaBuilder = provider.ofClass<FinalSchemaBuilderProviderModule>().last().provide(apiGen, provider)
val schemaBuilder = provider.ofType<FinalSchemaBuilderProviderModule>().last().provide(apiGen, provider)
@Suppress("UNCHECKED_CAST")
return mapOf(ContentType.MultiPart.FormData to MediaTypeModel(schemaBuilder.build(type) as SchemaModel<T>, example, null, contentTypes))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.papsign.ktor.openapigen.interop

import com.papsign.ktor.openapigen.APIException.Companion.apiException
import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.modules.registerModule
import com.papsign.ktor.openapigen.route.ThrowsInfo
import io.ktor.application.call
import io.ktor.features.StatusPages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ package com.papsign.ktor.openapigen.modules
import java.util.*
import kotlin.collections.LinkedHashSet
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.full.superclasses

class CachingModuleProvider: ModuleProvider<CachingModuleProvider> {

@Synchronized
override fun <T : OpenAPIModule> ofClass(clazz: KClass<T>): Collection<T> {
return modules[clazz]?.let @Suppress("UNCHECKED_CAST") { it.toSet() as Set<T> } ?: setOf()
override fun ofType(type: KType): Collection<Any> {
return modules[type]?.toList() ?: listOf()
}

@Synchronized
override fun registerModule(module: OpenAPIModule) {
registerModuleForClass(module::class, module)
override fun registerModule(module: OpenAPIModule, type: KType) {
registerModuleForClass(type, module)
if (module is DependentModule) {
module.handlers.forEach(this::registerModule)
module.handlers.forEach { registerModule(it, it::class.starProjectedType) }
}
}

Expand All @@ -26,22 +28,21 @@ class CachingModuleProvider: ModuleProvider<CachingModuleProvider> {
}

@Synchronized
private fun registerModuleForClass(clazz: KClass<*>, module: OpenAPIModule) {
val lst = modules.getOrPut(clazz) {LinkedHashSet()}
if (!lst.contains(module)) {
lst.add(module)
clazz.superclasses.forEach {
registerModuleForClass(it, module)
}
private fun registerModuleForClass(type: KType, module: OpenAPIModule) {
val lst = modules.getOrPut(type) {LinkedHashSet()}
lst.remove(module)
lst.add(module)
(type.classifier as KClass<*>).supertypes.forEach {
registerModuleForClass(it, module)
}
}

private val modules = HashMap<KClass<*>, LinkedHashSet<OpenAPIModule>>()
private val modules = HashMap<KType, LinkedHashSet<OpenAPIModule>>()

@Synchronized
override fun child(): CachingModuleProvider {
val new = CachingModuleProvider()
modules.forEach { (t: KClass<*>, u) ->
modules.forEach { (t: KType, u) ->
new.modules[t] = LinkedHashSet(u)
}
return new
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.papsign.ktor.openapigen.modules

import kotlin.reflect.KClass
import com.papsign.ktor.openapigen.getKType
import kotlin.reflect.KType

interface ModuleProvider<THIS: ModuleProvider<THIS>> {
fun <T: OpenAPIModule> ofClass(clazz: KClass<T>): Collection<T>
fun registerModule(module: OpenAPIModule)
fun ofType(type: KType): Collection<Any>
fun registerModule(module: OpenAPIModule, type: KType)
fun unRegisterModule(module: OpenAPIModule)
fun child(): THIS
}

inline fun <reified T: OpenAPIModule> ModuleProvider<*>.ofClass(): Collection<T> {
return ofClass(T::class)
}
inline fun <reified T: OpenAPIModule> ModuleProvider<*>.ofType(): Collection<T> {
return ofType(getKType<T>()) as Collection<T>
}

inline fun <reified T: OpenAPIModule> ModuleProvider<*>.registerModule(module: T) {
return registerModule(module, getKType<T>())
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.model.operation.OperationModel
import com.papsign.ktor.openapigen.model.security.SecurityModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
import com.papsign.ktor.openapigen.modules.providers.AuthProvider

object AuthHandler: OperationModule {

override fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>, operation: OperationModel) {
val authHandlers = provider.ofClass<AuthProvider<*>>()
val authHandlers = provider.ofType<AuthProvider<*>>()
val security = authHandlers.flatMap { it.security }.distinct()
operation.security = security.map { SecurityModel().also { sec ->
it.forEach { sec[it.scheme.name] = it.requirements }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import com.papsign.ktor.openapigen.content.type.SelectedParser
import com.papsign.ktor.openapigen.model.operation.OperationModel
import com.papsign.ktor.openapigen.model.operation.RequestBodyModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
import com.papsign.ktor.openapigen.modules.providers.ParameterProvider
import com.papsign.ktor.openapigen.modules.registerModule
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
Expand All @@ -26,7 +27,7 @@ class RequestHandlerModule<T : Any>(
private val log = classLogger()

override fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>, operation: OperationModel) {
val map = provider.ofClass<BodyParser>().mapNotNull {
val map = provider.ofType<BodyParser>().mapNotNull {
val mediaType = it.getMediaType(requestType, apiGen, provider, requestExample, ContentTypeProvider.Usage.PARSE)
?: return@mapNotNull null
provider.registerModule(SelectedParser(it))
Expand All @@ -35,7 +36,7 @@ class RequestHandlerModule<T : Any>(

val requestMeta = requestClass.findAnnotation<Request>()

val parameters = provider.ofClass<ParameterProvider>().flatMap { it.getParameters(apiGen, provider) }
val parameters = provider.ofType<ParameterProvider>().flatMap { it.getParameters(apiGen, provider) }
operation.parameters = operation.parameters?.let { (it + parameters).distinct() } ?: parameters
operation.requestBody = operation.requestBody?.apply {
map.forEach { (key, value) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ import com.papsign.ktor.openapigen.content.type.SelectedSerializer
import com.papsign.ktor.openapigen.model.operation.OperationModel
import com.papsign.ktor.openapigen.model.operation.StatusResponseModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
import com.papsign.ktor.openapigen.modules.providers.StatusProvider
import com.papsign.ktor.openapigen.modules.registerModule
import io.ktor.http.HttpStatusCode
import kotlin.reflect.KClass
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation

class ResponseHandlerModule<T : Any>(val responseClass: KClass<T>, val responseType: KType, val responseExample: T? = null) : OperationModule {
class ResponseHandlerModule<T>(val responseType: KType, val responseExample: T? = null) : OperationModule {
private val log = classLogger()
override fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>, operation: OperationModel) {

val responseMeta = responseClass.findAnnotation<Response>()
val statusCode = responseMeta?.statusCode?.let { HttpStatusCode.fromValue(it) } ?: HttpStatusCode.OK
val responseMeta = (responseType.classifier as? KAnnotatedElement)?.findAnnotation<Response>()
val statusCode = provider.ofType<StatusProvider>().lastOrNull()?.getStatusForType(responseType) ?: responseMeta?.statusCode?.let { HttpStatusCode.fromValue(it) }
?: HttpStatusCode.OK
val status = statusCode.value.toString()
val map = provider.ofClass<ResponseSerializer>().mapNotNull {
val map = provider.ofType<ResponseSerializer>().mapNotNull {
val mediaType = it.getMediaType(responseType, apiGen, provider, responseExample, ContentTypeProvider.Usage.SERIALIZE)
?: return@mapNotNull null
provider.registerModule(SelectedSerializer(it))
Expand All @@ -44,7 +46,6 @@ class ResponseHandlerModule<T : Any>(val responseClass: KClass<T>, val responseT
}

companion object {
inline fun <reified T : Any> create(responseExample: T? = null) = ResponseHandlerModule(T::class,
getKType<T>(), responseExample)
inline fun <reified T : Any> create(responseExample: T? = null) = ResponseHandlerModule(getKType<T>(), responseExample)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.papsign.ktor.openapigen.content.type.SelectedModule
import com.papsign.ktor.openapigen.model.base.PathItemModel
import com.papsign.ktor.openapigen.model.operation.OperationModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.HandlerModule
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
import com.papsign.ktor.openapigen.modules.providers.MethodProvider
Expand All @@ -18,11 +18,11 @@ object RouteHandler: HandlerModule {
private val log: Logger = LoggerFactory.getLogger(this::class.java)

override fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>) {
val methods = provider.ofClass<MethodProvider>()
val methods = provider.ofType<MethodProvider>()
if (methods.size > 1) error("API cannot have two methods simultaneously: ${methods.map { it.method.value }}")
val paths = provider.ofClass<PathProvider>()
val paths = provider.ofType<PathProvider>()
val path = "/${paths.flatMap { it.path.split('/').filter(String::isNotEmpty) }.joinToString("/")}"
val operationModules = provider.ofClass<OperationModule>()
val operationModules = provider.ofType<OperationModule>()
apiGen.api.paths.getOrPut(path) { PathItemModel() }.also {pathItem ->
methods.forEach {
val name = it.method.value.toLowerCase()
Expand All @@ -33,6 +33,6 @@ object RouteHandler: HandlerModule {
}
}
}
log.trace("Registered $path::${methods.map { it.method.value }} with OpenAPI description with ${provider.ofClass<SelectedModule>().map { it.module::class.simpleName }}")
log.trace("Registered $path::${methods.map { it.method.value }} with OpenAPI description with ${provider.ofType<SelectedModule>().map { it.module::class.simpleName }}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package com.papsign.ktor.openapigen.modules.handlers
import com.papsign.ktor.openapigen.OpenAPIGen
import com.papsign.ktor.openapigen.model.operation.OperationModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
import com.papsign.ktor.openapigen.modules.providers.TagProviderModule

object TagHandlerModule: OperationModule {
override fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>, operation: OperationModel) {
val tags = provider.ofClass<TagProviderModule>().flatMap { it.tags.map(apiGen::getOrRegisterTag) }
val tags = provider.ofType<TagProviderModule>().flatMap { it.tags.map(apiGen::getOrRegisterTag) }
val current = operation.tags
if (current != null) {
operation.tags = (tags + current).distinct()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ import com.papsign.ktor.openapigen.model.operation.OperationModel
import com.papsign.ktor.openapigen.model.operation.StatusResponseModel
import com.papsign.ktor.openapigen.model.schema.SchemaModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
import com.papsign.ktor.openapigen.modules.providers.ThrowInfoProvider
import com.papsign.ktor.openapigen.modules.registerModule

object ThrowOperationHandler : OperationModule {
private val log = classLogger()
override fun configure(apiGen: OpenAPIGen, provider: ModuleProvider<*>, operation: OperationModel) {

val exceptions = provider.ofClass<ThrowInfoProvider>().flatMap { it.exceptions }
val exceptions = provider.ofType<ThrowInfoProvider>().flatMap { it.exceptions }
exceptions.groupBy { it.status }.forEach { exceptions ->
val map: MutableMap<String, MediaTypeModel<*>> = exceptions.value.flatMap { ex ->
provider.ofClass<ResponseSerializer>().mapNotNull {
provider.ofType<ResponseSerializer>().mapNotNull {
if (ex.contentType == unitKType) return@mapNotNull null
val mediaType = it.getMediaType(ex.contentType, apiGen, provider, ex.example, ContentTypeProvider.Usage.SERIALIZE) ?: return@mapNotNull null
provider.registerModule(SelectedExceptionSerializer(it))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.papsign.ktor.openapigen.modules.providers

import com.papsign.ktor.openapigen.modules.OpenAPIModule
import io.ktor.http.HttpStatusCode
import kotlin.reflect.KType

interface StatusProvider : OpenAPIModule {
fun getStatusForType(responseType: KType): HttpStatusCode
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.papsign.ktor.openapigen.model.operation.ParameterLocation
import com.papsign.ktor.openapigen.model.operation.ParameterModel
import com.papsign.ktor.openapigen.model.schema.SchemaModel
import com.papsign.ktor.openapigen.modules.ModuleProvider
import com.papsign.ktor.openapigen.modules.ofClass
import com.papsign.ktor.openapigen.modules.ofType
import com.papsign.ktor.openapigen.parameters.parsers.builders.Builder
import com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProviderModule
import io.ktor.http.Headers
Expand All @@ -30,7 +30,7 @@ class ModularParameterHandler<T>(val parsers: Map<KParameter, Builder<*>>, val c
}

override fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<ParameterModel<*>> {
val schemaBuilder = provider.ofClass<FinalSchemaBuilderProviderModule>().last().provide(apiGen, provider)
val schemaBuilder = provider.ofType<FinalSchemaBuilderProviderModule>().last().provide(apiGen, provider)

fun createParam(param: KParameter, `in`: ParameterLocation, config: (ParameterModel<*>) -> Unit): ParameterModel<*> {
return ParameterModel<Any>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.papsign.ktor.openapigen.annotations.Path
import com.papsign.ktor.openapigen.content.type.ContentTypeProvider
import com.papsign.ktor.openapigen.modules.handlers.RequestHandlerModule
import com.papsign.ktor.openapigen.modules.handlers.ResponseHandlerModule
import com.papsign.ktor.openapigen.modules.registerModule
import com.papsign.ktor.openapigen.route.modules.HttpMethodProviderModule
import com.papsign.ktor.openapigen.route.modules.PathProviderModule
import io.ktor.http.HttpMethod
Expand Down
Loading

0 comments on commit b20a531

Please sign in to comment.