Skip to content

Commit 31ae8ad

Browse files
committed
- improved Serializaton/Deserialization modules to allow multiple simultaneous modules based on accept and content type
- created ktor route selectors based on content type and accept depending on the support of the modules - separated handling of requests and responses to allow finer control - reworked the inline dsl to work around compiler bugs - added tests for Serialization/Deserialization modules
1 parent 2cb2d2e commit 31ae8ad

26 files changed

+389
-100
lines changed

src/main/kotlin/com/papsign/ktor/openapigen/OpenAPIGen.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
package com.papsign.ktor.openapigen
22

3-
import com.papsign.ktor.openapigen.annotations.encodings.APIEncoding
3+
import com.papsign.ktor.openapigen.annotations.encodings.APIFormatter
44
import com.papsign.ktor.openapigen.content.type.ContentTypeProvider
55
import com.papsign.ktor.openapigen.modules.CachingModuleProvider
66
import com.papsign.ktor.openapigen.modules.schema.*
77
import com.papsign.ktor.openapigen.openapi.ExternalDocumentation
88
import com.papsign.ktor.openapigen.openapi.OpenAPI
99
import com.papsign.ktor.openapigen.openapi.Schema.SchemaRef
1010
import com.papsign.ktor.openapigen.openapi.Server
11-
import io.ktor.application.ApplicationCallPipeline
12-
import io.ktor.application.ApplicationFeature
13-
import io.ktor.application.call
11+
import io.ktor.application.*
1412
import io.ktor.request.path
1513
import io.ktor.util.AttributeKey
1614
import org.reflections.Reflections
1715
import kotlin.reflect.KType
1816

