Allow async metadata in RSocketRequester
This commit allows single-value async producers for the values of metadata entries in both the SETUP and for requests. The same is also enabled for data in the SETUP frame. Close gh-23640
This commit is contained in:
@@ -84,7 +84,7 @@ public class DefaultMetadataExtractorTests {
|
||||
.metadata("html data", TEXT_HTML)
|
||||
.metadata("xml data", TEXT_XML);
|
||||
|
||||
DataBuffer metadata = metadataEncoder.encode();
|
||||
DataBuffer metadata = metadataEncoder.encode().block();
|
||||
Payload payload = createPayload(metadata);
|
||||
Map<String, Object> result = this.extractor.extract(payload, COMPOSITE_METADATA);
|
||||
payload.release();
|
||||
@@ -104,7 +104,7 @@ public class DefaultMetadataExtractorTests {
|
||||
.metadata("html data", TEXT_HTML)
|
||||
.metadata("xml data", TEXT_XML);
|
||||
|
||||
DataBuffer metadata = metadataEncoder.encode();
|
||||
DataBuffer metadata = metadataEncoder.encode().block();
|
||||
Payload payload = createPayload(metadata);
|
||||
Map<String, Object> result = this.extractor.extract(payload, COMPOSITE_METADATA);
|
||||
payload.release();
|
||||
@@ -120,7 +120,7 @@ public class DefaultMetadataExtractorTests {
|
||||
public void route() {
|
||||
MimeType metaMimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString());
|
||||
MetadataEncoder metadataEncoder = new MetadataEncoder(metaMimeType, this.strategies).route("toA");
|
||||
DataBuffer metadata = metadataEncoder.encode();
|
||||
DataBuffer metadata = metadataEncoder.encode().block();
|
||||
Payload payload = createPayload(metadata);
|
||||
Map<String, Object> result = this.extractor.extract(payload, metaMimeType);
|
||||
payload.release();
|
||||
@@ -133,7 +133,7 @@ public class DefaultMetadataExtractorTests {
|
||||
this.extractor.metadataToExtract(TEXT_PLAIN, String.class, ROUTE_KEY);
|
||||
|
||||
MetadataEncoder metadataEncoder = new MetadataEncoder(TEXT_PLAIN, this.strategies).route("toA");
|
||||
DataBuffer metadata = metadataEncoder.encode();
|
||||
DataBuffer metadata = metadataEncoder.encode().block();
|
||||
Payload payload = createPayload(metadata);
|
||||
Map<String, Object> result = this.extractor.extract(payload, TEXT_PLAIN);
|
||||
payload.release();
|
||||
@@ -151,7 +151,7 @@ public class DefaultMetadataExtractorTests {
|
||||
});
|
||||
|
||||
MetadataEncoder encoder = new MetadataEncoder(TEXT_PLAIN, this.strategies).metadata("toA:text data", null);
|
||||
DataBuffer metadata = encoder.encode();
|
||||
DataBuffer metadata = encoder.encode().block();
|
||||
Payload payload = createPayload(metadata);
|
||||
Map<String, Object> result = this.extractor.extract(payload, TEXT_PLAIN);
|
||||
payload.release();
|
||||
@@ -167,7 +167,7 @@ public class DefaultMetadataExtractorTests {
|
||||
extractor.metadataToExtract(TEXT_PLAIN, String.class, "name");
|
||||
|
||||
MetadataEncoder encoder = new MetadataEncoder(TEXT_PLAIN, this.strategies).metadata("value", null);
|
||||
DataBuffer metadata = encoder.encode();
|
||||
DataBuffer metadata = encoder.encode().block();
|
||||
Payload payload = createPayload(metadata);
|
||||
|
||||
Map<String, Object> result = extractor.extract(payload, TEXT_PLAIN);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.messaging.rsocket;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -39,6 +40,7 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.DecodingException;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
@@ -191,6 +193,39 @@ public class DefaultRSocketRequesterBuilderTests {
|
||||
assertThat(setupPayload.getDataUtf8()).isEqualTo("My data");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setupWithAsyncValues() {
|
||||
|
||||
Mono<String> asyncMeta1 = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async Metadata 1");
|
||||
Mono<String> asyncMeta2 = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async Metadata 2");
|
||||
Mono<String> data = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async data");
|
||||
|
||||
RSocketRequester.builder()
|
||||
.dataMimeType(MimeTypeUtils.TEXT_PLAIN)
|
||||
.setupRoute("toA")
|
||||
.setupMetadata(asyncMeta1, new MimeType("text", "x.test.metadata1"))
|
||||
.setupMetadata(asyncMeta2, new MimeType("text", "x.test.metadata2"))
|
||||
.setupData(data)
|
||||
.connect(this.transport)
|
||||
.block();
|
||||
|
||||
ConnectionSetupPayload payload = Mono.from(this.connection.sentFrames())
|
||||
.map(ConnectionSetupPayload::create)
|
||||
.block();
|
||||
|
||||
MimeType compositeMimeType =
|
||||
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
|
||||
|
||||
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(StringDecoder.allMimeTypes());
|
||||
extractor.metadataToExtract(new MimeType("text", "x.test.metadata1"), String.class, "asyncMeta1");
|
||||
extractor.metadataToExtract(new MimeType("text", "x.test.metadata2"), String.class, "asyncMeta2");
|
||||
Map<String, Object> metadataValues = extractor.extract(payload, compositeMimeType);
|
||||
|
||||
assertThat(metadataValues.get("asyncMeta1")).isEqualTo("Async Metadata 1");
|
||||
assertThat(metadataValues.get("asyncMeta2")).isEqualTo("Async Metadata 2");
|
||||
assertThat(payload.getDataUtf8()).isEqualTo("Async data");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frameDecoderMatchesDataBufferFactory() throws Exception {
|
||||
testFrameDecoder(new NettyDataBufferFactory(ByteBufAllocator.DEFAULT), PayloadDecoder.ZERO_COPY);
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -28,6 +29,7 @@ import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.rsocket.AbstractRSocket;
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.metadata.WellKnownMimeType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
@@ -38,10 +40,12 @@ import reactor.test.StepVerifier;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.rsocket.RSocketRequester.RequestSpec;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.springframework.util.MimeTypeUtils.TEXT_PLAIN;
|
||||
|
||||
/**
|
||||
@@ -131,6 +135,54 @@ public class DefaultRSocketRequesterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWithoutData() {
|
||||
this.requester.route("toA").send().block(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(this.rsocket.getSavedMethodName()).isEqualTo("fireAndForget");
|
||||
assertThat(this.rsocket.getSavedPayload().getMetadataUtf8()).isEqualTo("toA");
|
||||
assertThat(this.rsocket.getSavedPayload().getDataUtf8()).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMonoWithoutData() {
|
||||
this.requester.route("toA").retrieveMono(String.class).block(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(this.rsocket.getSavedMethodName()).isEqualTo("requestResponse");
|
||||
assertThat(this.rsocket.getSavedPayload().getMetadataUtf8()).isEqualTo("toA");
|
||||
assertThat(this.rsocket.getSavedPayload().getDataUtf8()).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendWithAsyncMetadata() {
|
||||
|
||||
MimeType compositeMimeType =
|
||||
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
|
||||
|
||||
Mono<String> asyncMeta1 = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async Metadata 1");
|
||||
Mono<String> asyncMeta2 = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async Metadata 2");
|
||||
|
||||
TestRSocket rsocket = new TestRSocket();
|
||||
RSocketRequester.wrap(rsocket, TEXT_PLAIN, compositeMimeType, this.strategies)
|
||||
.route("toA")
|
||||
.metadata(asyncMeta1, new MimeType("text", "x.test.metadata1"))
|
||||
.metadata(asyncMeta2, new MimeType("text", "x.test.metadata2"))
|
||||
.data("data")
|
||||
.send()
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
Payload payload = rsocket.getSavedPayload();
|
||||
|
||||
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(this.strategies.decoders());
|
||||
extractor.metadataToExtract(new MimeType("text", "x.test.metadata1"), String.class, "asyncMeta1");
|
||||
extractor.metadataToExtract(new MimeType("text", "x.test.metadata2"), String.class, "asyncMeta2");
|
||||
Map<String, Object> metadataValues = extractor.extract(payload, compositeMimeType);
|
||||
|
||||
assertThat(metadataValues.get("asyncMeta1")).isEqualTo("Async Metadata 1");
|
||||
assertThat(metadataValues.get("asyncMeta2")).isEqualTo("Async Metadata 2");
|
||||
assertThat(payload.getDataUtf8()).isEqualTo("data");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retrieveMono() {
|
||||
String value = "bodyA";
|
||||
@@ -176,7 +228,7 @@ public class DefaultRSocketRequesterTests {
|
||||
|
||||
@Test
|
||||
public void fluxToMonoIsRejected() {
|
||||
assertThatIllegalArgumentException()
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.requester.route("").data(Flux.just("a", "b")).retrieveMono(String.class))
|
||||
.withMessage("No RSocket interaction model for Flux request to Mono response.");
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.messaging.rsocket;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
@@ -26,6 +27,7 @@ import io.rsocket.metadata.CompositeMetadata;
|
||||
import io.rsocket.metadata.RoutingMetadata;
|
||||
import io.rsocket.metadata.WellKnownMimeType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
@@ -56,11 +58,17 @@ public class MetadataEncoderTests {
|
||||
@Test
|
||||
public void compositeMetadata() {
|
||||
|
||||
Mono<String> asyncMeta1 = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async Metadata 1");
|
||||
Mono<String> asyncMeta2 = Mono.delay(Duration.ofMillis(1)).map(aLong -> "Async Metadata 2");
|
||||
|
||||
DataBuffer buffer = new MetadataEncoder(COMPOSITE_METADATA, this.strategies)
|
||||
.route("toA")
|
||||
.metadata("My metadata", MimeTypeUtils.TEXT_PLAIN)
|
||||
.metadata(asyncMeta1, new MimeType("text", "x.test.metadata1"))
|
||||
.metadata(Unpooled.wrappedBuffer("Raw data".getBytes(UTF_8)), MimeTypeUtils.APPLICATION_OCTET_STREAM)
|
||||
.encode();
|
||||
.metadata(asyncMeta2, new MimeType("text", "x.test.metadata2"))
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
CompositeMetadata entries = new CompositeMetadata(((NettyDataBuffer) buffer).getNativeBuffer(), false);
|
||||
Iterator<CompositeMetadata.Entry> iterator = entries.iterator();
|
||||
@@ -75,11 +83,21 @@ public class MetadataEncoderTests {
|
||||
assertThat(entry.getMimeType()).isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
|
||||
assertThat(entry.getContent().toString(UTF_8)).isEqualTo("My metadata");
|
||||
|
||||
assertThat(iterator.hasNext()).isTrue();
|
||||
entry = iterator.next();
|
||||
assertThat(entry.getMimeType()).isEqualTo("text/x.test.metadata1");
|
||||
assertThat(entry.getContent().toString(UTF_8)).isEqualTo("Async Metadata 1");
|
||||
|
||||
assertThat(iterator.hasNext()).isTrue();
|
||||
entry = iterator.next();
|
||||
assertThat(entry.getMimeType()).isEqualTo(MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE);
|
||||
assertThat(entry.getContent().toString(UTF_8)).isEqualTo("Raw data");
|
||||
|
||||
assertThat(iterator.hasNext()).isTrue();
|
||||
entry = iterator.next();
|
||||
assertThat(entry.getMimeType()).isEqualTo("text/x.test.metadata2");
|
||||
assertThat(entry.getContent().toString(UTF_8)).isEqualTo("Async Metadata 2");
|
||||
|
||||
assertThat(iterator.hasNext()).isFalse();
|
||||
}
|
||||
|
||||
@@ -92,7 +110,8 @@ public class MetadataEncoderTests {
|
||||
DataBuffer buffer =
|
||||
new MetadataEncoder(mimeType, this.strategies)
|
||||
.route("toA")
|
||||
.encode();
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
assertRoute("toA", ((NettyDataBuffer) buffer).getNativeBuffer());
|
||||
}
|
||||
@@ -102,7 +121,8 @@ public class MetadataEncoderTests {
|
||||
DataBuffer buffer =
|
||||
new MetadataEncoder(MimeTypeUtils.TEXT_PLAIN, this.strategies)
|
||||
.route("toA")
|
||||
.encode();
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
assertThat(dumpString(buffer)).isEqualTo("toA");
|
||||
}
|
||||
@@ -112,7 +132,8 @@ public class MetadataEncoderTests {
|
||||
DataBuffer buffer =
|
||||
new MetadataEncoder(MimeTypeUtils.TEXT_PLAIN, this.strategies)
|
||||
.route("a.{b}.{c}", "BBB", "C.C.C")
|
||||
.encode();
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
assertThat(dumpString(buffer)).isEqualTo("a.BBB.C%2EC%2EC");
|
||||
}
|
||||
@@ -122,7 +143,8 @@ public class MetadataEncoderTests {
|
||||
DataBuffer buffer =
|
||||
new MetadataEncoder(MimeTypeUtils.TEXT_PLAIN, this.strategies)
|
||||
.metadata(Unpooled.wrappedBuffer("Raw data".getBytes(UTF_8)), null)
|
||||
.encode();
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
assertThat(dumpString(buffer)).isEqualTo("Raw data");
|
||||
}
|
||||
@@ -132,7 +154,8 @@ public class MetadataEncoderTests {
|
||||
DataBuffer buffer =
|
||||
new MetadataEncoder(MimeTypeUtils.TEXT_PLAIN, this.strategies)
|
||||
.metadata("toA", null)
|
||||
.encode();
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
assertThat(dumpString(buffer)).isEqualTo("toA");
|
||||
}
|
||||
@@ -175,8 +198,8 @@ public class MetadataEncoderTests {
|
||||
MetadataEncoder encoder = new MetadataEncoder(MimeTypeUtils.TEXT_PLAIN, this.strategies);
|
||||
|
||||
assertThatThrownBy(() -> encoder.metadata("toA", MimeTypeUtils.APPLICATION_JSON))
|
||||
.hasMessage("Mime type is optional (may be null) " +
|
||||
"but was provided and does not match the connection metadata mime type.");
|
||||
.hasMessage("Mime type is optional when not using composite metadata, " +
|
||||
"but it was provided and does not match the connection metadata mime type 'text/plain'.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -186,7 +209,8 @@ public class MetadataEncoderTests {
|
||||
|
||||
DataBuffer buffer = new MetadataEncoder(COMPOSITE_METADATA, strategies)
|
||||
.route("toA")
|
||||
.encode();
|
||||
.encode()
|
||||
.block();
|
||||
|
||||
ByteBuf byteBuf = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT)
|
||||
.wrap(buffer.asByteBuffer())
|
||||
|
||||
Reference in New Issue
Block a user