Skip to content

Commit

Permalink
Support for Vert.x expectations as pluggable operators
Browse files Browse the repository at this point in the history
Issue: #989
  • Loading branch information
jponge committed Nov 8, 2024
1 parent a8078f2 commit 02e911d
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 0 deletions.
50 changes: 50 additions & 0 deletions docs/using-vertx-expectations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Using Vert.x expectations

Vert.x `Future` support [expectations](https://javadoc.io/static/io.vertx/vertx-core/4.5.10/io/vertx/core/Expectation.html) as predicates on the resolved values.

A good example of pre-defined expectations are those from `HttpResponseExpectation`:

```java
Future<JsonObject> future = client
.request(HttpMethod.GET, "some-uri")
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.SC_OK.and(HttpResponseExpectation.JSON))
.compose(response -> response
.body()
.map(buffer -> buffer.toJsonObject())));
```

In this example the HTTP response is expected to be with status code 200 (`SC_OK`) and have the `application/json` content-type (`JSON`).

## Where are expectations gone in a Vert.x Mutiny bindings API?

While expectations are very useful, they apply to `Future` in most of the Vert.x APIs such as the core HTTP client.

Since the Mutiny bindings generator turns `Future`-returning methods into `Uni`-returning methods, you need to leverage another route to use them.

## Turning expectations into operators

The `io.smallrye.mutiny.vertx.core.Expectations` class that comes with the `smallrye-mutiny-vertx-core` artifact bring 2 helper methods.

The `expecting` methods build functions that can be used with the `Uni::plug` operator, as in:

```java
Expectation<Integer> tenToTwenty = (value -> value >= 10 && value <= 20);

return Uni.createFrom().item(15)
.plug(expectation(tenToTwenty));
```

Some expectations such as those in `HttpResponseExpectation` apply to types from the core Vert.x APIs.
Since the Mutiny bindings generate shims (e.g., `io.vertx.mutiny.core.http.HttpResponseHead`), you need to extract the delegate type for the expectations to work on the correct type (e.g., `io.vertx.core.http.HttpResponseHead`):

```java
return vertx.createHttpClient()
.request(HttpMethod.GET, port, "localhost", "/")
.chain(HttpClientRequest::send)
.plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain"))))
.onItem().transformToUni(HttpClientResponse::body)
```

The extractor function here is `HttpClientResponse::getDelegate`, so that the `status(200).and(contentType("text/plain"))` expectation applies to `io.vertx.core.http.HttpResponseHead` and not the `io.vertx.mutiny.core.http.HttpResponseHead` generated shim.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.smallrye.mutiny.vertx.core;

import java.util.function.Function;

import io.smallrye.mutiny.Uni;
import io.vertx.core.Expectation;

/**
* Helper methods to turn Vert.x {@link Expectation} that work on {@link io.vertx.core.Future} into {@link Uni} that
* can be used in a pipeline using the {@link Uni#plug(Function)} operator, as in:
*
* <pre>{@code
* vertx.createHttpClient()
* .request(HttpMethod.GET, port, "localhost", "/")
* .chain(HttpClientRequest::send)
* .plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain"))))
* .onItem().transformToUni(HttpClientResponse::body)
* }</pre>
*/
public interface Expectations {

/**
* Yields a function to turn an {@link Expectation} into a {@link Uni}.
*
* @param expectation the expectation
* @return the mapping function
* @param <T> the element type
*/
static <T> Function<Uni<T>, Uni<T>> expectation(Expectation<? super T> expectation) {
return uni -> uni.onItem().transformToUni(item -> {
if (expectation.test(item)) {
return Uni.createFrom().item(item);
} else {
return Uni.createFrom().failure(expectation.describe(item));
}
});
}

/**
* Yields a function to turn an {@link Expectation} into a {@link Uni} and uses an extractor so that expectations
* work on the correct types (e.g., {@link io.vertx.core.http.HttpResponseHead}) instead of the Mutiny shim types
* (e.g., {@link io.vertx.mutiny.core.http.HttpResponseHead}.
*
* @param extractor the extractor function, often a reference to a {@code getDelegate()} method
* @param expectation the expectation
* @return the mapping function
* @param <T> the element type
* @param <R> the extracted element type
*/
static <T, R> Function<Uni<T>, Uni<T>> expectation(Function<T, R> extractor, Expectation<? super R> expectation) {
return uni -> uni
.onItem().transformToUni(item -> {
R unwrapped = extractor.apply(item);
if (expectation.test(unwrapped)) {
return Uni.createFrom().item(item);
} else {
return Uni.createFrom().failure(expectation.describe(unwrapped));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.smallrye.mutiny.vertx.core;

import static io.smallrye.mutiny.vertx.core.Expectations.expectation;
import static io.vertx.core.http.HttpResponseExpectation.contentType;
import static io.vertx.core.http.HttpResponseExpectation.status;
import static org.assertj.core.api.Assertions.assertThat;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

import org.junit.Test;

import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import io.vertx.core.Expectation;
import io.vertx.core.VertxException;
import io.vertx.core.http.HttpMethod;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.core.http.HttpClientRequest;
import io.vertx.mutiny.core.http.HttpClientResponse;
import io.vertx.mutiny.core.http.HttpServer;

public class ExpectationsTest {

@Test
public void plugMatchingExpectation() {
Expectation<Integer> tenToTwenty = (value -> value >= 10 && value <= 20);

UniAssertSubscriber<Integer> sub = Uni.createFrom().item(15)
.plug(expectation(tenToTwenty))
.subscribe().withSubscriber(UniAssertSubscriber.create());
sub.assertItem(15);
}

@Test
public void plugFailingExpectation() {
Expectation<Integer> tenToTwenty = (value -> value >= 10 && value <= 20);

UniAssertSubscriber<Integer> sub = Uni.createFrom().item(42)
.plug(expectation(tenToTwenty))
.subscribe().withSubscriber(UniAssertSubscriber.create());
sub.assertFailedWith(VertxException.class, "Unexpected result: 42");
}

@Test
public void httpAssertion() {
Vertx vertx = Vertx.vertx();
try {

HttpServer server = vertx.createHttpServer()
.requestHandler(req -> req.response()
.setStatusCode(200)
.putHeader("content-type", "text/plain")
.endAndForget("Yolo"))
.listen()
.await().atMost(Duration.ofSeconds(30));

int port = server.actualPort();
Buffer payload = vertx.createHttpClient()
.request(HttpMethod.GET, port, "localhost", "/")
.chain(HttpClientRequest::send)
.plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain"))))
.onItem().transformToUni(HttpClientResponse::body)
.await().atMost(Duration.ofSeconds(5));
assertThat(payload.toString(StandardCharsets.UTF_8)).isEqualTo("Yolo");

} finally {
vertx.closeAndAwait();
}
}
}

0 comments on commit 02e911d

Please sign in to comment.