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

Spring: support all media-types for consumes #58

Merged
merged 1 commit into from
Aug 19, 2021
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,39 @@ import org.springframework.http.MediaType.APPLICATION_JSON_VALUE
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.mvc.method.RequestMappingInfo
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.jvmErasure
import kotlin.reflect.jvm.kotlinFunction

internal fun Map.Entry<RequestMappingInfo, HandlerMethod>.consumes(): Set<String> {
val consumes = this.key
.consumesCondition
.expressions
.map { it.mediaType.toString() }
.toSet()

if (consumes.isNotEmpty()) {
return consumes
}

val providesRequestBodyAnnotation = this.value
.method
.kotlinFunction
?.parameters
?.any {
it.annotations
.filterIsInstance<RequestBody>()
.any()
it.findAnnotation<RequestBody>() !== null
} ?: false

if (!providesRequestBodyAnnotation) {
return emptySet()
}

val consumes = this.key
.consumesCondition
.expressions
.map { it.mediaType.toString() }
.toSet()

if (consumes.isNotEmpty()) {
return consumes
}

val isParameterString = this.value
.method
.kotlinFunction
?.parameters
?.firstOrNull {
it.annotations
.filterIsInstance<RequestBody>()
.any()
it.findAnnotation<RequestBody>() !== null
}
?.type
?.jvmErasure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.http.MediaType.*
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

@SpringBootApplication
open class DummyApp
Expand Down Expand Up @@ -43,6 +44,21 @@ open class RequestMappingOneMediaTypeIsExtractedCorrectlyController {
fun todos(@RequestBody todo: Todo) { }
}

@Controller
@Suppress("UNUSED_PARAMETER")
open class RequestMappingMultipartFormIsExtractedCorrectlyController {

@RequestMapping(
path = ["/form"],
method = [RequestMethod.POST],
consumes = [MULTIPART_FORM_DATA_VALUE]
)
fun form(
@RequestPart("title") title: String,
@RequestPart("file") form: MultipartFile
) { }
}

@Controller
@Suppress("UNUSED_PARAMETER")
open class RequestMappingMultipleMediaTypesAreExtractedCorrectlyController {
Expand Down Expand Up @@ -120,7 +136,7 @@ open class RequestMappingOnClassWithoutRequestBodyAnnotationController {

@Controller
@Suppress("UNUSED_PARAMETER")
open class RequestMappingOnFunctionWithoutRequestBodyAnnotationController {
open class RequestMappingOnFunctionWithoutConsumesAnnotationController {

@RequestMapping("/todos")
fun todos(todo: String) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ class SpringConverterConsumesTest {

@Nested
inner class ClassLevelTests {

@Nested
@WebMvcTest(RequestMappingOneMediaTypeIsInheritedByAllFunctionsController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class OneMediaTypeIsInheritedByAllFunctionsTest {

@Autowired
lateinit var context: ConfigurableApplicationContext

@Test
fun `media type declared at class level using RequestMapping annotation is inherited by all functions`() {
//given
Expand Down Expand Up @@ -94,22 +94,22 @@ class SpringConverterConsumesTest {
),
Endpoint("/tags", OPTIONS)
)

//when
val implementation = SpringConverter(context)

//then
assertThat(implementation.conversionResult).containsExactlyInAnyOrderElementsOf(specification)
}
}

@Nested
@WebMvcTest(RequestMappingMultipleMediaTypesAreInheritedByAllFunctionsController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class MultipleMediaTypesAreInheritedByAllFunctionsTest {

@Autowired
lateinit var context: ConfigurableApplicationContext

@Test
fun `multiple media types declared at class level using RequestMapping annotation are inherited by all functions`() {
//given
Expand Down Expand Up @@ -177,10 +177,10 @@ class SpringConverterConsumesTest {
),
Endpoint("/tags", OPTIONS)
)

//when
val implementation = SpringConverter(context)

//then
assertThat(implementation.conversionResult).containsExactlyInAnyOrderElementsOf(specification)
}
Expand Down Expand Up @@ -318,17 +318,17 @@ class SpringConverterConsumesTest {
}
}
}

@Nested
inner class FunctionLevelTests {

@Nested
@WebMvcTest(RequestMappingOneMediaTypeIsExtractedCorrectlyController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class OneMediaTypeIsExtractedCorrectlyTest {

@Autowired
lateinit var context: ConfigurableApplicationContext

@Test
fun `media type declared at function level using RequestMapping annotation is extracted correctly`() {
//given
Expand Down Expand Up @@ -365,22 +365,57 @@ class SpringConverterConsumesTest {
),
Endpoint("/todos", OPTIONS)
)

//when
val implementation = SpringConverter(context)

//then
assertThat(implementation.conversionResult).containsExactlyInAnyOrderElementsOf(specification)
}
}


@Nested
@WebMvcTest(RequestMappingMultipartFormIsExtractedCorrectlyController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class MultipartFormIsExtractedCorrectlyTest {

@Autowired
lateinit var context: ConfigurableApplicationContext

@Test
fun `multipart form media type is extracted correctly`() {
//given
val specification: Set<Endpoint> = setOf(
Endpoint(
path = "/form",
httpMethod = POST,
consumes = setOf(MULTIPART_FORM_DATA_VALUE)
),
Endpoint(
path = "/form",
httpMethod = HEAD,
consumes = setOf(MULTIPART_FORM_DATA_VALUE)
),
Endpoint(
path = "/form",
httpMethod = OPTIONS,
),
)

//when
val implementation = SpringConverter(context)

//then
assertThat(implementation.conversionResult).containsExactlyInAnyOrderElementsOf(specification)
}
}

@Nested
@WebMvcTest(RequestMappingMultipleMediaTypesAreExtractedCorrectlyController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class MultipleMediaTypesAreExtractedCorrectlyTest {

@Autowired
lateinit var context: ConfigurableApplicationContext

@Test
fun `multiple media types declared at function level using RequestMapping annotation are extracted correctly`() {
//given
Expand Down Expand Up @@ -417,10 +452,10 @@ class SpringConverterConsumesTest {
),
Endpoint("/todos", OPTIONS)
)

//when
val implementation = SpringConverter(context)

//then
assertThat(implementation.conversionResult).containsExactlyInAnyOrderElementsOf(specification)
}
Expand Down Expand Up @@ -531,14 +566,14 @@ class SpringConverterConsumesTest {
}

@Nested
@WebMvcTest(RequestMappingOnFunctionWithoutRequestBodyAnnotationController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class NoRequestBodyAnnotationTest {
@WebMvcTest(RequestMappingOnFunctionWithoutConsumesAnnotationController::class, excludeAutoConfiguration = [ErrorMvcAutoConfiguration::class])
inner class EmptyAnnotationTest {

@Autowired
lateinit var context: ConfigurableApplicationContext

@Test
fun `no RequestBody annotation results in an empty produces list`() {
fun `no RequestBody nor consumes annotation results in an empty produces list`() {
//given
val specification: Set<Endpoint> = setOf(
Endpoint("/todos", GET),
Expand Down