Skip to content

zskamljic/rest-ahead

Repository files navigation

RestAhead - compile time generated REST client

Quality Gate Status Coverage

This project draws inspiration from projects such as Retrofit and Feign, but with a twist: your services are generated at compile time, preventing any issues from being found at runtime.

The aim of this project is to have as much validation done at compile time, rather than runtime. Additionally, since the code is generated at compile time there is no need for reflection. The generated code can be inspected for a no-magic approach.

Features

Introduction

Much like other clients, the service is declared as following:

public interface HttpBinService {
    @Get("/get")
    Future<Response> performGet();
}

An instance of this class can be generated by using RestAhead class.

var service = RestAhead.builder("https://httpbin.org")
    .build(HttpBinService.class);

Calls can then be performed simply by calling the instance of the interface:

var response = service.performGet();

Samples of services can be found in demo project here , examples of obtaining their instances are in this directory.

Options

There are multiple options you have when generating requests, all of which will be done automatically when building your project.

Response types

Out of the box the following types are supported:

  • void
  • Response
  • Future<Response>
  • CompletableFuture<Response>

Other types require you to specify an instance of Converter (rest-ahead-jackson-converter contains an implementation for Jackson library). This will allow you to use virtually any type that the converter can construct.

Example of using a return type:

interface Service {
    @Get
    void requestIgnoringResponse();

    @Get
    Response requestFullResponse();

    @Get
    Map<String, Object> performGet();

    @Get
    CustomResponseType performGetWithSpecificTarget();
}

If you require response headers and the status code as well, two more types can be used along with custom response types:

interface Service {
    // BodyResponse.body() is an optional that contains the deserialized type in case of success.
    // If non 2xx code is returned InputStream errorBody will be present, that contains untouched response body.
    @Get
    BodyResponse<CustomResponseType> get();

    // BodyAndErrorResponse.body() is an optional that contains the deserialized body in case of success.
    // If non 2xx code is returned the errorBody will contain the deserialized body
    @Get
    BodyAndErrorResponse<CustomResponseType, CustomErrorType> getErrors();
}

Body

You can specify a request body by annotating it with @Body. Doing so will make the service require a converter to serialize the body.

public interface HttpBinService {
    @Post("/post")
    Future<Response> performPost(@Body CustomRequest request);
}

Form encoding

Sending a form-url-encoded body can be done by adding @FormUrlEncoded annotation to the body:

public interface HttpBinService {
    @Post("/post")
    Future<Response> performPost(@FormUrlEncoded @Body CustomRequest request);
}

Changing the name of parameters in the form line can be done by using @FormName annotation on desired fields:

// This will cause the body to send "first=<value of first>&second=<value of b>"
record SampleFormBody(String first, @FormName("second") String b) {
}

// This will cause the body to send "customName=hello"
class SampleFormClass {
    @FormName("customName")
    String getSomething() {
        return "hello";
    }
}

Such bodies do not require a converter, one will be generated for the given type.

Supported types:

  • Map<String, String> and inherited classes
  • Records composed of primitives, boxed values, String or UUID
  • Classes with public, non-static getters returning only primitives, boxed values, String or UUID

Multipart encoding

Multipart requests can be executed as following:

public interface MultipartService {
    @Post("/post")
    HttpBinResponse postMultiPart(
        @Part String part,
        @Body @Part("two") String part2,
        @Part File file,
        @Part Path path,
        @Part FilePart part
    );
}

Parts can be added manually by using FilePart for example, it allows usage of InputStreams, byte[] etc.

Note that files and paths will be read when the request reads the body - meaning their evaluation is lazy.

Headers

Adding headers is possible by using the @Header or @Headers annotation. Valid parameters for headers are either primitive types, their boxed counterparts, Strings, instances of UUID or collections/arrays of them.

Using multiple annotations with the same value will add extra headers. The following declarations will generate requests that behave the same:

interface Service {
    @Get
    void performGet(@Header("Some-Header") String first, @Header("Some-Header") String second);

    @Get
    void performGetVarargs(@Header("Some-Header") String... headers);

    @Get
    @Headers({
        "First-Header: value",
        "Second-Header: value"
    })
    void performGetArray(@Header("Some-Header") String[] headers);

    // Can also use List, Set etc.
    @Get
    @Headers("Static-Header: header value")
    void performGetCollection(@Header("Some-Header") Collection<String> headers);
}

Queries

Queries can be added to a request in two ways, seen below. Collections, arrays and varargs types are allowed.

interface Service {
    @Get("/query?q=value")
    void getWithQuery(); // will use the preset value from path

    @Get("/query")
    void getWithParam(@Query("q") String query); // will use the parameter
}

Adapters

The default type for all services is Future<Response>. While the value can be mapped to Future<YourObject> using a converter directly, sometimes interop with other libraries is required, or maybe you need a blocking call and don't want to type .get() all the time, as well as catch the exceptions. For these cases a default adapter is included, to allow for blocking calls as evident here:

import java.util.concurrent.CompletableFuture;

interface SampleBlocking {
    @Get
    Future<Response> getFuture();

    @Get
    CompletableFuture<Response> getCompletableFuture();

    @Get
    Response getBlocking();
}

All three examples above will perform the same request, but Future and CompletableFuture will attempt to do this in non-blocking manner (this depends on the client, default JavaHttpClient supports this), but the last, Response will execute a blocking call.

If you wish to declare your own adapters simply create a class with a method annotated with @Adapter:

public class CustomAdapter {
    @Adapter
    public <T> Supplier<T> adapt(Future<T> future) {
        return () -> {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                throw new RestException(e);
            }
        };
    }
}

