Skip to content

Commit

Permalink
Add BodyInserters.fromValue(T, ParameterizedTypeReference<?>)
Browse files Browse the repository at this point in the history
This commit introduces
BodyInserters.fromValue(T, ParameterizedTypeReference<?>) variant as
well as related WebClient.RequestBodySpec API,
ServerResponse.BodyBuilder API and Kotlin extensions.

Closes spring-projectsgh-32713
  • Loading branch information
sdeleuze committed Apr 29, 2024
1 parent 9492d88 commit 09451f1
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 9 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -103,6 +103,32 @@ public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromValue(T body) {
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null);
}

/**
* Inserter to write the given value.
* <p>Alternatively, consider using the {@code bodyValue(Object, ParameterizedTypeReference)} shortcuts on
* {@link org.springframework.web.reactive.function.client.WebClient WebClient} and
* {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}.
* @param body the value to write
* @param bodyType the type of the body, used to capture the generic type
* @param <T> the type of the body
* @return the inserter to write a single value
* @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an
* instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()},
* for which {@link #fromPublisher(Publisher, ParameterizedTypeReference)} or
* {@link #fromProducer(Object, ParameterizedTypeReference)} should be used.
* @since 6.2
* @see #fromPublisher(Publisher, ParameterizedTypeReference)
* @see #fromProducer(Object, ParameterizedTypeReference)
*/
public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromValue(T body, ParameterizedTypeReference<T> bodyType) {
Assert.notNull(body, "'body' must not be null");
Assert.notNull(bodyType, "'bodyType' must not be null");
Assert.isNull(registry.getAdapter(body.getClass()),
"'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type");
return (message, context) ->
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forType(bodyType), null);
}

/**
* Inserter to write the given object.
* <p>Alternatively, consider using the {@code bodyValue(Object)} shortcuts on
Expand Down
Expand Up @@ -367,6 +367,12 @@ public RequestHeadersSpec<?> bodyValue(Object body) {
return this;
}

@Override
public <T> RequestHeadersSpec<?> bodyValue(T body, ParameterizedTypeReference<T> bodyType) {
this.inserter = BodyInserters.fromValue(body, bodyType);
return this;
}

@Override
public <T, P extends Publisher<T>> RequestHeadersSpec<?> body(
P publisher, ParameterizedTypeReference<T> elementTypeRef) {
Expand Down
Expand Up @@ -692,9 +692,38 @@ interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
* @throws IllegalArgumentException if {@code body} is a
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
* @since 5.2
* @see #bodyValue(Object, ParameterizedTypeReference)
*/
RequestHeadersSpec<?> bodyValue(Object body);

/**
* Shortcut for {@link #body(BodyInserter)} with a
* {@linkplain BodyInserters#fromValue value inserter}.
* For example:
* <p><pre class="code">
* List&lt;Person&gt; list = ... ;
*
* Mono&lt;Void&gt; result = client.post()
* .uri("/persons/{id}", id)
* .contentType(MediaType.APPLICATION_JSON)
* .bodyValue(list, new ParameterizedTypeReference&lt;List&lt;Person&gt;&gt;() {};)
* .retrieve()
* .bodyToMono(Void.class);
* </pre>
* <p>For multipart requests consider providing
* {@link org.springframework.util.MultiValueMap MultiValueMap} prepared
* with {@link org.springframework.http.client.MultipartBodyBuilder
* MultipartBodyBuilder}.
* @param body the value to write to the request body
* @param bodyType the type of the body, used to capture the generic type
* @param <T> the type of the body
* @return this builder
* @throws IllegalArgumentException if {@code body} is a
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
* @since 6.2
*/
<T> RequestHeadersSpec<?> bodyValue(T body, ParameterizedTypeReference<T> bodyType);

/**
* Shortcut for {@link #body(BodyInserter)} with a
* {@linkplain BodyInserters#fromPublisher Publisher inserter}.
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -225,6 +225,11 @@ public Mono<ServerResponse> bodyValue(Object body) {
return initBuilder(body, BodyInserters.fromValue(body));
}

@Override
public <T> Mono<ServerResponse> bodyValue(T body, ParameterizedTypeReference<T> bodyType) {
return initBuilder(body, BodyInserters.fromValue(body, bodyType));
}

@Override
public <T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher, Class<T> elementClass) {
return initBuilder(publisher, BodyInserters.fromPublisher(publisher, elementClass));
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -420,6 +420,20 @@ interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
*/
Mono<ServerResponse> bodyValue(Object body);

/**
* Set the body of the response to the given {@code Object} and return it.
* This is a shortcut for using a {@link #body(BodyInserter)} with a
* {@linkplain BodyInserters#fromValue value inserter}.
* @param body the body of the response
* @param bodyType the type of the body, used to capture the generic type
* @param <T> the type of the body
* @return the built response
* @throws IllegalArgumentException if {@code body} is a
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
* @since 6.2
*/
<T> Mono<ServerResponse> bodyValue(T body, ParameterizedTypeReference<T> bodyType);

