From eaf6490b4f9f50fb10a4f069cbf6ea8b827c0901 Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Wed, 1 Jan 2025 16:09:31 +0100 Subject: [PATCH] GH-31 Support auto-boxing of nullable parameters with Optional (Resolve #31) --- .../annotations/ReflectiveEndpointLoader.kt | 64 +++++++++++++------ .../annotations/AnnotatedRoutingTest.kt | 4 ++ 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt index 074d739..09fe4b8 100644 --- a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt +++ b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt @@ -8,9 +8,13 @@ import io.javalin.event.JavalinLifecycleEvent import io.javalin.http.Context import io.javalin.http.HttpStatus import io.javalin.validation.Validation +import io.javalin.validation.Validator import java.lang.reflect.AnnotatedElement import java.lang.reflect.Method import java.lang.reflect.Parameter +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.util.* import kotlin.reflect.KClass typealias AnnotatedRoute = DefaultDslRoute @@ -62,8 +66,9 @@ internal class ReflectiveEndpointLoader( "Unable to access method $method in class $endpointClass" } - val argumentSuppliers = method.parameters.map { - createArgumentSupplier(it) ?: throw IllegalArgumentException("Unsupported parameter type: $it") + val types = method.genericParameterTypes + val argumentSuppliers = method.parameters.mapIndexed { idx, parameter -> + createArgumentSupplier(parameter, types[idx]) ?: throw IllegalArgumentException("Unsupported parameter type: $parameter") } val status = method.getAnnotation() @@ -103,8 +108,9 @@ internal class ReflectiveEndpointLoader( "Unable to access method $method in class $endpointClass" } - val argumentSuppliers = method.parameters.map { - createArgumentSupplier(it) ?: throw IllegalArgumentException("Unsupported parameter type: $it") + val types = method.genericParameterTypes + val argumentSuppliers = method.parameters.mapIndexed { idx, parameter -> + createArgumentSupplier(parameter, types[idx]) ?: throw IllegalArgumentException("Unsupported parameter type: $parameter") } val status = method.getAnnotation() @@ -194,49 +200,62 @@ internal class ReflectiveEndpointLoader( private inline fun createArgumentSupplier( parameter: Parameter, + parameterType: Type, noinline custom: (Context, CUSTOM) -> Any? = { _, self -> self } - ): ((Context, CUSTOM) -> Any?)? = - with (parameter) { + ): ((Context, CUSTOM) -> Any?)? { + val isOptional = parameter.type.isAssignableFrom(Optional::class.java) + + val expectedType = when { + isOptional -> (parameterType as ParameterizedType).actualTypeArguments[0] + else -> parameterType + } + val expectedTypeAsClass = expectedType as Class<*> + + return with(parameter) { when { - CUSTOM::class.java.isAssignableFrom(type) -> - custom - type.isAssignableFrom(Context::class.java) -> { ctx, _ -> + CUSTOM::class.java.isAssignableFrom(expectedTypeAsClass) -> { ctx, c -> + when { + isOptional -> Optional.ofNullable(custom(ctx, c)) + else -> custom(ctx, c) + } + } + expectedTypeAsClass.isAssignableFrom(Context::class.java) -> { ctx, _ -> ctx } isAnnotationPresent() -> { ctx, _ -> getAnnotationOrThrow() .value .ifEmpty { name } - .let { ctx.pathParamAsClass(it, type) } - .get() + .let { ctx.pathParamAsClass(it, expectedTypeAsClass) } + .getValidatorValue(optional = isOptional) } isAnnotationPresent
() -> { ctx, _ -> getAnnotationOrThrow
() .value .ifEmpty { name } - .let { ctx.headerAsClass(it, type) } - .get() + .let { ctx.headerAsClass(it, expectedTypeAsClass) } + .getValidatorValue(optional = isOptional) } isAnnotationPresent() -> { ctx, _ -> getAnnotationOrThrow() .value .ifEmpty { name } - .let { ctx.queryParamAsClass(it, type) } - .get() + .let { ctx.queryParamAsClass(it, expectedTypeAsClass) } + .getValidatorValue(optional = isOptional) } isAnnotationPresent
() -> { ctx, _ -> getAnnotationOrThrow() .value .ifEmpty { name } - .let { ctx.formParamAsClass(it, type) } - .get() + .let { ctx.formParamAsClass(it, expectedTypeAsClass) } + .getValidatorValue(optional = isOptional) } isAnnotationPresent() -> { ctx, _ -> getAnnotationOrThrow() .value .ifEmpty { name } - .let { Validation().validator(it, type, ctx.cookie(it)) } - .get() + .let { Validation().validator(it, expectedTypeAsClass, ctx.cookie(it)) } + .getValidatorValue(optional = isOptional) } isAnnotationPresent() -> { ctx, _ -> ctx.bodyAsClass(parameter.parameterizedType) @@ -244,6 +263,13 @@ internal class ReflectiveEndpointLoader( else -> null } } + } + + private fun Validator.getValidatorValue(optional: Boolean): Any = + when { + optional -> Optional.ofNullable(allowNullable().get()) + else -> get() + } private inline fun AnnotatedElement.isAnnotationPresent(): Boolean = isAnnotationPresent(A::class.java) diff --git a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt index c46d412..558f8be 100644 --- a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt +++ b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt @@ -14,6 +14,7 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow +import java.util.* class AnnotatedRoutingTest { @@ -145,12 +146,14 @@ class AnnotatedRoutingTest { ctx: Context, @Param param: Int, @Header header: Int, + @Header optionalHeader: Optional, @Query query: Int, @Cookie cookie: Int, @Body body: Int, ) { ctx.header("param", param.toString()) ctx.header("header", header.toString()) + ctx.header("optionalHeader", optionalHeader.orElse("default")) ctx.header("query", query.toString()) ctx.header("cookie", cookie.toString()) ctx.header("body", body.toString()) @@ -170,6 +173,7 @@ class AnnotatedRoutingTest { assertThat(responseHeaders.getFirst("param")).isEqualTo("1") assertThat(responseHeaders.getFirst("header")).isEqualTo("2") + assertThat(responseHeaders.getFirst("optionalHeader")).isEqualTo("default") assertThat(responseHeaders.getFirst("query")).isEqualTo("3") assertThat(responseHeaders.getFirst("cookie")).isEqualTo("4") assertThat(responseHeaders.getFirst("body")).isEqualTo("5")