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:
- Simple scalars (
Integer
,String
,Long
, etc.) - Retrofit
Response
- Ratpack
ReceivedResponse
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();
});
}
}