Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin 2.0 #383

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ buildscript {

plugins {
java
kotlin("jvm") version "1.9.23"
kotlin("jvm") version "2.0.0"
`maven-publish`
jacoco
id("com.github.kt3k.coveralls") version "2.12.2"
Expand Down
26 changes: 8 additions & 18 deletions router/src/main/kotlin/io/moia/router/RequestHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ import com.google.common.net.MediaType
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.reflect.KClass
import kotlin.reflect.jvm.ExperimentalReflectionOnLambdas
import kotlin.reflect.jvm.reflect

@Suppress("UnstableApiUsage")
abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
open val objectMapper = jacksonObjectMapper()

Expand All @@ -49,7 +46,6 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
.apply { headers = headers.mapKeys { it.key.lowercase() } }
.let { router.filter.then(this::handleRequest)(it) }

@ExperimentalReflectionOnLambdas
@Suppress("UNCHECKED_CAST")
private fun handleRequest(input: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent {
log.debug(
Expand All @@ -66,16 +62,16 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
routerFunction.requestPredicate.matchedAcceptType(input.acceptedMediaTypes())
?: MediaType.parse(router.defaultContentType)

val handler: HandlerFunction<Any, Any> = routerFunction.handler
val handler: HandlerFunctionWrapper<Any, Any> = routerFunction.handler

val response =
try {
if (missingPermissions(input, routerFunction)) {
throw ApiException("missing permissions", "MISSING_PERMISSIONS", 403)
} else {
val requestBody = deserializeRequest(handler, input)
val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern)
(handler as HandlerFunction<*, *>)(request)
val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern) as Request<Any>
handler.handlerFunction(request)
}
} catch (e: Exception) {
exceptionToResponseEntity(e, input)
Expand Down Expand Up @@ -144,22 +140,16 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG

open fun predicatePermissionHandlerSupplier(): ((r: APIGatewayProxyRequestEvent) -> PredicatePermissionHandler)? = null

@ExperimentalReflectionOnLambdas
private fun deserializeRequest(
handler: HandlerFunction<Any, Any>,
handler: HandlerFunctionWrapper<Any, Any>,
input: APIGatewayProxyRequestEvent,
): Any? {
val requestType =
handler.reflect()?.parameters?.first()?.type?.arguments?.first()?.type
?: throw IllegalArgumentException(
"reflection failed, try using a real lambda instead of function references (Kotlin 1.6 bug?)",
)
return when {
requestType.classifier as KClass<*> == Unit::class -> Unit
input.body == null && requestType.isMarkedNullable -> null
handler.requestType.classifier as KClass<*> == Unit::class -> Unit
input.body == null && handler.requestType.isMarkedNullable -> null
input.body == null -> throw ApiException("no request body present", "REQUEST_BODY_MISSING", 400)
input.body is String && requestType.classifier as KClass<*> == String::class -> input.body
else -> deserializationHandlerChain.deserialize(input, requestType)
input.body is String && handler.requestType.classifier as KClass<*> == String::class -> input.body
else -> deserializationHandlerChain.deserialize(input, handler.requestType)
}
}

Expand Down
57 changes: 39 additions & 18 deletions router/src/main/kotlin/io/moia/router/Router.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package io.moia.router
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent.ProxyRequestContext
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
import kotlin.reflect.KType
import kotlin.reflect.typeOf

typealias PredicateFactory = (String, String, Set<String>, Set<String>) -> RequestPredicate

Expand All @@ -33,35 +35,35 @@ class Router(private val predicateFactory: PredicateFactory) {

var filter: Filter = Filter.NoOp

fun <I, T> GET(
inline fun <reified I, reified T> GET(
pattern: String,
handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "GET", handlerFunction, emptySet())
crossinline handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "GET", HandlerFunctionWrapper.invoke(handlerFunction), emptySet())

fun <I, T> POST(
inline fun <reified I, reified T> POST(
pattern: String,
handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "POST", handlerFunction)
crossinline handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "POST", HandlerFunctionWrapper.invoke(handlerFunction))

fun <I, T> PUT(
inline fun <reified I, reified T> PUT(
pattern: String,
handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "PUT", handlerFunction)
crossinline handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "PUT", HandlerFunctionWrapper.invoke(handlerFunction))

fun <I, T> DELETE(
inline fun <reified I, reified T> DELETE(
pattern: String,
handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "DELETE", handlerFunction, emptySet())
crossinline handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "DELETE", HandlerFunctionWrapper.invoke(handlerFunction), emptySet())

fun <I, T> PATCH(
inline fun <reified I, reified T> PATCH(
pattern: String,
handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "PATCH", handlerFunction)
crossinline handlerFunction: HandlerFunction<I, T>,
) = defaultRequestPredicate(pattern, "PATCH", HandlerFunctionWrapper.invoke(handlerFunction))

private fun <I, T> defaultRequestPredicate(
fun <I, T> defaultRequestPredicate(
pattern: String,
method: String,
handlerFunction: HandlerFunction<I, T>,
handlerFunction: HandlerFunctionWrapper<I, T>,
consuming: Set<String> = defaultConsuming,
) = predicateFactory(method, pattern, consuming, defaultProducing)
.also { routes += RouterFunction(it, handlerFunction) }
Expand Down Expand Up @@ -108,9 +110,28 @@ fun Filter.then(next: APIGatewayRequestHandlerFunction): APIGatewayRequestHandle
typealias APIGatewayRequestHandlerFunction = (APIGatewayProxyRequestEvent) -> APIGatewayProxyResponseEvent
typealias HandlerFunction<I, T> = (request: Request<I>) -> ResponseEntity<T>

abstract class HandlerFunctionWrapper<I, T> {
abstract val requestType: KType
abstract val responseType: KType

abstract val handlerFunction: HandlerFunction<I, T>

companion object {
inline operator fun <reified I, reified T> invoke(crossinline handler: HandlerFunction<I, T>): HandlerFunctionWrapper<I, T> {
val requestType = typeOf<I>()
val responseType = typeOf<T>()
return object : HandlerFunctionWrapper<I, T>() {
override val requestType: KType = requestType
override val responseType: KType = responseType
override val handlerFunction: HandlerFunction<I, T> = { request -> handler.invoke(request) }
}
}
}
}

class RouterFunction<I, T>(
val requestPredicate: RequestPredicate,
val handler: HandlerFunction<I, T>,
val handler: HandlerFunctionWrapper<I, T>,
) {
override fun toString(): String {
return "RouterFunction(requestPredicate=$requestPredicate)"
Expand Down
17 changes: 9 additions & 8 deletions router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import io.mockk.mockk
import io.moia.router.Router.Companion.router
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.time.LocalDate

@Suppress("ktlint:standard:max-line-length")
Expand Down Expand Up @@ -710,7 +709,7 @@ class RequestHandlerTest {
}

@Test
fun `should fail for function references when using Kotlin 1_6_10`() {
fun `should be able to use function references as handler`() {
class DummyHandler : RequestHandler() {
val dummy =
object {
Expand All @@ -725,15 +724,17 @@ class RequestHandlerTest {
GET("/some", dummy::handler).producing("application/json")
}
}
assertThrows<IllegalArgumentException> {

val response =
DummyHandler().handleRequest(
APIGatewayProxyRequestEvent()
.withHttpMethod("GET")
.withPath("/some")
.withAcceptHeader("application/json"),
mockk(),
)
}

assertThat(response.statusCode).isEqualTo(200)
}

class TestRequestHandlerAuthorization : RequestHandler() {
Expand Down Expand Up @@ -864,16 +865,16 @@ class RequestHandlerTest {
POST("/no-content") { _: Request<TestRequest> ->
ResponseEntity.noContent()
}
POST("/create-without-location") { _: Request<TestRequest> ->
POST<TestRequest, Unit>("/create-without-location") { _: Request<TestRequest> ->
ResponseEntity.created(null, null, emptyMap())
}
POST("/create-with-location") { r: Request<TestRequest> ->
POST<TestRequest, Unit>("/create-with-location") { r: Request<TestRequest> ->
ResponseEntity.created(null, r.apiRequest.location("test"), emptyMap())
}
DELETE("/delete-me") { _: Request<Unit> ->
ResponseEntity.noContent()
}
GET("/non-existing-path-parameter") { request: Request<Unit> ->
GET<Unit, Unit>("/non-existing-path-parameter") { request: Request<Unit> ->
request.getPathParameter("foo")
ResponseEntity.ok(null)
}
Expand All @@ -883,7 +884,7 @@ class RequestHandlerTest {
class TestQueryParamParsingHandler : RequestHandler() {
override val router =
router {
GET("/search") { r: Request<TestRequestHandler.TestRequest> ->
GET<TestRequestHandler.TestRequest, Unit>("/search") { r: Request<TestRequestHandler.TestRequest> ->
assertThat(r.getQueryParameter("testQueryParam")).isNotNull()
assertThat(r.getQueryParameter("testQueryParam")).isEqualTo("foo")
assertThat(r.queryParameters!!["testQueryParam"]).isNotNull()
Expand Down
2 changes: 1 addition & 1 deletion router/src/test/kotlin/io/moia/router/RouterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class RouterTest {
fun `should not consume for a deletion route`() {
val router =
router {
DELETE("/delete-me") { _: Request<Unit> ->
DELETE<Unit, Unit>("/delete-me") { _: Request<Unit> ->
ResponseEntity.ok(null)
}
}
Expand Down
Loading