This manual is a work in progress and is currently incomplete.
If you'd like to help improve it, and we hope you do, please see the README.

21 Retrofit Type Safe Clients

The ratpack-retrofit2 extension provides integration for declarative type-safe HTTP clients with the retrofit2 library (v2.9.0).

The retrofit library allows for representing HTTP APIs via a type-safe interface. This allows application code to remain agnostic about the underlying API design and implementation and focus solely on the behavioral aspects of interfacing with the API.

Retrofit clients generated using the RatpackRetrofit class are backed with Ratpack’s HttpClient and are capable of interfacing with Ratpack’s Promise construct as a return type.

By using the ratpack-retrofit2 integration, developers can gain the benefit of isolating the API constructs as class and method annotations, while still utilizing the non-blocking nature of the Netty backed HttpClient and Ratpack’s execution model.

1.21 Usage

The RatpackRetrofit.client(URI endpoint) and RatpackRetrofit.client(String endpoint) methods provide the entry point for creating API clients.

The provided URI or String specifies the base URL for all clients generated by the Builder.

The underlying Retrofit.Builder can be configured with the RatpackRetrofit.Builder.configure(Action<? super Retrofit.Builder> spec) method.

Once configured, the client is constructed by calling build(Class<T> api) with the api interface. This method returns a generated instance of the interface that will issue the HTTP requests and adapt the responses to the configured return type.

import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.http.GET;

import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static interface HelloApi {
    @GET("hi") Promise<String> hello();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("hi", ctx -> ctx.render("hello"))
      )
    );
    EmbeddedApp.of(s -> s
      .registryOf(r -> 
        r.add(HelloApi.class,
          RatpackRetrofit
            .client(api.getAddress())
            .build(HelloApi.class))
      )
      .handlers(chain -> {
        chain.get(ctx -> {
          HelloApi helloApi = ctx.get(HelloApi.class);
            
          ctx.render(helloApi.hello());
        });
      })
    ).test(httpClient -> {
      assertEquals("hello", httpClient.getText());
      api.close();
    });
  }
}

2.21 Using Ratpack Promises in Retrofit APIs

The ratpack-retrofit2 integration provides support for utilizing Ratpack’s Promise as a return type for a client interface. It supports adapting to Promise when the promised value is the following types:

The following example shows the 3 variations configured for the same endpoint.

import ratpack.exec.Promise;
import ratpack.core.http.client.ReceivedResponse;
import retrofit2.Response;
import retrofit2.http.GET;

public interface Example {
  @GET("hi") Promise<String> hello();
  
  @GET("hi") Promise<Response<String>> helloResponse();
  
  @GET("hi") Promise<ReceivedResponse> helloRaw();
}

3.21 Creating multiple API implementations

Many APIs may be represented using distinct interfaces for different capabilities. To create multiple clients, the underlying Retrofit class can be obtained.

import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.http.GET;
import retrofit2.Retrofit;

import static org.junit.jupiter.api.Assertions.*;

  
public class Example {

  public static interface HelloApi {
    @GET("hi") Promise<String> hello();
  }
  
  public static interface GoodbyeApi {
    @GET("bye") Promise<String> bye();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("hi", ctx -> ctx.render("hello"))
        .get("bye", ctx -> ctx.render("goodbye"))
      )
    );
    EmbeddedApp.of(s -> s
      .registryOf(r -> {
        Retrofit retrofit = RatpackRetrofit
          .client(api.getAddress())
          .retrofit();
        r.add(HelloApi.class, retrofit.create(HelloApi.class));
        r.add(GoodbyeApi.class, retrofit.create(GoodbyeApi.class));
      })
      .handlers(chain -> {
        chain.get(ctx -> {
            
          HelloApi hiApi = ctx.get(HelloApi.class);
          GoodbyeApi byeApi = ctx.get(GoodbyeApi.class);
            
          ctx.render(hiApi.hello().right(byeApi.bye()).map(p -> p.left() + " and " + p.right()));
        });
      })
    ).test(httpClient -> {
      assertEquals("hello and goodbye", httpClient.getText());
      api.close();
    });
  }
}

4.21 Using Retrofit Converters

By default, ratpack-retrofit2 registers the ScalarsConverterFactory. This allows for API responses to be converted into Java String, primitives and their box types.

If the remote API is responding in JSON, then the JacksonConverterFactory must be registered.

import com.fasterxml.jackson.databind.ObjectMapper;
import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.exec.registry.Registry;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.http.GET;
import retrofit2.Retrofit;

import java.util.List;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

  
public class Example {

  public static interface NameApi {
    @GET("names") Promise<List<String>> names();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("names", ctx -> ctx.render(json(new String[]{"John", "Jane"})))
      )
    );
    EmbeddedApp.of(s -> s
      .registry(r -> 
        Registry.single(NameApi.class, 
          RatpackRetrofit
            .client(api.getAddress())
            .configure(b -> 
              b.addConverterFactory(
                JacksonConverterFactory.create(
                  r.get(ObjectMapper.class)
                )
              )
            )
            .build(NameApi.class))
      )
      .handlers(chain -> {
        chain.get(ctx -> {
          ctx.get(NameApi.class).names().then(nameList -> ctx.render(json(nameList)));  
        });
      })
    ).test(httpClient -> {
      assertEquals("[\"John\",\"Jane\"]", httpClient.getText());
      api.close();
    });
  }
}