Skip to content

Commit

Permalink
#62 introduce queryList for multiple params with the same name, and a…
Browse files Browse the repository at this point in the history
…lso bind it to a lists when using annotated routes
  • Loading branch information
angryziber committed Dec 15, 2024
1 parent 71daf53 commit 94dabc7
Show file tree
Hide file tree
Showing 5 changed files with 23 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* server: `Server(InetSocketAddress(0))` can now be used to bind to any available port.
* server: make idle restarts quick #96
* server: initialize `HttpExchange.pathParams` even no route is matched (404), for decorators #82
* server: put list into queryParams if user specifies multiple parameters with the same name, introduce `e.queryList` #62
* jdbc: between operator introduced with open and closed ranges, also in and notIn
* jdbc: @NoTransaction can now be used on jobs
* jdbc: fixed usage of multiple different DataSources when there is an active transaction
Expand Down
1 change: 1 addition & 0 deletions server/src/klite/HttpExchange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ open class HttpExchange(
val query: String get() = original.requestURI.query?.let { "?$it" } ?: ""
val queryParams: Params by lazy { original.requestURI.queryParams }
fun query(param: String): String? = queryParams[param]
fun queryList(param: String) = (queryParams[param] as Any?).asList<String>()
inline fun <reified T: Any> query(param: String): T? = query(param)?.let { Converter.from<T>(it) }

val fullUrl get() = fullUrl(original.requestURI.toString())
Expand Down
8 changes: 8 additions & 0 deletions server/src/klite/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ val URI.queryParams: Params get() = urlDecodeParams(rawQuery)

fun urlEncodeParams(params: Map<String, Any?>) = params.mapNotNull { e -> e.value?.let { e.key + "=" + it.toString().urlEncode() } }.joinToString("&")

@Suppress("UNCHECKED_CAST")
fun urlDecodeParams(params: String?): Params = params?.split('&')?.fold(mutableMapOf<String, Any?>()) { m, p ->
val (name, value) = keyValue(p)
m.apply {
Expand All @@ -36,3 +37,10 @@ internal fun keyValue(s: String) = s.split('=', limit = 2).let { it[0] to it.get

operator fun URI.plus(suffix: String) = URI(toString().substringBefore("#") + suffix + (fragment?.let { "#$it" } ?: ""))
operator fun URI.plus(params: Map<String, Any?>) = plus((if (rawQuery == null) "?" else "&") + urlEncodeParams(params))

@Suppress("UNCHECKED_CAST")
fun <T> Any?.asList(): List<T> = when (this) {
null -> emptyList()
is List<*> -> this as List<T>
else -> listOf(this as T)
}
10 changes: 9 additions & 1 deletion server/src/klite/annotations/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import kotlin.reflect.KParameter
import kotlin.reflect.KParameter.Kind.INSTANCE
import kotlin.reflect.full.callSuspendBy
import kotlin.reflect.full.functions
import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.jvm.javaMethod
import kotlin.reflect.jvm.jvmErasure

@Target(CLASS) annotation class Path(val value: String)
@Target(FUNCTION) annotation class GET(val value: String = "")
Expand Down Expand Up @@ -83,7 +85,10 @@ class Param(val p: KParameter) {
else {
when (source) {
is PathParam -> e.path(name)?.toType()
is QueryParam -> e.query(name).let { if (it == null && p.type.classifier == Boolean::class && name in e.queryParams) true else it?.toType() }
is QueryParam ->
if (p.type.jvmErasure.isSuperclassOf(List::class)) e.queryList(name).map { Converter.from<Any>(it, p.type.arguments[0].type!!) }
else (e.query(name)).let { if (it !is String) it else if (e.isValueLessQueryParam(it)) true else it.toType()
}
is HeaderParam -> e.header(name)?.toType()
is CookieParam -> e.cookie(name)?.toType()
is SessionParam -> e.session[name]?.toType()
Expand All @@ -97,6 +102,9 @@ class Param(val p: KParameter) {
throw IllegalArgumentException("Cannot get $name: ${e.message}", e)
}

private fun HttpExchange.isValueLessQueryParam(v: String?) =
v == null && p.type.classifier == Boolean::class && name in queryParams

private val Annotation.value: String? get() = (javaClass.getMethod("value").invoke(this) as String).takeIf { it.isNotEmpty() }
private fun String.toType() = Converter.from<Any>(this, p.type)
}
Expand Down
7 changes: 4 additions & 3 deletions server/test/klite/annotations/AnnotationsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class AnnotationsTest {
@GET("/hello/:world")
fun generic(body: String,
@PathParam world: BigDecimal, @QueryParam date: LocalDate, @HeaderParam header: Long,
@CookieParam cookie: Locale, @AttrParam attr: BigInteger, @BodyParam file: FileUpload) =
"Hello $body $world $date $header $cookie $attr ${file.fileName}"
@CookieParam cookie: Locale, @AttrParam attr: BigInteger, @QueryParam list: List<Int>, @BodyParam file: FileUpload) =
"Hello $body $world $date $header $cookie $attr $list ${file.fileName}"

@GET("/hello/specific") @CustomAnnotation("method") fun specific() = "Hello"

Expand Down Expand Up @@ -72,10 +72,11 @@ class AnnotationsTest {
every { exchange.body<FileUpload>("file") } returns FileUpload("name.txt", "text/plain", "".byteInputStream())
every { exchange.path("world") } returns "7.9e9"
every { exchange.query("date") } returns "2021-10-21"
every { exchange.queryList("list") } returns listOf("5", "77")
every { exchange.header("header") } returns "42"
every { exchange.cookie("cookie") } returns "et"
every { exchange.attr<BigInteger>("attr") } returns BigInteger("90909")
runBlocking { expect(handler(exchange)).toEqual("Hello TheBody 7.9E+9 2021-10-21 42 et 90909 name.txt") }
runBlocking { expect(handler(exchange)).toEqual("Hello TheBody 7.9E+9 2021-10-21 42 et 90909 [5, 77] name.txt") }
}

@Test fun `nicer exception if getting parameter value fails`() {
Expand Down

0 comments on commit 94dabc7

Please sign in to comment.