Skip to content

Commit 328df27

Browse files
committed
added list parameter support
1 parent a35e1a0 commit 328df27

File tree

3 files changed

+166
-64
lines changed

3 files changed

+166
-64
lines changed

src/main/kotlin/com/papsign/ktor/openapigen/annotations/parameters/QueryParam.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.papsign.ktor.openapigen.parameters.QueryParamStyle
99
annotation class QueryParam(
1010
val description: String,
1111
val style: QueryParamStyle = QueryParamStyle.form,
12-
val explode: Boolean = false,
12+
val explode: Boolean = true,
1313
val allowEmptyValues: Boolean = false,
1414
val deprecated: Boolean = false
1515
)

src/main/kotlin/com/papsign/ktor/openapigen/parameters/parsers/ParameterHandler.kt

Lines changed: 164 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import io.ktor.http.Parameters
1111
import java.math.BigDecimal
1212
import java.math.BigInteger
1313
import java.text.SimpleDateFormat
14-
import java.time.Instant
1514
import java.util.*
1615
import kotlin.reflect.KFunction
1716
import kotlin.reflect.KParameter
1817
import kotlin.reflect.KType
1918
import kotlin.reflect.full.findAnnotation
2019
import kotlin.reflect.full.isSubclassOf
20+
import kotlin.reflect.full.isSubtypeOf
2121
import kotlin.reflect.full.primaryConstructor
2222
import kotlin.reflect.jvm.jvmErasure
2323

@@ -27,7 +27,7 @@ interface ParameterHandler<T> {
2727
val translator: OpenAPIPathSegmentTranslator
2828
}
2929