/**
* Set the body from the given {@code Publisher}. Shortcut for
* {@link #body(BodyInserter)} with a
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -68,6 +68,18 @@ inline fun <reified T : Any> RequestBodySpec.body(flow: Flow<T>): RequestHeaders
inline fun <reified T : Any> RequestBodySpec.body(producer: Any): RequestHeadersSpec<*> =
body(producer, object : ParameterizedTypeReference<T>() {})

/**
* Extension for [WebClient.RequestBodySpec.bodyValue] providing a `bodyValueWithType<T>(Any)` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type
* erasure and retains actual generic type arguments.
* @param body the value to write to the request body
* @param T the type of the body
* @author Sebastien Deleuze
* @since 6.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyValueWithType(body: T): RequestHeadersSpec<*> =
bodyValue(body, object : ParameterizedTypeReference<T>() {})

/**
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
*
Expand Down
Expand Up @@ -51,9 +51,6 @@ inline fun <reified T : Any> ServerResponse.BodyBuilder.body(producer: Any): Mon
/**
* Coroutines variant of [ServerResponse.BodyBuilder.bodyValue].
*
* Set the body of the response to the given {@code Object} and return it.
* This convenience method combines [body] and
* [org.springframework.web.reactive.function.BodyInserters.fromValue].
* @param body the body of the response
* @return the built response
* @throws IllegalArgumentException if `body` is a [Publisher] or an
Expand All @@ -62,6 +59,21 @@ inline fun <reified T : Any> ServerResponse.BodyBuilder.body(producer: Any): Mon
suspend fun ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any): ServerResponse =
bodyValue(body).awaitSingle()

/**
* Coroutines variant of [ServerResponse.BodyBuilder.bodyValue] providing a `bodyValueWithTypeAndAwait<T>(Any)` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type
* erasure and retains actual generic type arguments.
*
* @param body the body of the response
* @param T the type of the body
* @return the built response
* @throws IllegalArgumentException if `body` is a [Publisher] or an
* instance of a type supported by [org.springframework.core.ReactiveAdapterRegistry.getSharedInstance],
* @since 6.2
*/
suspend inline fun <reified T: Any> ServerResponse.BodyBuilder.bodyValueWithTypeAndAwait(body: T): ServerResponse =
bodyValue(body, object : ParameterizedTypeReference<T>() {}).awaitSingle()

/**
* Coroutines variant of [ServerResponse.BodyBuilder.body] with [Any] and
* [ParameterizedTypeReference] parameters providing a `bodyAndAwait(Flow<T>)` variant.
Expand Down
@@ -0,0 +1,81 @@
/*
* Copyright 2002-2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.codec.EncoderHttpMessageWriter
import org.springframework.http.codec.HttpMessageWriter
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse
import reactor.test.StepVerifier
import java.util.*

/**
* @author Sebastien Deleuze
*/
class KotlinBodyInsertersTests {

private lateinit var context: BodyInserter.Context

private lateinit var hints: Map<String, Any>


@BeforeEach
fun createContext() {
val messageWriters: MutableList<HttpMessageWriter<*>> = ArrayList()
val jsonEncoder = KotlinSerializationJsonEncoder()
messageWriters.add(EncoderHttpMessageWriter(jsonEncoder))

this.context = object : BodyInserter.Context {
override fun messageWriters(): List<HttpMessageWriter<*>> {
return messageWriters
}

override fun serverRequest(): Optional<ServerHttpRequest> {
return Optional.empty()
}

override fun hints(): Map<String, Any> {
return hints
}
}
this.hints = HashMap()
}

@Test
fun ofObjectWithBodyType() {
val somebody = SomeBody(1, "name")
val body = listOf(somebody)
val inserter = BodyInserters.fromValue(body, object: ParameterizedTypeReference<List<SomeBody>>() {})
val response = MockServerHttpResponse()
val result = inserter.insert(response, context)
StepVerifier.create(result).expectComplete().verify()

StepVerifier.create(response.bodyAsString)
.expectNext("[{\"user_id\":1,\"name\":\"name\"}]")
.expectComplete()
.verify()
}

@Serializable
data class SomeBody(@SerialName("user_id") val userId: Int, val name: String)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -66,6 +66,13 @@ class WebClientExtensionsTests {
verify { requestBodySpec.body(ofType<Any>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}

@Test
fun `RequestBodySpec#bodyValueWithType with reified type parameters`() {
val body = mockk<List<Foo>>()
requestBodySpec.bodyValueWithType<List<Foo>>(body)
verify { requestBodySpec.bodyValue(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}

@Test
fun `ResponseSpec#bodyToMono with reified type parameters`() {
responseSpec.bodyToMono<List<Foo>>()
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -75,6 +75,19 @@ class ServerResponseExtensionsTests {
}
}

@Test
fun `BodyBuilder#bodyValueWithTypeAndAwait with object parameter and reified type parameters`() {
val response = mockk<ServerResponse>()
val body = listOf("foo", "bar")
every { bodyBuilder.bodyValue(ofType<List<String>>(), object : ParameterizedTypeReference<List<String>>() {}) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyValueWithTypeAndAwait<List<String>>(body)
}
verify {
bodyBuilder.bodyValue(body, object : ParameterizedTypeReference<List<String>>() {})
}
}

@Test
fun `BodyBuilder#bodyAndAwait with flow parameter`() {
val response = mockk<ServerResponse>()
Expand Down

0 comments on commit 09451f1

Please sign in to comment.