Skip to content

Commit 4179138

Browse files
committed
Support for nullable request parameters in Kotlin. Fixes #2006
1 parent 2603a8b commit 4179138

File tree

9 files changed

+335
-102
lines changed

9 files changed

+335
-102
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocKotlinConfiguration.java

-93
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.springdoc.core.configuration
2+
3+
import com.fasterxml.jackson.module.kotlin.KotlinModule
4+
import io.swagger.v3.oas.models.media.ByteArraySchema
5+
import org.springdoc.core.customizers.ParameterCustomizer
6+
import org.springdoc.core.parsers.KotlinCoroutinesReturnTypeParser
7+
import org.springdoc.core.providers.ObjectMapperProvider
8+
import org.springdoc.core.utils.Constants
9+
import org.springdoc.core.utils.SpringDocUtils
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
12+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
13+
import org.springframework.context.annotation.Bean
14+
import org.springframework.context.annotation.Configuration
15+
import org.springframework.context.annotation.Lazy
16+
import org.springframework.core.MethodParameter
17+
import kotlin.coroutines.Continuation
18+
import kotlin.reflect.KParameter
19+
import kotlin.reflect.jvm.kotlinFunction
20+
21+
/**
22+
* The type Spring doc kotlin configuration.
23+
* @author bnasslahsen
24+
*/
25+
@Lazy(false)
26+
@Configuration(proxyBeanMethods = false)
27+
@ConditionalOnProperty(name = [Constants.SPRINGDOC_ENABLED], matchIfMissing = true)
28+
@ConditionalOnBean(
29+
SpringDocConfiguration::class
30+
)
31+
open class SpringDocKotlinConfiguration(objectMapperProvider: ObjectMapperProvider) {
32+
/**
33+
* Instantiates a new Spring doc kotlin configuration.
34+
*
35+
*/
36+
init {
37+
SpringDocUtils.getConfig()
38+
.addRequestWrapperToIgnore(Continuation::class.java)
39+
.replaceWithSchema(ByteArray::class.java, ByteArraySchema())
40+
.addDeprecatedType(Deprecated::class.java)
41+
objectMapperProvider.jsonMapper().registerModule(KotlinModule.Builder().build())
42+
}
43+
44+
/**
45+
* Kotlin coroutines return type parser kotlin coroutines return type parser.
46+
*
47+
* @return the kotlin coroutines return type parser
48+
*/
49+
@Bean
50+
@Lazy(false)
51+
@ConditionalOnMissingBean
52+
open fun kotlinCoroutinesReturnTypeParser(): KotlinCoroutinesReturnTypeParser {
53+
return KotlinCoroutinesReturnTypeParser()
54+
}
55+
56+
/**
57+
* Kotlin springdoc-openapi ParameterCustomizer
58+
*
59+
* @return the nullable Kotlin Request Parameter Customizer
60+
*/
61+
@Bean
62+
@Lazy(false)
63+
@ConditionalOnMissingBean
64+
open fun nullableKotlinRequestParameterCustomizer(): ParameterCustomizer {
65+
return ParameterCustomizer { parameterModel, methodParameter ->
66+
if (parameterModel == null) return@ParameterCustomizer null
67+
val kParameter = methodParameter.toKParameter()
68+
if (kParameter != null) {
69+
parameterModel.required = kParameter.type.isMarkedNullable == false
70+
}
71+
return@ParameterCustomizer parameterModel
72+
}
73+
}
74+
75+
private fun MethodParameter.toKParameter(): KParameter? {
76+
// ignore return type, see org.springframework.core.MethodParameter.getParameterIndex
77+
if (parameterIndex == -1) return null
78+
val kotlinFunction = method?.kotlinFunction ?: return null
79+
// The first parameter of the kotlin function is the "this" reference and not needed here.
80+
// See also kotlin.reflect.KCallable.getParameters
81+
return kotlinFunction.parameters[parameterIndex + 1]
82+
}
83+
}

springdoc-openapi-tests/springdoc-openapi-kotlin-tests/src/test/kotlin/test/org/springdoc/api/AbstractKotlinSpringDocTest.kt

+16-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package test.org.springdoc.api
2020