30-
object UnitParameterHandler: ParameterHandler<Unit> {
30+
object UnitParameterHandler : ParameterHandler<Unit> {
3131
override val translator: OpenAPIPathSegmentTranslator = NoTranslation
3232
override fun parse(parameters: Parameters) = Unit
3333
}
@@ -54,12 +54,6 @@ interface QueryParameterParser : ParameterParser {
5454
val explode: Boolean
5555
}
5656

57-
class PrimitiveParameterParser<T>(override val key: String, val parse: (String?) -> T) : ParameterParser {
58-
override fun parse(parameters: Parameters): T {
59-
return parse(parameters[key])
60-
}
61-
}
62-
6357
private fun <T> genPathParseFunc(key: String, style: PathParamStyle, parse: (String?) -> T): (String?) -> T {
6458
return when (style) {
6559
PathParamStyle.simple -> parse
@@ -68,24 +62,24 @@ private fun <T> genPathParseFunc(key: String, style: PathParamStyle, parse: (Str
6862
}
6963
}
7064

71-
class PrimitivePathParameterParserWrapper<T>(
72-
val parser: PrimitiveParameterParser<T>,
65+
class PrimitivePathParameterParser<T>(
66+
override val key: String,
67+
val parse: (String?) -> T,
7368
override val style: PathParamStyle,
7469
override val explode: Boolean
7570
) : PathParameterParser {
76-
override val key: String
77-
get() = parser.key
7871
override val translation: PathParameterTranslation = PathParameterTranslation(key, style, explode)
7972

80-
private val parseFunc = genPathParseFunc(key, style, parser.parse)
73+
private val parseFunc = genPathParseFunc(key, style, parse)
8174

8275
override fun parse(parameters: Parameters): Any? {
8376
return parseFunc(parameters[key])
8477
}
8578
}
8679

87-
class PrimitiveQueryParameterParserWrapper<T>(
88-
val parser: PrimitiveParameterParser<T>,
80+
class PrimitiveQueryParameterParser<T>(
81+
override val key: String,
82+
val parse: (String?) -> T,
8983
style: QueryParamStyle,
9084
override val explode: Boolean
9185
) : QueryParameterParser {
@@ -94,21 +88,22 @@ class PrimitiveQueryParameterParserWrapper<T>(
9488
log.warn("Using non-form style for primitive type, it is undefined in the OpenAPI standard, reverting to form style")
9589
}
9690

97-
override val key: String
98-
get() = parser.key
9991
override val style: QueryParamStyle = QueryParamStyle.form
10092
override val translation: QueryParameterTranslation = QueryParameterTranslation(key, this.style, explode)
10193
override fun parse(parameters: Parameters): Any? {
102-
return parser.parse(parameters[key])
94+
return parse(parameters[key])
10395
}
10496
}
10597

106-
class EnumQueryParameterParser(info: ParameterInfo, val enumMap: Map<String, *>, val nullable: Boolean): QueryParameterParser {
98+
class EnumQueryParameterParser(info: ParameterInfo, val enumMap: Map<String, *>, val nullable: Boolean) :
99+
QueryParameterParser {
107100
override val key: String = info.key
101+
108102
init {
109103
if (info.queryAnnotation?.style != QueryParamStyle.form)
110104
log.warn("Using non-form style for enum type, it is undefined in the OpenAPI standard, reverting to form style")
111105
}
106+
112107
override val style: QueryParamStyle = QueryParamStyle.form
113108
override val explode: Boolean = info.queryAnnotation!!.explode
114109
override val translation: QueryParameterTranslation = QueryParameterTranslation(key, style, explode)
@@ -118,10 +113,11 @@ class EnumQueryParameterParser(info: ParameterInfo, val enumMap: Map<String, *>,
118113
}
119114
}
120115

121-
class EnumPathParameterParser(info: ParameterInfo, val enumMap: Map<String, *>, val nullable: Boolean): PathParameterParser {
116+
class EnumPathParameterParser(info: ParameterInfo, val enumMap: Map<String, *>, val nullable: Boolean) :
117+
PathParameterParser {
122118
override val key: String = info.key
123119
override val style: PathParamStyle = info.pathAnnotation!!.style
124-
override val explode: Boolean = info.queryAnnotation!!.explode
120+
override val explode: Boolean = info.pathAnnotation!!.explode
125121
override val translation: PathParameterTranslation = PathParameterTranslation(key, style, explode)
126122

127123
private fun parse(parameter: String?): Any? {
@@ -135,37 +131,81 @@ class EnumPathParameterParser(info: ParameterInfo, val enumMap: Map<String, *>,
135131
}
136132
}
137133

138-
//class CollectionPathParameterParser<T, A>(info: ParameterInfo, val cvt: (List<T>)->A): PathParameterParser {
139-
// override val key: String = info.key
140-
// override val style: PathParamStyle = info.pathAnnotation!!.style
141-
// override val explode: Boolean = info.queryAnnotation!!.explode
142-
// override val translation: PathParameterTranslation = PathParameterTranslation(key, style, explode)
143-
//
144-
// private fun parse(parameter: String?): Any? {
145-
// return parameter?.let { enumMap[it] }
146-
// }
147-
//
148-
// private val parseFunc = when (style) {
149-
// PathParamStyle.simple -> {
150-
//
151-
// }
152-
// PathParamStyle.label -> {
153-
//
154-
// }
155-
// PathParamStyle.matrix -> {
156-
//
157-
// }
158-
// }
159-
//
160-
// override fun parse(parameters: Parameters): Any? {
161-
// return parseFunc(parameters[key])
162-
// }
163-
//}
134+
class CollectionPathParameterParser<T, A>(info: ParameterInfo, type: KType, val cvt: (List<T>?) -> A) :
135+
PathParameterParser {
136+
override val key: String = info.key
137+
override val style: PathParamStyle = info.pathAnnotation!!.style
138+
override val explode: Boolean = info.pathAnnotation!!.explode
139+
override val translation: PathParameterTranslation = PathParameterTranslation(key, style, explode)
140+
141+
private val typeParser =
142+
primitiveParsers[type] as ((String?) -> T)? ?: error("Non-primitive Arrays aren't yet supported")
143+
private val parseFunc: (String?) -> A = when (style) {
144+
PathParamStyle.simple -> ({ str: String? -> cvt(str?.split(',')?.map(typeParser)) })
145+
PathParamStyle.label -> {
146+
if (explode) {
147+
({ str: String? -> cvt(str?.split('.')?.drop(1)?.map(typeParser)) })
148+
} else {
149+
({ str: String? -> cvt(str?.removePrefix(".")?.split(',')?.map(typeParser)) })
150+
}
151+
}
152+
PathParamStyle.matrix -> {
153+
if (explode) {
154+
({ str: String? -> cvt(str?.split(";$key=")?.drop(1)?.map(typeParser)) })
155+
} else {
156+
({ str: String? -> cvt(str?.removePrefix(";$key=")?.split(',')?.map(typeParser)) })
157+
}
158+
}
159+
}
164160

161+
override fun parse(parameters: Parameters): Any? {
162+
return parseFunc(parameters[key])
163+
}
164+
}
165165

166+
class CollectionQueryParameterParser<T, A>(info: ParameterInfo, type: KType, val cvt: (List<T>?) -> A) :
167+
QueryParameterParser {
168+
override val key: String = info.key
169+
override val style: QueryParamStyle = info.queryAnnotation!!.style
170+
override val explode: Boolean = info.queryAnnotation!!.explode
171+
override val translation: QueryParameterTranslation = QueryParameterTranslation(key, style, explode)
166172

167-
inline fun <reified T> primitive(noinline cvt: (String?) -> T): Pair<KType, (String) -> PrimitiveParameterParser<T>> {
168-
return getKType<T>() to { key -> PrimitiveParameterParser(key, cvt) }
173+
private val typeParser =
174+
primitiveParsers[type] as ((String?) -> T)? ?: error("Non-primitive Arrays aren't yet supported")
175+
private val explodedParse = ({ parameters: Parameters -> cvt(parameters.getAll(key)?.map(typeParser)) })
176+
private val parseFunc: (Parameters) -> A = when (style) {
177+
QueryParamStyle.form -> {
178+
if (explode) {
179+
explodedParse
180+
} else {
181+
({ parameters: Parameters -> cvt(parameters[key]?.split(',')?.map(typeParser)) })
182+
}
183+
}
184+
QueryParamStyle.pipeDelimited -> {
185+
if (explode) {
186+
explodedParse
187+
} else {
188+
({ parameters: Parameters -> cvt(parameters[key]?.split('|')?.map(typeParser)) })
189+
}
190+
}
191+
QueryParamStyle.spaceDelimited -> {
192+
if (explode) {
193+
explodedParse
194+
} else {
195+
({ parameters: Parameters -> cvt(parameters[key]?.split(' ')?.map(typeParser)) })
196+
}
197+
}
198+
QueryParamStyle.deepObject -> error("Deep Objects are not supported for Arrays")
199+
}
200+
201+
override fun parse(parameters: Parameters): Any? {
202+
return parseFunc(parameters)
203+
}
204+
}
205+
206+
207+
inline fun <reified T> primitive(noinline cvt: (String?) -> T): Pair<KType, (String?) -> T> {
208+
return getKType<T>() to cvt
169209
}
170210

171211
private val dateFormat = SimpleDateFormat()
@@ -185,10 +225,11 @@ val primitiveParsers = mapOf(
185225
primitive { it?.toDoubleOrNull() },
186226
primitive { it?.toBoolean() ?: false },
187227
primitive { it?.toBoolean() },
188-
primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) ?: Date() },
189-
primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) },
190-
primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) ?: Instant.now() },
191-
primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) },
228+
// removed temporarily because behavior may not be standard or expected
229+
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) ?: Date() },
230+
// primitive { it?.toLongOrNull()?.let(::Date) ?: it?.let(dateFormat::parse) },
231+
// primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) ?: Instant.now() },
232+
// primitive { it?.toLongOrNull()?.let(Instant::ofEpochMilli) ?: it?.let(Instant::parse) },
192233
primitive {
193234
it?.let {
194235
try {
@@ -220,7 +261,7 @@ class ModularParameterHander<T>(val parsers: Map<KParameter, ParameterParser>, v
220261
)
221262

222263
override fun parse(parameters: Parameters): T {
223-
return constructor.callBy(parsers.mapValues { it.value.parse(parameters) })
264+
return constructor.callBy(parsers.mapValues { it.value.parse(parameters)?.also { println(it::class) } })
224265
}
225266
}
226267

@@ -235,17 +276,21 @@ inline fun <reified T : Any> buildParameterHandler(): ParameterHandler<T> {
235276
val info = ParameterInfo(key, param)
236277
primitiveParsers[type]?.let {
237278
if (info.pathAnnotation != null) {
238-
PrimitivePathParameterParserWrapper(it(key), info.pathAnnotation.style, info.pathAnnotation.explode)
279+
PrimitivePathParameterParser(key, it, info.pathAnnotation.style, info.pathAnnotation.explode)
239280
} else {
240-
PrimitiveQueryParameterParserWrapper(it(key), info.queryAnnotation!!.style, info.queryAnnotation.explode)
281+
PrimitiveQueryParameterParser(key, it, info.queryAnnotation!!.style, info.queryAnnotation.explode)
241282
}
242283
} ?: makeParameterParser(type, info)
243284
}
244285
return ModularParameterHander(parsers, constructor)
245286
}
246287

247288

248-
data class ParameterInfo(val key: String, val pathAnnotation: PathParam? = null, val queryAnnotation: QueryParam? = null) {
289+
data class ParameterInfo(
290+
val key: String,
291+
val pathAnnotation: PathParam? = null,
292+
val queryAnnotation: QueryParam? = null
293+
) {
249294
constructor(key: String, parameter: KParameter) : this(
250295
key,
251296
parameter.findAnnotation<PathParam>(),
@@ -259,7 +304,7 @@ fun makeParameterParser(type: KType, info: ParameterInfo): ParameterParser {
259304
val jclazz = clazz.java
260305
return when {
261306
jclazz.isEnum -> makeEnumParameterParser(type, info)
262-
clazz.isSubclassOf(List::class) || (jclazz.let { it.isArray && !it.componentType.isPrimitive }) -> {
307+
clazz.isSubclassOf(List::class) -> {
263308
makeListParameterParser(type, info)
264309
}
265310
jclazz.isArray -> makeArrayParameterParser(type, info)
@@ -270,20 +315,77 @@ fun makeParameterParser(type: KType, info: ParameterInfo): ParameterParser {
270315

271316
private fun makeEnumParameterParser(type: KType, info: ParameterInfo): ParameterParser {
272317
return if (info.pathAnnotation != null) {
273-
EnumPathParameterParser(info, type.jvmErasure.java.enumConstants.associateBy { (it as Enum<*>).name }, type.isMarkedNullable)
318+
EnumPathParameterParser(
319+
info,
320+
type.jvmErasure.java.enumConstants.associateBy { (it as Enum<*>).name },
321+
type.isMarkedNullable
322+
)
274323
} else {
275-
EnumQueryParameterParser(info, type.jvmErasure.java.enumConstants.associateBy { (it as Enum<*>).name }, type.isMarkedNullable)
324+
EnumQueryParameterParser(
325+
info,
326+
type.jvmErasure.java.enumConstants.associateBy { (it as Enum<*>).name },
327+
type.isMarkedNullable
328+
)
276329
}
277330
}
278331

279332
private fun makeListParameterParser(type: KType, info: ParameterInfo): ParameterParser {
280333
val contentType = type.arguments[0].type!!
281-
return TODO("Implement $type")
334+
return if (info.pathAnnotation != null) {
335+
CollectionPathParameterParser<Any?, Any?>(info, contentType) { it }
336+
} else {
337+
CollectionQueryParameterParser<Any?, Any?>(info, contentType) { it }
338+
}
339+
}
340+
341+
inline fun <reified T> primCVT(noinline cvt: (List<T>) -> Any): Pair<KType, (List<Any?>?) -> Any?> {
342+
return getKType<T>() to ({ lst ->
343+
lst?.let {
344+
cvt(
345+
it as List<T>
346+
)
347+
}
348+
})
282349
}
283350

351+
val primCVT = mapOf(
352+
primCVT<Long> { it.toLongArray() },
353+
primCVT<Int> { it.toIntArray() },
354+
primCVT<Float> { it.toFloatArray() },
355+
primCVT<Double> { it.toDoubleArray() },
356+
primCVT<Boolean> { it.toBooleanArray() }
357+
)
358+
359+
/**
360+
* you may think it is redundant but it is not. Maybe the nullable types are useless though.
361+
*/
362+
val arrCVT = mapOf(
363+
primCVT<Long> { it.toTypedArray() },
364+
primCVT<Long?> { it.toTypedArray() },
365+
primCVT<Int> { it.toTypedArray() },
366+
primCVT<Int?> { it.toTypedArray() },
367+
primCVT<Float> { it.toTypedArray() },
368+
primCVT<Float?> { it.toTypedArray() },
369+
primCVT<Double> { it.toTypedArray() },
370+
primCVT<Double?> { it.toTypedArray() },
371+
primCVT<Boolean> { it.toTypedArray() },
372+
primCVT<Boolean?> { it.toTypedArray() }
373+
)
374+
375+
284376
private fun makeArrayParameterParser(type: KType, info: ParameterInfo): ParameterParser {
285377
val contentType = type.jvmErasure.java.componentType.toKType()
286-
return TODO("Implement $type")
378+
379+
val cvt = if (type.toString().startsWith("kotlin.Array")) {
380+
arrCVT[contentType] ?: ({ lst -> lst?.toTypedArray() })
381+
} else {
382+
primCVT[contentType] ?: error("Arrays with primitive type $contentType are not supported")
383+
}
384+
return if (info.pathAnnotation != null) {
385+
CollectionPathParameterParser<Any?, Any?>(info, contentType) { cvt(it) }
386+
} else {
387+
CollectionQueryParameterParser<Any?, Any?>(info, contentType) { cvt(it) }
388+
}
287389
}
288390

289391
private fun makeObjectParameterParser(type: KType, info: ParameterInfo): ParameterParser {

src/test/kotlin/Basic.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ object Basic {
102102
@Path("string/{a}")
103103
data class StringParam(
104104
@PathParam("A simple String Param", style = PathParamStyle.matrix) val a: String,
105-
@QueryParam("Optional String") val optional: String? // Nullable Types are optional
105+
@QueryParam("Optional String") val optional: Array<Long>? // Nullable Types are optional
106106
)
107107

108108
// A response can be any class, but a description will be generated from the annotation

0 commit comments

Comments
 (0)