19-
class OpenAPIGen(private val config: Configuration) {
17+
class OpenAPIGen(
18+
private val config: Configuration,
19+
@Deprecated("Will be replaced with less dangerous alternative when the use case has been fleshed out.") val pipeline: ApplicationCallPipeline
20+
) {
2021

2122
val api = config.api
2223

@@ -35,7 +36,7 @@ class OpenAPIGen(private val config: Configuration) {
3536

3637
init {
3738
val reflections = Reflections(this::class.java.`package`.name)
38-
val classes = reflections.getTypesAnnotatedWith(APIEncoding::class.java).mapNotNull { it.kotlin.objectInstance }
39+
val classes = reflections.getTypesAnnotatedWith(APIFormatter::class.java).mapNotNull { it.kotlin.objectInstance }
3940
classes.forEach {
4041
when (it) {
4142
is ContentTypeProvider -> {
@@ -118,7 +119,7 @@ class OpenAPIGen(private val config: Configuration) {
118119
ui.serve(call.request.path().removePrefix(cmp), call)
119120
}
120121
}
121-
return OpenAPIGen(cfg)
122+
return OpenAPIGen(cfg, pipeline)
122123
}
123124
}
124125
}

src/main/kotlin/com/papsign/ktor/openapigen/annotations/encodings/APIEncoding.kt renamed to src/main/kotlin/com/papsign/ktor/openapigen/annotations/encodings/APIFormatter.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.papsign.ktor.openapigen.annotations.encodings
22

3-
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
4-
@Retention(AnnotationRetention.RUNTIME)
53
/**
64
* must be applied to parser or serializer object, or annotation to mark it as Encoding Selector
75
*/
8-
annotation class APIEncoding
6+
@Target(AnnotationTarget.CLASS)
7+
@Retention(AnnotationRetention.RUNTIME)
8+
annotation class APIFormatter
9+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.papsign.ktor.openapigen.annotations.encodings
2+
3+
@Target(AnnotationTarget.CLASS)
4+
@Retention(AnnotationRetention.RUNTIME)
5+
annotation class APIRequestFormat
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.papsign.ktor.openapigen.annotations.encodings
2+
3+
@Target(AnnotationTarget.CLASS)
4+
@Retention(AnnotationRetention.RUNTIME)
5+
annotation class APIResponseFormat
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.papsign.ktor.openapigen.content.type
22

33
import io.ktor.application.ApplicationCall
4+
import io.ktor.http.ContentType
45
import io.ktor.util.pipeline.PipelineContext
56
import kotlin.reflect.KClass
67

78
interface BodyParser: ContentTypeProvider {
9+
fun <T: Any> getParseableContentTypes(clazz: KClass<T>): List<ContentType>
810
suspend fun <T: Any> parseBody(clazz: KClass<T>, request: PipelineContext<Unit, ApplicationCall>): T
911
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.papsign.ktor.openapigen.content.type
2+
3+
import com.papsign.ktor.openapigen.route.response.Responder
4+
import io.ktor.application.ApplicationCall
5+
import io.ktor.http.ContentType
6+
import io.ktor.http.HttpStatusCode
7+
import io.ktor.util.pipeline.PipelineContext
8+
9+
class ContentTypeResponder(val responseSerializer: ResponseSerializer, val contentType: ContentType): Responder {
10+
override suspend fun <T : Any> respond(response: T, request: PipelineContext<Unit, ApplicationCall>) {
11+
responseSerializer.respond(response, request, contentType)
12+
}
13+
14+
override suspend fun <T : Any> respond(statusCode: HttpStatusCode, response: T, request: PipelineContext<Unit, ApplicationCall>) {
15+
responseSerializer.respond(statusCode, response, request, contentType)
16+
}
17+
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package com.papsign.ktor.openapigen.content.type
22

3+
import io.ktor.application.Application
34
import io.ktor.application.ApplicationCall
45
import io.ktor.http.ContentType
56
import io.ktor.http.HttpStatusCode
67
import io.ktor.util.pipeline.PipelineContext
8+
import kotlin.reflect.KClass
79

810
interface ResponseSerializer: ContentTypeProvider {
911
/**
1012
* used to determine which registered response serializer is used, based on the accept header
1113
*/
12-
fun accept(contentType: ContentType): Boolean
13-
suspend fun <T: Any> respond(response: T, request: PipelineContext<Unit, ApplicationCall>)
14-
suspend fun <T: Any> respond(statusCode: HttpStatusCode, response: T, request: PipelineContext<Unit, ApplicationCall>)
14+
fun <T: Any> getSerializableContentTypes(clazz: KClass<T>): List<ContentType>
15+
suspend fun <T: Any> respond(response: T, request: PipelineContext<Unit, ApplicationCall>, contentType: ContentType)
16+
suspend fun <T: Any> respond(statusCode: HttpStatusCode, response: T, request: PipelineContext<Unit, ApplicationCall>, contentType: ContentType)
1517
}

src/main/kotlin/com/papsign/ktor/openapigen/content/type/binary/BinaryContentTypeParser.kt

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import com.papsign.kotlin.reflection.getKType
44
import com.papsign.kotlin.reflection.getObjectSubtypes
55
import com.papsign.ktor.openapigen.OpenAPIGen
66
import com.papsign.ktor.openapigen.annotations.Response
7-
import com.papsign.ktor.openapigen.annotations.encodings.APIEncoding
7+
import com.papsign.ktor.openapigen.annotations.encodings.APIFormatter
88
import com.papsign.ktor.openapigen.content.type.BodyParser
99
import com.papsign.ktor.openapigen.content.type.ContentTypeProvider
1010
import com.papsign.ktor.openapigen.content.type.ResponseSerializer
@@ -29,18 +29,23 @@ import kotlin.reflect.full.declaredMemberProperties
2929
import kotlin.reflect.full.findAnnotation
3030
import kotlin.reflect.jvm.jvmErasure
3131

32-
@APIEncoding
32+
@APIFormatter
3333
object BinaryContentTypeParser: BodyParser, ResponseSerializer {
3434

35-
override fun accept(contentType: ContentType): Boolean = true
35+
override fun <T : Any> getParseableContentTypes(clazz: KClass<T>): List<ContentType> {
36+
return clazz.findAnnotation<BinaryRequest>()?.contentTypes?.map(ContentType.Companion::parse) ?: listOf()
37+
}
38+
39+
override fun <T: Any> getSerializableContentTypes(clazz: KClass<T>): List<ContentType> {
40+
return clazz.findAnnotation<BinaryResponse>()?.contentTypes?.map(ContentType.Companion::parse) ?: listOf()
41+
}
3642

37-
override suspend fun <T : Any> respond(response: T, request: PipelineContext<Unit, ApplicationCall>) {
43+
override suspend fun <T : Any> respond(response: T, request: PipelineContext<Unit, ApplicationCall>, contentType: ContentType) {
3844
val code = response::class.findAnnotation<Response>()?.statusCode?.let { HttpStatusCode.fromValue(it) } ?: HttpStatusCode.OK
39-
respond(code, response, request)
45+
respond(code, response, request, contentType)
4046
}
4147

42-
override suspend fun <T : Any> respond(statusCode: HttpStatusCode, response: T, request: PipelineContext<Unit, ApplicationCall>) {
43-
val contentType = ContentType.parse(response::class.findAnnotation<BinaryRequest>()!!.contentTypes[0])
48+
override suspend fun <T : Any> respond(statusCode: HttpStatusCode, response: T, request: PipelineContext<Unit, ApplicationCall>, contentType: ContentType) {
4449
@Suppress("UNCHECKED_CAST")
4550
val prop = response::class.declaredMemberProperties.first { it.visibility == KVisibility.PUBLIC } as KProperty1<T, *>
4651
val data = prop.get(response) as InputStream
@@ -53,7 +58,18 @@ object BinaryContentTypeParser: BodyParser, ResponseSerializer {
5358
}
5459

5560
override fun <T> getMediaType(type: KType, apiGen: OpenAPIGen, provider: ModuleProvider<*>, example: T?, usage: ContentTypeProvider.Usage): Map<ContentType, MediaType<T>>? {
56-
val binaryRequest = type.jvmErasure.findAnnotation<BinaryRequest>() ?: return null
61+
val contentTypes = when(usage) {
62+
ContentTypeProvider.Usage.PARSE -> {
63+
val binaryRequest = type.jvmErasure.findAnnotation<BinaryRequest>() ?: return null
64+
binaryRequest.contentTypes
65+
}
66+
ContentTypeProvider.Usage.SERIALIZE -> {
67+
val binaryRequest = type.jvmErasure.findAnnotation<BinaryResponse>() ?: return null
68+
binaryRequest.contentTypes
69+
}
70+
}.also {
71+
it.forEach { ContentType.parse(it) }
72+
}
5773
val subtypes = type.getObjectSubtypes()
5874
assertContent (acceptedTypes.containsAll(subtypes)) {
5975
"${this::class.simpleName} can only be used with type ${acceptedTypes.joinToString()}, you are using ${subtypes.minus(acceptedTypes)}"
@@ -65,17 +81,14 @@ object BinaryContentTypeParser: BodyParser, ResponseSerializer {
6581
}
6682
}
6783
ContentTypeProvider.Usage.SERIALIZE -> {
68-
assertContent(binaryRequest.contentTypes.size == 1) {
69-
"${this::class.simpleName} allows exactly 1 content type when serializing, but you provide ${binaryRequest.contentTypes.size}"
70-
}
7184
val public = type.jvmErasure.declaredMemberProperties.filter { it.visibility == KVisibility.PUBLIC }
7285
assertContent(public.size == 1 && public.all { acceptedTypes.contains(it.returnType) }) {
7386
"${this::class.simpleName} must provide exactly 1 public member property of type $acceptedTypes"
7487
}
7588
}
7689
}
7790
val mediaType = MediaType(Schema.SchemaLitteral(DataType.string, DataFormat.binary), example)
78-
return binaryRequest.contentTypes.map(ContentType.Companion::parse).associateWith { mediaType }
91+
return contentTypes.map(ContentType.Companion::parse).associateWith { mediaType.copy() }
7992
}
8093

8194
private val acceptedTypes = setOf(getKType<InputStream>())
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.papsign.ktor.openapigen.content.type.binary
22

3-
import com.papsign.ktor.openapigen.annotations.encodings.APIEncoding
3+
import com.papsign.ktor.openapigen.annotations.encodings.APIRequestFormat
44

55
@Target(AnnotationTarget.CLASS)
66
@Retention(AnnotationRetention.RUNTIME)
7-
@APIEncoding
8-
annotation class BinaryRequest(val contentTypes: Array<String>)
7+
@APIRequestFormat
8+
annotation class BinaryRequest(val contentTypes: Array<String>)
9+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.papsign.ktor.openapigen.content.type.binary
2+
3+
import com.papsign.ktor.openapigen.annotations.encodings.APIResponseFormat
4+
5+
@Target(AnnotationTarget.CLASS)
6+
@Retention(AnnotationRetention.RUNTIME)
7+
@APIResponseFormat
8+
annotation class BinaryResponse(val contentTypes: Array<String>)

0 commit comments

Comments
 (0)