Adapter will also need to be added to RestAhead builder, via the addAdapter(Object object) method. Exceptions can be thrown by declared adapters and can be propagated via the service (see Call exceptions).

Interceptors

Interceptors can be added to the client to perform common logic before, after or around a request. Interceptor should implement the Interceptor interface and be added to the client like so:

var client = new JavaHttpClient();
    client.addInterceptor(new PreRequestInterceptor());

    var service = RestAhead.builder("https://httpbin.org/")
        .client(client)
        .converter(new JacksonConverter())
        .build(InterceptedService.class);

Logging interceptor

Logging interceptor is provided by default. It can be built as following:

new LoggingInterceptor.Builder()
    .logger(System.out::println)
    .logHeaders(true)
    .logBody(true)
    .joinRequestResponse(true)
    .build();

Default logger used is System.out, other values default to false.

Paths

Path parameters can also be provided through requests by using the @Path annotation on a parameter:

interface PathExample {
    @Get("/{path1}/{path2}")
    Response performGet(@Path("path1") String path, @Path String path2); // value can be omitted in favor or parameter name
}

Call exceptions

By default, no exceptions need to be declared to execute calls, but beware! An unchecked exception (RestException) will be thrown in case there was an exception thrown during execution. You can also add throws declaration for either or both exceptions that are likely to occur: ExecutionException, InterruptedException, to make sure they are handled. If either of these is not specified in the signature, RestException will still be thrown, wrapping the other one, for example:

import java.util.concurrent.ExecutionException;

public interface HttpBinService {
    // Will throw a RestException if any errors occur
    @Get("/get")
    Response performGet();

    // Allows you to handle IOException, RestException wrapping InterruptedException may still occur
    @Get("/get")
    Response performGet2() throws ExecutionException;

    // Allows you to handle both exception, no RestException will be thrown
    @Get("/get")
    Response performGet3() throws ExecutionException, InterruptedException;
}

A failed request, with custom responses will throw RequestFailedException, that contains a code and the input stream from the request.

Custom client

The RestAhead builder declares an interface Client that allows you to implement custom clients. By default, if no client is specified, Java HTTP client is used.

Spring Boot

For compatibility with spring boot you can add the following to your pom.xml:

<dependency>
    <groupId>io.github.zskamljic</groupId>
    <artifactId>rest-ahead-spring</artifactId>
    <version>${rest.ahead.version}</version>
</dependency>

To enable automatic creation of Spring beans add the @EnableRestAhead annotation to your application class as following:

@EnableRestAhead
@SpringBootApplication
public class SpringApplicationDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringApplicationDemo.class, args);
    }
}

Finally, to have services available as injectable beans add the @RestAheadService annotation to the service:

// Instead of placeholder you can also use a hardcoded URL
@RestAheadService(url = "${placeholder.url}", converter = JacksonConverter.class)
public interface DemoService {
    @Get("/get")
    Map<String, Object> performGet();
}

DemoService will then be injectable wherever you use it as a bean - either constructor injection or @Autowired injection. URL property needs to be provided to have a baseUrl configured, converter property is optional and is required only if the service requires one, see response types.

Dialects

The code generator allows for usage of multiple dialects (Default being RestAhead).

Spring

For example, Spring dialect can be used, by adding the dependency:

<dependency>
    <groupId>io.github.zskamljic</groupId>
    <artifactId>rest-ahead-spring-dialect</artifactId>
    <version>${rest.ahead.version}</version>
</dependency>

It can then be used as following:

interface SpringService {
    @GetMapping("/get")
    Response performGet(@RequestHeader String header, @RequestParam String query);

    @RequestMapping(method = RequestMethod.GET, value = "/{param}")
    HttpBinResponse get2(@PathVariable String param);

    @PostMapping("/multipart")
    HttpBinResponse postFile(@RequestPart MultiPartFile file);

    @PostMapping("/customBody")
    HttpBinResponse postFormData(@RequestPart Map<String, String> body);
}

JAX-RS

JAX-RS dialect can be used by adding:

<dependency>
    <groupId>io.github.zskamljic</groupId>
    <artifactId>rest-ahead-jax-rs</artifactId>
    <version>${rest.ahead.version}</version>
</dependency>

And it can be used as:

public interface JaxRsService {
    @GET
    @Path(("/get/{something}"))
    Map<String, Object> performGet(@PathParam("something") String something);
}

Info on how to declare a new dialect can be seen in Dialects

Adding to project

Add the dependencies as following:

<dependencies>
    <!-- other dependencies -->
    <dependency>
        <groupId>io.github.zskamljic</groupId>
        <artifactId>rest-ahead-client</artifactId>
        <version>${rest.ahead.version}</version>
    </dependency>
    <dependency>
        <groupId>io.github.zskamljic</groupId>
        <artifactId>rest-ahead-processor</artifactId>
        <version>${rest.ahead.version}</version>
        <scope>provided</scope>
    </dependency>
    <!-- If you want to use the Jackson converter -->
    <dependency>
        <groupId>io.github.zskamljic</groupId>
        <artifactId>rest-ahead-jackson-converter</artifactId>
        <version>${rest.ahead.version}</version>
    </dependency>
</dependencies>

Also add the maven-compiler-plugin if not present:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler.plugin.version}</version>
</plugin>

Snapshots

Snapshots can be accessed by adding the snapshot repository:

<repositories>
    <repository>
        <id>oss.sonatype.org-snapshot</id>
        <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
    </repository>
</repositories>

License

Project uses Apache 2.0 license. More info in license file

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages