Skip to content

Commit c364af5

Browse files
committed
refactored ParamBuilder.kt, fixed parameters not updating style in openapi description, added deep objects
1 parent 174fa41 commit c364af5

File tree

12 files changed

+188
-94
lines changed

12 files changed

+188
-94
lines changed

src/main/kotlin/com/papsign/ktor/openapigen/modules/handlers/RequestHandlerModule.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.papsign.ktor.openapigen.classLogger
77
import com.papsign.ktor.openapigen.content.type.BodyParser
88
import com.papsign.ktor.openapigen.content.type.ContentTypeProvider
99
import com.papsign.ktor.openapigen.content.type.SelectedParser
10-
import com.papsign.ktor.openapigen.parameters.ParamBuilder
1110
import com.papsign.ktor.openapigen.modules.ModuleProvider
1211
import com.papsign.ktor.openapigen.modules.ofClass
1312
import com.papsign.ktor.openapigen.modules.openapi.OperationModule
@@ -36,7 +35,7 @@ class RequestHandlerModule<T : Any>(
3635

3736
val requestMeta = requestClass.findAnnotation<Request>()
3837

39-
val parameters = provider.ofClass<ParameterProvider>().flatMap { it.getParameters(ParamBuilder(apiGen, provider)) }
38+
val parameters = provider.ofClass<ParameterProvider>().flatMap { it.getParameters(apiGen, provider) }
4039
operation.parameters = operation.parameters?.let { (it + parameters).distinct() } ?: parameters
4140
operation.requestBody = operation.requestBody?.apply {
4241
map.forEach { (key, value) ->
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.papsign.ktor.openapigen.modules.providers
22

3-
import com.papsign.ktor.openapigen.parameters.ParamBuilder
3+
import com.papsign.ktor.openapigen.OpenAPIGen
4+
import com.papsign.ktor.openapigen.modules.ModuleProvider
45
import com.papsign.ktor.openapigen.modules.OpenAPIModule
56
import com.papsign.ktor.openapigen.openapi.Parameter
67

78
interface ParameterProvider: OpenAPIModule {
8-
fun getParameters(builder: ParamBuilder): List<Parameter<*>>
9+
fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<Parameter<*>>
910
}

src/main/kotlin/com/papsign/ktor/openapigen/parameters/ParamBuilder.kt

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,68 @@
11
package com.papsign.ktor.openapigen.parameters.handlers
22

3+
import com.papsign.ktor.openapigen.OpenAPIGen
4+
import com.papsign.ktor.openapigen.annotations.parameters.PathParam
5+
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
6+
import com.papsign.ktor.openapigen.annotations.parameters.apiParam
7+
import com.papsign.ktor.openapigen.modules.ModuleProvider
8+
import com.papsign.ktor.openapigen.openapi.Parameter
9+
import com.papsign.ktor.openapigen.openapi.ParameterLocation
10+
import com.papsign.ktor.openapigen.openapi.Schema
311
import com.papsign.ktor.openapigen.parameters.parsers.ParameterParser
412
import io.ktor.http.Parameters
513
import kotlin.reflect.KFunction
614
import kotlin.reflect.KParameter
15+
import kotlin.reflect.full.findAnnotation
716

817
class ModularParameterHander<T>(val parsers: Map<KParameter, ParameterParser>, val constructor: KFunction<T>) :
918
ParameterHandler<T> {
1019

1120
override fun parse(parameters: Parameters): T {
1221
return constructor.callBy(parsers.mapValues { it.value.parse(parameters) })
1322
}
23+
24+
override fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<Parameter<*>> {
25+
26+
fun createParam(param: KParameter, `in`: ParameterLocation, config: (Parameter<*>) -> Unit): Parameter<*> {
27+
return Parameter<Any>(
28+
param.name.toString(),
29+
`in`,
30+
!param.type.isMarkedNullable
31+
).also {
32+
it.schema = apiGen.schemaRegistrar[param.type].schema as Schema<Any>
33+
config(it)
34+
}
35+
}
36+
37+
fun QueryParam.createParam(param: KParameter): Parameter<*> {
38+
val parser = parsers[param]!!
39+
return createParam(param, apiParam.`in`) {
40+
it.description = description
41+
it.allowEmptyValue = allowEmptyValues
42+
it.deprecated = deprecated
43+
it.style = parser.queryStyle
44+
it.explode = parser.explode
45+
}
46+
}
47+
48+
fun PathParam.createParam(param: KParameter): Parameter<*> {
49+
val parser = parsers[param]!!
50+
return createParam(param, apiParam.`in`) {
51+
it.description = description
52+
it.deprecated = deprecated
53+
it.style = parser.pathStyle
54+
it.explode = parser.explode
55+
}
56+
}
57+
58+
return constructor.parameters.map {
59+
it.findAnnotation<PathParam>()?.createParam(it) ?:
60+
it.findAnnotation<QueryParam>()?.createParam(it) ?:
61+
error("API routes with ${constructor.returnType} must have parameters annotated with one of ${paramAnnotationClasses.map { it.simpleName }}")
62+
}
63+
}
64+
65+
companion object {
66+
private val paramAnnotationClasses = hashSetOf(PathParam::class, QueryParam::class)
67+
}
1468
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.papsign.ktor.openapigen.parameters.handlers
22

3+
import com.papsign.ktor.openapigen.modules.providers.ParameterProvider
34
import io.ktor.http.Parameters
45

5-
interface ParameterHandler<T> {
6+
interface ParameterHandler<T>: ParameterProvider {
67
fun parse(parameters: Parameters): T
78
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.papsign.ktor.openapigen.parameters.parsers
2+
3+
import com.papsign.ktor.openapigen.parameters.QueryParamStyle
4+
import com.papsign.ktor.openapigen.parameters.util.*
5+
import io.ktor.http.Parameters
6+
import kotlin.reflect.KFunction
7+
import kotlin.reflect.KParameter
8+
import kotlin.reflect.KType
9+
import kotlin.reflect.full.isSubclassOf
10+
import kotlin.reflect.full.primaryConstructor
11+
import kotlin.reflect.jvm.jvmErasure
12+
13+
class ObjectParameterParser(info: ParameterInfo, type: KType) : InfoParameterParser(info, {
14+
when (it) {
15+
QueryParamStyle.DEFAULT, QueryParamStyle.deepObject -> QueryParamStyle.deepObject
16+
QueryParamStyle.form -> error("Due to unmanageable ambiguities the form style is forbidden for objects")
17+
else -> error("Query param style $it is undefined for objects in the OpenAPI Spec")
18+
}
19+
}) {
20+
21+
private val builder: (Map<String, Any?>) -> Any?
22+
private val cvt: (Parameters) -> Any?
23+
24+
init {
25+
val kclass = type.jvmErasure
26+
if (kclass.isData) {
27+
val constructor = kclass.primaryConstructor ?: error("Parameter objects must have primary constructors")
28+
val parameters = constructor.parameters
29+
val parameterMap = parameters.associateBy { it.name!! }
30+
builder = { map -> constructor.callBy(map.mapKeys { parameterMap[it.key] }.filterKeys { it != null }.mapKeys { it.key!! }) }
31+
cvt = when (queryStyle) {
32+
QueryParamStyle.deepObject -> {
33+
val builder = getBuilderForType(type)
34+
({ builder.build(key, it) })
35+
}
36+
null -> error("Only query params can hold objects")
37+
else -> error("Query param style $queryStyle is not supported")
38+
}
39+
} else {
40+
error("Only data classes are currently supported as parameter objects")
41+
}
42+
}
43+
44+
45+
override fun parse(parameters: Parameters): Any? {
46+
return cvt(parameters)
47+
}
48+
49+
companion object {
50+
51+
interface DeepBuilder {
52+
fun build(path: String, parameters: Parameters): Any?
53+
}
54+
55+
class ObjectBuilder(val type: KType) : DeepBuilder {
56+
private val builderMap: Map<KParameter, DeepBuilder>
57+
private val constructor: KFunction<Any>
58+
init {
59+
val kclass = type.jvmErasure
60+
if (kclass.isData) {
61+
constructor = kclass.primaryConstructor ?: error("Parameter objects must have primary constructors")
62+
val parameters = constructor.parameters
63+
builderMap = parameters.associate { parameter ->
64+
parameter to getBuilderForType(parameter.type)
65+
}
66+
} else {
67+
error("Only data classes are currently supported as parameter objects")
68+
}
69+
}
70+
71+
override fun build(path: String, parameters: Parameters): Any? {
72+
return constructor.callBy(builderMap.mapValues { it.value.build("$path[${it.key.name}]", parameters) })
73+
}
74+
}
75+
76+
class PrimitiveBuilder(val cvt: (String?)->Any?): DeepBuilder {
77+
override fun build(path: String, parameters: Parameters): Any? {
78+
return cvt(parameters[path])
79+
}
80+
}
81+
82+
class EnumBuilder(val enumMap: Map<String, Any?>): DeepBuilder {
83+
override fun build(path: String, parameters: Parameters): Any? {
84+
return parameters[path]?.let { enumMap[it] }
85+
}
86+
}
87+
88+
class ListBuilder(val type: KType): DeepBuilder {
89+
private val contentType = type.arguments[0].type!!
90+
private val builder = getBuilderForType(contentType)
91+
override fun build(path: String, parameters: Parameters): Any? {
92+
val names = parameters.names().filter { it.startsWith(path) }.toSet()
93+
val indices = names.map { it.substring(path.length + 1, it.indexOf("]", path.length)).toInt() }.toSet()
94+
return indices.fold(ArrayList<Any?>().also { it.ensureCapacity(indices.max()?:0) }) { acc, idx ->
95+
acc[idx] = builder.build("$path[$idx]", parameters)
96+
acc
97+
}
98+
}
99+
}
100+
101+
private fun getBuilderForType(type: KType): DeepBuilder {
102+
primitiveParsers[type]?.let { return PrimitiveBuilder(it) }
103+
val clazz = type.jvmErasure
104+
val jclazz = clazz.java
105+
return when {
106+
jclazz.isEnum -> EnumBuilder(jclazz.enumConstants.associateBy { it.toString() })
107+
clazz.isSubclassOf(List::class) -> ListBuilder(type)
108+
jclazz.isArray -> error("Nested array are not yet supported")
109+
clazz.isSubclassOf(Map::class) -> error("Nested maps are not yet supported")
110+
else -> ObjectBuilder(type)
111+
}
112+
}
113+
}
114+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package com.papsign.ktor.openapigen.parameters.parsers
22

3+
import com.papsign.ktor.openapigen.OpenAPIGen
4+
import com.papsign.ktor.openapigen.modules.ModuleProvider
5+
import com.papsign.ktor.openapigen.openapi.Parameter
36
import com.papsign.ktor.openapigen.parameters.handlers.ParameterHandler
47
import io.ktor.http.Parameters
58

69
object UnitParameterHandler :
710
ParameterHandler<Unit> {
811
override fun parse(parameters: Parameters) = Unit
12+
override fun getParameters(apiGen: OpenAPIGen, provider: ModuleProvider<*>): List<Parameter<*>> {
13+
return listOf()
14+
}
915
}

src/main/kotlin/com/papsign/ktor/openapigen/parameters/util/Util.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,7 @@ private fun makeArrayParameterParser(type: KType, info: ParameterInfo): Paramete
183183
}
184184

185185
private fun makeObjectParameterParser(type: KType, info: ParameterInfo): ParameterParser {
186-
TODO("Implement $type")
187-
val erasure = type.jvmErasure
188-
// if (erasure.isSealed) {
189-
// return Schema.OneSchemaOf(erasure.sealedSubclasses.map { get(it.starProjectedType).schema })
190-
// }
191-
// val props = erasure.declaredMemberProperties.filter { it.visibility == KVisibility.PUBLIC }
192-
// val properties = props.associate {
193-
// Pair(it.name, get(it.returnType).schema)
194-
// }
195-
// if (properties.isEmpty()) log.warn("No public properties found in object $type")
196-
// return Schema.SchemaObj<Any>(
197-
// properties,
198-
// props.filter { !it.returnType.isMarkedNullable }.map { it.name })
186+
return ObjectParameterParser(info, type)
199187
}
200188

201189
private fun makeMapParameterParser(type: KType, info: ParameterInfo): ParameterParser {

src/main/kotlin/com/papsign/ktor/openapigen/route/Functions.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.papsign.ktor.openapigen.content.type.ContentTypeProvider
66
import com.papsign.ktor.openapigen.modules.handlers.RequestHandlerModule
77
import com.papsign.ktor.openapigen.modules.handlers.ResponseHandlerModule
88
import com.papsign.ktor.openapigen.route.modules.HttpMethodProviderModule
9-
import com.papsign.ktor.openapigen.route.modules.ParamClassProviderModule
109
import com.papsign.ktor.openapigen.route.modules.PathProviderModule
1110
import io.ktor.http.HttpMethod
1211
import io.ktor.routing.HttpMethodRouteSelector
@@ -83,8 +82,6 @@ inline fun <reified P : Any, reified R : Any, reified B : Any, T: OpenAPIRoute<T
8382
if (path != null) {
8483
provider.registerModule(PathProviderModule(path.path))
8584
}
86-
if (Unit !is P)
87-
provider.registerModule(ParamClassProviderModule(P::class))
8885
handle()
8986
}
90-
}
87+
}

src/main/kotlin/com/papsign/ktor/openapigen/route/OpenAPIRoute.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ abstract class OpenAPIRoute<T : OpenAPIRoute<T>>(val ktorRoute: Route, val provi
3333
crossinline pass: suspend OpenAPIRoute<*>.(pipeline: PipelineContext<Unit, ApplicationCall>, responder: Responder, P, B) -> Unit
3434
) {
3535
val parameterHandler = buildParameterHandler<P>()
36+
provider.registerModule(parameterHandler)
3637

3738
val apiGen = ktorRoute.application.openAPIGen
3839
provider.ofClass<HandlerModule>().forEach {

0 commit comments

Comments
 (0)