Add BodyInserters.fromValue(T, ParameterizedTypeReference<T>)
This commit introduces BodyInserters.fromValue(T, ParameterizedTypeReference<T>) variant as well as related WebClient.RequestBodySpec API, ServerResponse.BodyBuilder API and Kotlin extensions. Closes gh-32713
This commit is contained in:
@@ -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.
|
||||
@@ -103,6 +103,32 @@ public abstract class BodyInserters {
|
||||
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
|
||||
|
||||
@@ -367,6 +367,12 @@ final class DefaultWebClient implements WebClient {
|
||||
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) {
|
||||
|
||||
@@ -692,9 +692,38 @@ public interface WebClient {
|
||||
* @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<Person> list = ... ;
|
||||
*
|
||||
* Mono<Void> result = client.post()
|
||||
* .uri("/persons/{id}", id)
|
||||
* .contentType(MediaType.APPLICATION_JSON)
|
||||
* .bodyValue(list, new ParameterizedTypeReference<List<Person>>() {};)
|
||||
* .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}.
|
||||
|
||||
@@ -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.
|
||||
@@ -225,6 +225,11 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
|
||||
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));
|
||||
|
||||
@@ -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.
|
||||
@@ -420,6 +420,20 @@ public interface ServerResponse {
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
@@ -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].
|
||||
*
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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>>()
|
||||
|
||||
@@ -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.
|
||||
@@ -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>()
|
||||
|
||||
Reference in New Issue
Block a user