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:
		
							parent
							
								
									9492d88270
								
							
						
					
					
						commit
						331bdb066e
					
				|  | @ -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>() | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue