Support Kotlin parameter default values in handler methods
This commit adds support for Kotlin parameter default values in handler methods. It allows to write: @RequestParam value: String = "default" as an alternative to: @RequestParam(defaultValue = "default") value: String Both Spring MVC and WebFlux are supported, including on suspending functions. Closes gh-21139
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,16 +21,21 @@ import io.mockk.mockk
|
||||
import kotlinx.coroutines.delay
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.ReactiveAdapterRegistry
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest.get
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.reactive.BindingContext
|
||||
import org.springframework.web.reactive.HandlerResult
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver
|
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod
|
||||
import org.springframework.web.reactive.result.method.annotation.ContinuationHandlerMethodArgumentResolver
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestParamMethodArgumentResolver
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest.get
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier
|
||||
import java.lang.reflect.Method
|
||||
@@ -39,9 +44,10 @@ import kotlin.reflect.jvm.javaMethod
|
||||
|
||||
class KotlinInvocableHandlerMethodTests {
|
||||
|
||||
private val exchange = MockServerWebExchange.from(get("http://localhost:8080/path"))
|
||||
private var exchange = MockServerWebExchange.from(get("http://localhost:8080/path"))
|
||||
|
||||
private val resolvers = mutableListOf<HandlerMethodArgumentResolver>(ContinuationHandlerMethodArgumentResolver())
|
||||
private val resolvers = mutableListOf<HandlerMethodArgumentResolver>(ContinuationHandlerMethodArgumentResolver(),
|
||||
RequestParamMethodArgumentResolver(null, ReactiveAdapterRegistry.getSharedInstance(), false))
|
||||
|
||||
@Test
|
||||
fun resolveNoArg() {
|
||||
@@ -104,6 +110,58 @@ class KotlinInvocableHandlerMethodTests {
|
||||
assertHandlerResultValue(result, "success:foo")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultValue() {
|
||||
this.resolvers.add(stubResolver(Mono.empty()))
|
||||
val method = DefaultValueController::handle.javaMethod!!
|
||||
val result = invoke(DefaultValueController(), method)
|
||||
assertHandlerResultValue(result, "default")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultValueOverridden() {
|
||||
this.resolvers.add(stubResolver(Mono.empty()))
|
||||
val method = DefaultValueController::handle.javaMethod!!
|
||||
exchange = MockServerWebExchange.from(get("http://localhost:8080/path").queryParam("value", "override"))
|
||||
val result = invoke(DefaultValueController(), method)
|
||||
assertHandlerResultValue(result, "override")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultValues() {
|
||||
this.resolvers.add(stubResolver(Mono.empty()))
|
||||
val method = DefaultValueController::handleMultiple.javaMethod!!
|
||||
val result = invoke(DefaultValueController(), method)
|
||||
assertHandlerResultValue(result, "10-20")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultValuesOverridden() {
|
||||
this.resolvers.add(stubResolver(Mono.empty()))
|
||||
val method = DefaultValueController::handleMultiple.javaMethod!!
|
||||
exchange = MockServerWebExchange.from(get("http://localhost:8080/path").queryParam("limit2", "40"))
|
||||
val result = invoke(DefaultValueController(), method)
|
||||
assertHandlerResultValue(result, "10-40")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingDefaultValue() {
|
||||
this.resolvers.add(stubResolver(Mono.empty()))
|
||||
val method = DefaultValueController::handleSuspending.javaMethod!!
|
||||
val result = invoke(DefaultValueController(), method)
|
||||
assertHandlerResultValue(result, "default")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingDefaultValueOverridden() {
|
||||
this.resolvers.add(stubResolver(Mono.empty()))
|
||||
val method = DefaultValueController::handleSuspending.javaMethod!!
|
||||
exchange = MockServerWebExchange.from(get("http://localhost:8080/path").queryParam("value", "override"))
|
||||
val result = invoke(DefaultValueController(), method)
|
||||
assertHandlerResultValue(result, "override")
|
||||
}
|
||||
|
||||
|
||||
private fun invokeForResult(handler: Any, method: Method, vararg providedArgs: Any): HandlerResult? {
|
||||
return invoke(handler, method, *providedArgs).block(Duration.ofSeconds(5))
|
||||
}
|
||||
@@ -127,8 +185,13 @@ class KotlinInvocableHandlerMethodTests {
|
||||
|
||||
private fun assertHandlerResultValue(mono: Mono<HandlerResult>, expected: String) {
|
||||
StepVerifier.create(mono)
|
||||
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).expectNext(expected).verifyComplete() }
|
||||
.verifyComplete()
|
||||
.consumeNextWith {
|
||||
if (it.returnValue is Mono<*>) {
|
||||
StepVerifier.create(it.returnValue as Mono<*>).expectNext(expected).verifyComplete()
|
||||
} else {
|
||||
assertThat(it.returnValue).isEqualTo(expected)
|
||||
}
|
||||
}.verifyComplete()
|
||||
}
|
||||
|
||||
class CoroutinesController {
|
||||
@@ -166,4 +229,16 @@ class KotlinInvocableHandlerMethodTests {
|
||||
return "success:$q"
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
class DefaultValueController {
|
||||
|
||||
fun handle(@RequestParam value: String = "default") = value
|
||||
|
||||
fun handleMultiple(@RequestParam(defaultValue = "10") limit1: Int, @RequestParam limit2: Int = 20) = "${limit1}-${limit2}"
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
suspend fun handleSuspending(@RequestParam value: String = "default") = value
|
||||
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,13 @@ class RequestParamMethodArgumentResolverKotlinTests {
|
||||
lateinit var nonNullableParamRequired: MethodParameter
|
||||
lateinit var nonNullableParamNotRequired: MethodParameter
|
||||
|
||||
lateinit var defaultValueBooleanParamRequired: MethodParameter
|
||||
lateinit var defaultValueBooleanParamNotRequired: MethodParameter
|
||||
lateinit var defaultValueIntParamRequired: MethodParameter
|
||||
lateinit var defaultValueIntParamNotRequired: MethodParameter
|
||||
lateinit var defaultValueStringParamRequired: MethodParameter
|
||||
lateinit var defaultValueStringParamNotRequired: MethodParameter
|
||||
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
@@ -54,13 +61,22 @@ class RequestParamMethodArgumentResolverKotlinTests {
|
||||
initializer.conversionService = DefaultFormattingConversionService()
|
||||
bindingContext = BindingContext(initializer)
|
||||
|
||||
val method = ReflectionUtils.findMethod(javaClass, "handle", String::class.java,
|
||||
String::class.java, String::class.java, String::class.java)!!
|
||||
val method = ReflectionUtils.findMethod(javaClass, "handle",
|
||||
String::class.java, String::class.java, String::class.java, String::class.java,
|
||||
Boolean::class.java, Boolean::class.java, Int::class.java, Int::class.java,
|
||||
String::class.java, String::class.java)!!
|
||||
|
||||
nullableParamRequired = SynthesizingMethodParameter(method, 0)
|
||||
nullableParamNotRequired = SynthesizingMethodParameter(method, 1)
|
||||
nonNullableParamRequired = SynthesizingMethodParameter(method, 2)
|
||||
nonNullableParamNotRequired = SynthesizingMethodParameter(method, 3)
|
||||
|
||||
defaultValueBooleanParamRequired = SynthesizingMethodParameter(method, 4)
|
||||
defaultValueBooleanParamNotRequired = SynthesizingMethodParameter(method, 5)
|
||||
defaultValueIntParamRequired = SynthesizingMethodParameter(method, 6)
|
||||
defaultValueIntParamNotRequired = SynthesizingMethodParameter(method, 7)
|
||||
defaultValueStringParamRequired = SynthesizingMethodParameter(method, 8)
|
||||
defaultValueStringParamNotRequired = SynthesizingMethodParameter(method, 9)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,13 +135,104 @@ class RequestParamMethodArgumentResolverKotlinTests {
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueRequiredWithBooleanParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=false"))
|
||||
val result = resolver.resolveArgument(defaultValueBooleanParamRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectNext(false).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueRequiredWithoutBooleanParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"))
|
||||
val result = resolver.resolveArgument(defaultValueBooleanParamRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueNotRequiredWithBooleanParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=false"))
|
||||
val result = resolver.resolveArgument(defaultValueBooleanParamNotRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectNext(false).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueNotRequiredWithoutBooleanParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"))
|
||||
val result = resolver.resolveArgument(defaultValueBooleanParamNotRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueRequiredWithIntParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=123"))
|
||||
val result = resolver.resolveArgument(defaultValueIntParamRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectNext(123).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueRequiredWithoutIntParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"))
|
||||
val result = resolver.resolveArgument(defaultValueIntParamRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueNotRequiredWithIntParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=123"))
|
||||
val result = resolver.resolveArgument(defaultValueIntParamNotRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectNext(123).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueNotRequiredWithoutIntParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"))
|
||||
val result = resolver.resolveArgument(defaultValueIntParamNotRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueRequiredWithStringParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=123"))
|
||||
val result = resolver.resolveArgument(defaultValueStringParamRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectNext("123").expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueRequiredWithoutStringParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"))
|
||||
val result = resolver.resolveArgument(defaultValueStringParamRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueNotRequiredWithStringParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=123"))
|
||||
val result = resolver.resolveArgument(defaultValueStringParamNotRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectNext("123").expectComplete().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDefaultValueNotRequiredWithoutStringParameter() {
|
||||
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"))
|
||||
val result = resolver.resolveArgument(defaultValueStringParamNotRequired, bindingContext, exchange)
|
||||
StepVerifier.create(result).expectComplete().verify()
|
||||
}
|
||||
|
||||
|
||||
@Suppress("unused_parameter")
|
||||
fun handle(
|
||||
@RequestParam("name") nullableParamRequired: String?,
|
||||
@RequestParam("name", required = false) nullableParamNotRequired: String?,
|
||||
@RequestParam("name") nonNullableParamRequired: String,
|
||||
@RequestParam("name", required = false) nonNullableParamNotRequired: String) {
|
||||
@RequestParam("name", required = false) nonNullableParamNotRequired: String,
|
||||
|
||||
@RequestParam("value") withDefaultValueBooleanParamRequired: Boolean = true,
|
||||
@RequestParam("value", required = false) withDefaultValueBooleanParamNotRequired: Boolean = true,
|
||||
@RequestParam("value") withDefaultValueIntParamRequired: Int = 20,
|
||||
@RequestParam("value", required = false) withDefaultValueIntParamNotRequired: Int = 20,
|
||||
@RequestParam("value") withDefaultValueStringParamRequired: String = "default",
|
||||
@RequestParam("value", required = false) withDefaultValueStringParamNotRequired: String = "default") {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user