diff --git a/build.gradle.kts b/build.gradle.kts index 7e523af..cad5a1f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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" diff --git a/router/src/main/kotlin/io/moia/router/RequestHandler.kt b/router/src/main/kotlin/io/moia/router/RequestHandler.kt index ff32324..093b59a 100644 --- a/router/src/main/kotlin/io/moia/router/RequestHandler.kt +++ b/router/src/main/kotlin/io/moia/router/RequestHandler.kt @@ -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 { open val objectMapper = jacksonObjectMapper() @@ -49,7 +46,6 @@ abstract class RequestHandler : RequestHandler = routerFunction.handler + val handler: HandlerFunctionWrapper = routerFunction.handler val response = try { @@ -74,8 +70,8 @@ abstract class RequestHandler : RequestHandler)(request) + val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern) as Request + handler.handlerFunction(request) } } catch (e: Exception) { exceptionToResponseEntity(e, input) @@ -144,22 +140,16 @@ abstract class RequestHandler : RequestHandler PredicatePermissionHandler)? = null - @ExperimentalReflectionOnLambdas private fun deserializeRequest( - handler: HandlerFunction, + handler: HandlerFunctionWrapper, 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) } } diff --git a/router/src/main/kotlin/io/moia/router/Router.kt b/router/src/main/kotlin/io/moia/router/Router.kt index 87c2236..8dcf202 100644 --- a/router/src/main/kotlin/io/moia/router/Router.kt +++ b/router/src/main/kotlin/io/moia/router/Router.kt @@ -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, Set) -> RequestPredicate @@ -33,35 +35,35 @@ class Router(private val predicateFactory: PredicateFactory) { var filter: Filter = Filter.NoOp - fun GET( + inline fun GET( pattern: String, - handlerFunction: HandlerFunction, - ) = defaultRequestPredicate(pattern, "GET", handlerFunction, emptySet()) + crossinline handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "GET", HandlerFunctionWrapper.invoke(handlerFunction), emptySet()) - fun POST( + inline fun POST( pattern: String, - handlerFunction: HandlerFunction, - ) = defaultRequestPredicate(pattern, "POST", handlerFunction) + crossinline handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "POST", HandlerFunctionWrapper.invoke(handlerFunction)) - fun PUT( + inline fun PUT( pattern: String, - handlerFunction: HandlerFunction, - ) = defaultRequestPredicate(pattern, "PUT", handlerFunction) + crossinline handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "PUT", HandlerFunctionWrapper.invoke(handlerFunction)) - fun DELETE( + inline fun DELETE( pattern: String, - handlerFunction: HandlerFunction, - ) = defaultRequestPredicate(pattern, "DELETE", handlerFunction, emptySet()) + crossinline handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "DELETE", HandlerFunctionWrapper.invoke(handlerFunction), emptySet()) - fun PATCH( + inline fun PATCH( pattern: String, - handlerFunction: HandlerFunction, - ) = defaultRequestPredicate(pattern, "PATCH", handlerFunction) + crossinline handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "PATCH", HandlerFunctionWrapper.invoke(handlerFunction)) - private fun defaultRequestPredicate( + fun defaultRequestPredicate( pattern: String, method: String, - handlerFunction: HandlerFunction, + handlerFunction: HandlerFunctionWrapper, consuming: Set = defaultConsuming, ) = predicateFactory(method, pattern, consuming, defaultProducing) .also { routes += RouterFunction(it, handlerFunction) } @@ -108,9 +110,28 @@ fun Filter.then(next: APIGatewayRequestHandlerFunction): APIGatewayRequestHandle typealias APIGatewayRequestHandlerFunction = (APIGatewayProxyRequestEvent) -> APIGatewayProxyResponseEvent typealias HandlerFunction = (request: Request) -> ResponseEntity +abstract class HandlerFunctionWrapper { + abstract val requestType: KType + abstract val responseType: KType + + abstract val handlerFunction: HandlerFunction + + companion object { + inline operator fun invoke(crossinline handler: HandlerFunction): HandlerFunctionWrapper { + val requestType = typeOf() + val responseType = typeOf() + return object : HandlerFunctionWrapper() { + override val requestType: KType = requestType + override val responseType: KType = responseType + override val handlerFunction: HandlerFunction = { request -> handler.invoke(request) } + } + } + } +} + class RouterFunction( val requestPredicate: RequestPredicate, - val handler: HandlerFunction, + val handler: HandlerFunctionWrapper, ) { override fun toString(): String { return "RouterFunction(requestPredicate=$requestPredicate)" diff --git a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt index b51982b..7c0e5ec 100644 --- a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt @@ -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") @@ -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 { @@ -725,7 +724,8 @@ class RequestHandlerTest { GET("/some", dummy::handler).producing("application/json") } } - assertThrows { + + val response = DummyHandler().handleRequest( APIGatewayProxyRequestEvent() .withHttpMethod("GET") @@ -733,7 +733,8 @@ class RequestHandlerTest { .withAcceptHeader("application/json"), mockk(), ) - } + + assertThat(response.statusCode).isEqualTo(200) } class TestRequestHandlerAuthorization : RequestHandler() { @@ -864,16 +865,16 @@ class RequestHandlerTest { POST("/no-content") { _: Request -> ResponseEntity.noContent() } - POST("/create-without-location") { _: Request -> + POST("/create-without-location") { _: Request -> ResponseEntity.created(null, null, emptyMap()) } - POST("/create-with-location") { r: Request -> + POST("/create-with-location") { r: Request -> ResponseEntity.created(null, r.apiRequest.location("test"), emptyMap()) } DELETE("/delete-me") { _: Request -> ResponseEntity.noContent() } - GET("/non-existing-path-parameter") { request: Request -> + GET("/non-existing-path-parameter") { request: Request -> request.getPathParameter("foo") ResponseEntity.ok(null) } @@ -883,7 +884,7 @@ class RequestHandlerTest { class TestQueryParamParsingHandler : RequestHandler() { override val router = router { - GET("/search") { r: Request -> + GET("/search") { r: Request -> assertThat(r.getQueryParameter("testQueryParam")).isNotNull() assertThat(r.getQueryParameter("testQueryParam")).isEqualTo("foo") assertThat(r.queryParameters!!["testQueryParam"]).isNotNull() diff --git a/router/src/test/kotlin/io/moia/router/RouterTest.kt b/router/src/test/kotlin/io/moia/router/RouterTest.kt index e75b218..f0e2a44 100644 --- a/router/src/test/kotlin/io/moia/router/RouterTest.kt +++ b/router/src/test/kotlin/io/moia/router/RouterTest.kt @@ -117,7 +117,7 @@ class RouterTest { fun `should not consume for a deletion route`() { val router = router { - DELETE("/delete-me") { _: Request -> + DELETE("/delete-me") { _: Request -> ResponseEntity.ok(null) } }