2121
import org.junit.jupiter.api.Test
2222
import org.skyscreamer.jsonassert.JSONAssert
23+
import org.slf4j.LoggerFactory
2324
import org.springdoc.core.utils.Constants
2425
import org.springframework.beans.factory.annotation.Autowired
2526
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
@@ -36,24 +37,30 @@ abstract class AbstractKotlinSpringDocTest {
3637
@Autowired
3738
private val webTestClient: WebTestClient? = null
3839

40+
private val logger = LoggerFactory.getLogger(AbstractKotlinSpringDocTest::class.java)
3941

4042
@Test
4143
@Throws(Exception::class)
4244
fun testApp() {
43-
val getResult =
44-
webTestClient!!.get().uri(Constants.DEFAULT_API_DOCS_URL).exchange()
45-
.expectStatus().isOk.expectBody().returnResult()
45+
var result: String? = null
46+
try {
47+
val getResult =
48+
webTestClient!!.get().uri(Constants.DEFAULT_API_DOCS_URL).exchange()
49+
.expectStatus().isOk.expectBody().returnResult()
4650

47-
val result = String(getResult.responseBody!!)
48-
val className = javaClass.simpleName
49-
val testNumber = className.replace("[^0-9]".toRegex(), "")
51+
result = String(getResult.responseBody!!)
52+
val className = javaClass.simpleName
53+
val testNumber = className.replace("[^0-9]".toRegex(), "")
5054

51-
val expected = getContent("results/app$testNumber.json")
52-
JSONAssert.assertEquals(expected, result, true)
55+
val expected = getContent("results/app$testNumber.json")
56+
JSONAssert.assertEquals(expected, result, true)
57+
} catch (e: AssertionError) {
58+
logger.error(result)
59+
throw e
60+
}
5361
}
5462

5563
companion object {
56-
5764
@Throws(Exception::class)
5865
fun getContent(fileName: String): String {
5966
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
*
3+
* * Copyright 2019-2022 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app6
20+
21+
import org.springframework.web.bind.annotation.GetMapping
22+
import org.springframework.web.bind.annotation.RequestMapping
23+
import org.springframework.web.bind.annotation.RestController
24+
25+
data class Foo(val data: ByteArray)
26+
27+
@RestController
28+
@RequestMapping("/bytearray")
29+
class ByteArrayController {
30+
31+
@GetMapping("/")
32+
fun getByteArray(): Foo = Foo(byteArrayOf(0))
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app6
20+
21+
import org.springframework.boot.autoconfigure.SpringBootApplication
22+
import org.springframework.context.annotation.ComponentScan
23+
import test.org.springdoc.api.AbstractKotlinSpringDocTest
24+
25+
class SpringDocApp6Test : AbstractKotlinSpringDocTest() {
26+
27+
@SpringBootApplication
28+
@ComponentScan(basePackages = ["org.springdoc", "test.org.springdoc.api.app6"])
29+
open class DemoApplication
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package test.org.springdoc.api.app7
2+
3+
import org.springframework.web.bind.annotation.GetMapping
4+
import org.springframework.web.bind.annotation.RequestMapping
5+
import org.springframework.web.bind.annotation.RequestParam
6+
import org.springframework.web.bind.annotation.RestController
7+
8+
data class Greeting(val greeting: String)
9+
10+
@RestController
11+
interface ExampleController {
12+
@GetMapping("/")
13+
fun greet(@RequestParam name: String?): Greeting
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app7
20+
21+
import org.springframework.aop.framework.ProxyFactory
22+
import org.springframework.boot.autoconfigure.SpringBootApplication
23+
import org.springframework.context.annotation.Bean
24+
import org.springframework.context.annotation.ComponentScan
25+
import org.springframework.context.support.GenericApplicationContext
26+
import test.org.springdoc.api.AbstractKotlinSpringDocTest
27+
28+
class SpringDocApp7Test : AbstractKotlinSpringDocTest() {
29+
30+
@SpringBootApplication
31+
@ComponentScan(basePackages = ["org.springdoc", "test.org.springdoc.api.app7"])
32+
open class DemoApplication{
33+
@Bean
34+
fun controller(applicationContext: GenericApplicationContext): ExampleController {
35+
return createProxy(ExampleController::class.java)
36+
}
37+
38+
private fun <T> createProxy(clazz: Class<T>): T {
39+
val proxyFactory = ProxyFactory(clazz)
40+
proxyFactory.targetClass = clazz
41+
@Suppress("UNCHECKED_CAST")
42+
return proxyFactory.proxy as T
43+
}
44+
}
45+
46+
}

0 commit comments

Comments
 (0)