Skip to content

Commit

Permalink
Merge pull request #21643 from cescoffier/reactive-routes-multi-seria…
Browse files Browse the repository at this point in the history
…lization

Fix Reactive Route stream serialization without relying on the Multi sub-type
  • Loading branch information
mkouba authored Nov 25, 2021
2 parents 301898e + 6949c56 commit bd87966
Show file tree
Hide file tree
Showing 13 changed files with 1,000 additions and 84 deletions.
55 changes: 41 additions & 14 deletions docs/src/main/asciidoc/reactive-routes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -399,18 +399,16 @@ The previous snippet produces:
You can return a `Multi` to produce a JSON Array, where every item is an item from this array.
The response is written item by item to the client.
The `content-type` is set to `application/json` if not set already.
To use this feature, you need to wrap the returned `Multi` using `io.quarkus.vertx.web.ReactiveRoutes.asJsonArray`:
To do that set the `produces` attribute to `"application/json"` (or `ReactiveRoutes.APPLICATION_JSON`).
[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.APPLICATION_JSON)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonArray(Multi.createFrom().items(
return Multi.createFrom().items(
new Person("superman", 1),
new Person("batman", 2),
new Person("spiderman", 3)));
new Person("spiderman", 3));
}
----
Expand All @@ -425,24 +423,34 @@ The previous snippet produces:
]
----
TIP: The `produces` attribute is an array.
When you pass a single value you can omit the "{" and "}".
Note that `"application/json"` must be the first value in the array.
Only `Multi<String>`, `Multi<Object>` and `Multi<Void>` can be written into the JSON Array.
Using a `Multi<Void>` produces an empty array.
You cannot use `Multi<Buffer>`.
If you need to use `Buffer`, transform the content into a JSON or String representation first.
[NOTE]
.Deprecation of `asJsonArray`
====
The `ReactiveRoutes.asJsonArray` has been deprecated as it is not compatible with the security layer of Quarkus.
====
=== Event Stream and Server-Sent Event support
You can return a `Multi` to produce an event source (stream of server sent events).
To enable this feature, you need to wrap the returned `Multi` using `io.quarkus.vertx.web.ReactiveRoutes.asEventStream`:
To enable this feature, set the `produces` attribute to `"text/event-stream"` (or `ReactiveRoutes.EVENT_STREAM`), such as in:
[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.EVENT_STREAM)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asEventStream(Multi.createFrom().items(
return Multi.createFrom().items(
new Person("superman", 1),
new Person("batman", 2),
new Person("spiderman", 3)));
new Person("spiderman", 3));
}
----
Expand All @@ -461,6 +469,10 @@ id: 2

----
TIP: The `produces` attribute is an array.
When you pass a single value you can omit the "{" and "}".
Note that `"text/event-stream"` must be the first value in the array.
You can also implement the `io.quarkus.vertx.web.ReactiveRoutes.ServerSentEvent` interface to customize the `event` and `id` section of the server sent event:
[source, java]
Expand Down Expand Up @@ -491,7 +503,7 @@ class PersonEvent implements ReactiveRoutes.ServerSentEvent<Person> {
}
----
Using a `Multi<PersonEvent>` (wrapped using `io.quarkus.vertx.web.ReactiveRoutes.asEventStream`) would produce:
Using a `Multi<PersonEvent>` would produce:
[source, text]
----
Expand All @@ -509,14 +521,20 @@ id: 3

