How to return a CompletableFuture<MyType<T>> from a CompletableFuture<AnotherType<T>>

504 Views Asked by At

I am attempting to implement a generic REST client as below. I have my own model to represent the HTTP response as Response T . Here T is the return type from the service call and it could be just T or List of Ts. Below code does not compile and that is where I need help. You can suggest if you know any better ways to accomplish the same task.

@Override
    public <T> CompletableFuture<Response<T>> getAsync(UriOptions endpoint, Headers headers) {
        var builder = HttpRequest.newBuilder()
                .uri(endpoint.getAbsoluteUri())
                .GET();
        for (var entry : headers.getMessageHeaders().entrySet()) {
            builder.setHeader(entry.getKey(), entry.getValue());
        }
        var request = builder.build();
        var response = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        ObjectMapper objectMapper = new ObjectMapper();
        return response.thenComposeAsync(r ->
            try {
                 new Response<T>(
                        r.statusCode(),
                        HttpUtils.headersToMap(r.headers().map()),
                        objectMapper.readValue(r.body(), new TypeReference<T>() {
                        }),
                        HttpUtils.isSuccess(r.statusCode()), r.uri()
                );
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        );
    }
public class Response<T> {
    private T content;
    private int statusCode;
    private boolean isSuccessful;
    private String statusDescription;
    private Exception exception;
    private Map<String, String> headers;
    private URI uri;

And eventually call the method as below,

@Test
    void getAsync() {
        // Arrange
        var baseUrl = settings.get("api.baseUri");
        var options = new UriOptions(baseUrl, "students/6277aade6f047803e8ae4bcf");
        var headers = new Headers(new HashMap<String, String>(), new HashMap<String, String>());

        // Act
        var response = restApiClient.<Student>getAsync(options, headers);

        // Assert
        response.thenAccept((r) ->
        {
            Assertions.assertEquals(200, r.getStatusCode());
        });
    }
1

There are 1 best solutions below

2
On

To answer the question "How to return a CompletableFuture<MyType> from a CompletableFuture<AnotherType>" from the title: you'd generally use thenApply and friends:

final CompletableFuture<String> future = CompletableFuture.supplyAsync(() => "42");
final CompletableFuture<Integer> intFuture = future.thenApply(Integer::parseInt);
// or future.thenApply(s -> Integer.parseInt(s));
// or future.thenApply(s -> { return Integer.parseInt(s); });

Looking at your specific code, there seems to be a syntax error in the lambda of thenComposeAsync (which composes two CompletableFuture instances, so its lambda needs to return another CompletableFuture instance, not the value itself).

And since it is not doing any expensive work, transforming the value asynchronically is overkill. A simple thenApply will do the job just fine:

return response.thenApply(r -> {
    try {
         return new Response<T>(
                r.statusCode(),
                HttpUtils.headersToMap(r.headers().map()),
                objectMapper.readValue(r.body(), new TypeReference<T>() {}),
                HttpUtils.isSuccess(r.statusCode()), r.uri()
        );
    } catch (JsonProcessingException e) {
        throw new RuntimeException(e);
    }
});

If you insist on using thenCompose, then you need to wrap your result in another CompletableFuture:

return response.then(r -> {
    try {
         return CompletableFuture.completedFuture(new Response<T>(
                r.statusCode(),
                HttpUtils.headersToMap(r.headers().map()),
                objectMapper.readValue(r.body(), new TypeReference<T>() {}),
                HttpUtils.isSuccess(r.statusCode()), r.uri()
        ));
    } catch (JsonProcessingException e) {
        throw new RuntimeException(e);
    }
});