----
[NOTE]
.Deprecation of `asEventStream`
====
The `ReactiveRoutes.asEventStream` has been deprecated as it is not compatible with the security layer of Quarkus.
====
=== Json Stream in NDJSON format
You can return a `Multi` to produce a newline delimited stream of JSON values.
To enable this feature, you need to wrap the returned `Multi` using `io.quarkus.vertx.web.ReactiveRoutes.asJsonStream`:
To enable this feature, set the `produces` attribute of the `@Route` annotation to `"application/x-ndjson"` (or `ReactiveRoutes.ND_JSON`):
[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.ND_JSON)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonStream(Multi.createFrom().items(
new Person("superman", 1),
Expand All @@ -536,11 +554,14 @@ This method would produce:

----
TIP: The `produces` attribute is an array. When you pass a single value you can omit the "{" and "}".
Note that `"application/x-ndjson"` must be the first value in the array.
You can also provide strings instead of Objects, in that case the strings will be wrapped in quotes to become valid JSON values:
[source, java]
----
@Route(path = "/people")
@Route(path = "/people", produces = ReactiveRoutes.ND_JSON)
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonStream(Multi.createFrom().items(
"superman",
Expand All @@ -558,6 +579,12 @@ Multi<Person> people(RoutingContext context) {

----
[NOTE]
.Deprecation of `asJsonStream`
====
The `ReactiveRoutes.asJsonStream` has been deprecated as it is not compatible with the security layer of Quarkus.
====
=== Using Bean Validation
You can combine reactive routes and Bean Validation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,32 @@ class HandlerDescriptor {
private final MethodInfo method;
private final BeanValidationAnnotationsBuildItem validationAnnotations;
private final HandlerType handlerType;
private final Type contentType;
private final Type payloadType;
private final String[] contentTypes;

HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType) {
HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType,
String[] producedTypes) {
this.method = method;
this.validationAnnotations = bvAnnotations;
this.handlerType = handlerType;
Type returnType = method.returnType();
if (returnType.kind() == Kind.VOID) {
contentType = null;
payloadType = null;
} else {
if (returnType.name().equals(DotNames.UNI) || returnType.name().equals(DotNames.MULTI)
|| returnType.name().equals(DotNames.COMPLETION_STAGE)) {
contentType = returnType.asParameterizedType().arguments().get(0);
payloadType = returnType.asParameterizedType().arguments().get(0);
} else {
contentType = returnType;
payloadType = returnType;
}
}
this.contentTypes = producedTypes;
}

Type getReturnType() {
return method.returnType();
}

boolean isReturningVoid() {
return method.returnType().kind().equals(Type.Kind.VOID);
}

boolean isReturningUni() {
return method.returnType().name().equals(DotNames.UNI);
}
Expand All @@ -55,6 +54,13 @@ boolean isReturningCompletionStage() {
return method.returnType().name().equals(DotNames.COMPLETION_STAGE);
}

public String getFirstContentType() {
if (contentTypes == null || contentTypes.length == 0) {
return null;
}
return contentTypes[0];
}

/**
* @return {@code true} if the method is annotated with a constraint or {@code @Valid} or any parameter has such kind of
* annotation.
Expand Down Expand Up @@ -86,28 +92,28 @@ boolean isProducedResponseValidated() {
return false;
}

Type getContentType() {
return contentType;
Type getPayloadType() {
return payloadType;
}

boolean isContentTypeString() {
Type type = getContentType();
boolean isPayloadString() {
Type type = getPayloadType();
if (type == null) {
return false;
}
return type.name().equals(io.quarkus.arc.processor.DotNames.STRING);
}

boolean isContentTypeBuffer() {
Type type = getContentType();
boolean isPayloadTypeBuffer() {
Type type = getPayloadType();
if (type == null) {
return false;
}
return type.name().equals(DotNames.BUFFER);
}

boolean isContentTypeMutinyBuffer() {
Type type = getContentType();
boolean isPayloadMutinyBuffer() {
Type type = getPayloadType();
if (type == null) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ static void returnAndClose(BytecodeCreator creator) {
}

static boolean isNoContent(HandlerDescriptor descriptor) {
return descriptor.getContentType().name()
return descriptor.getPayloadType().name()
.equals(DotName.createSimple(Void.class.getName()));
}

Expand All @@ -249,7 +249,7 @@ static ResultHandle createNpeBecauseItemIfNull(BytecodeCreator writer) {
}

static MethodDescriptor getEndMethodForContentType(HandlerDescriptor descriptor) {
if (descriptor.isContentTypeBuffer() || descriptor.isContentTypeMutinyBuffer()) {
if (descriptor.isPayloadTypeBuffer() || descriptor.isPayloadMutinyBuffer()) {
return END_WITH_BUFFER;
}
return END_WITH_STRING;
Expand Down
Loading

0 comments on commit bd87966

Please sign in to comment.