Skip to content

Commit

Permalink
Add RestClient for Java, Kotlin Coroutine and Scala (#4263)
Browse files Browse the repository at this point in the history
Motivation:

`WebClient` and `WebClientPreparation` has powerful and fluent features.
RESTful API is classic but it is still the dominant protocol for sharing
web resources.
The `WebClient` is able to handle not only REST responses but also
a stream response and a file response.
The API design of `WebClient`, in turn, is not optimized for REST API.
See #4250 

Modifications:

- Add `RestClient` for exchange RESTful APIs.
  - It assume that the response has a JSON body.
- Add `RestClientBuilder` so as to fluently build a `RestClient`.
- Refactor the intefaces for request preparations to accommodate
  `RestClientPreparation`.
  - Method and path setters are extracted to `RequestMethodSetters`.
  - Query and path param setters are moved to `PathAndQueryParamSetters`
  - Added `HttpMessageSetters` for setting `HttpMessage` properties.
- Add `execute()` extension methods for Kotlin Coroutine.
- Add `ScalaRestClient` for Scala.
  - Tried to add extension methods using an implicit class but Scala
    compiler failed to find the overloaded method which has a generic
    parameter.
- Provide a extension method to convert a `WebClient` to
  `ScalaRestClient`.
- Add `restClient()` to `ServerExtension` and `ServerRule`.

Result:

You can now easily send and receive RESTful APIs using `RestClient`
- Java
  ```java
  RestClient restClient = RestClient.of("...");
  CompletableFuture<ResponseEntity<Customer>> response =
      restClient.get("/api/v1/customers/{customerId}")
                .pathParam("customerId", "0000001")
                .execute(Customer.class);
  ```
- Kotlin
  ```kt
  val restClient: RestClient = RestClient.of("...");
  val response: ResponseEntity<Customer> =
     restClient
       .get("/api/v1/customers/{customerId}")
       .pathParam("customerId", "0000001")
       .execute<Customer>()  // a suspend function
  ```
- Scala
  ```scala
  val restClient: ScalaRestClient = ScalaRestClient("...")
  val response: Future[ResponseEntity[Result]] =
    restClient.post("/api/v1/customers")
              .contentJson(new Customer(...))
              .execute[Result]()
  ```
  • Loading branch information
ikhoon authored Jul 1, 2022
1 parent 197347d commit f407e1f
Show file tree
Hide file tree
Showing 37 changed files with 2,443 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
*/
@UnstableApi
public final class BlockingWebClientRequestPreparation
implements RequestPreparationSetters<AggregatedHttpResponse> {
implements WebRequestPreparationSetters<AggregatedHttpResponse> {

private final WebClientRequestPreparation delegate;

Expand Down Expand Up @@ -353,6 +353,12 @@ public BlockingWebClientRequestPreparation headers(
return this;
}

@Override
public BlockingWebClientRequestPreparation trailer(CharSequence name, Object value) {
delegate.trailer(name, value);
return this;
}

@Override
public BlockingWebClientRequestPreparation trailers(
Iterable<? extends Entry<? extends CharSequence, String>> trailers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.client;

import static java.util.Objects.requireNonNull;

import java.net.URI;

import com.linecorp.armeria.client.endpoint.EndpointGroup;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.Scheme;

final class DefaultRestClient implements RestClient {

static final RestClient DEFAULT = new DefaultRestClient(WebClient.of());

private final WebClient delegate;

DefaultRestClient(WebClient delegate) {
requireNonNull(delegate, "delegate");
this.delegate = delegate;
}

@Override
public RestClientPreparation path(HttpMethod method, String path) {
requireNonNull(method, "method");
requireNonNull(path, "path");
return new RestClientPreparation(delegate, method, path);
}

@Override
public Scheme scheme() {
return delegate.scheme();
}

@Override
public EndpointGroup endpointGroup() {
return delegate.endpointGroup();
}

@Override
public String absolutePathRef() {
return delegate.absolutePathRef();
}

@Override
public URI uri() {
return delegate.uri();
}

@Override
public Class<?> clientType() {
return delegate.clientType();
}

@Override
public ClientOptions options() {
return delegate.options();
}

@Override
public HttpClient unwrap() {
return delegate.unwrap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ final class DefaultWebClient extends UserClient<HttpRequest, HttpResponse> imple

@Nullable
private BlockingWebClient blockingWebClient;
@Nullable
private RestClient restClient;

DefaultWebClient(ClientBuilderParams params, HttpClient delegate, MeterRegistry meterRegistry) {
super(params, delegate, meterRegistry,
Expand Down Expand Up @@ -130,6 +132,14 @@ public BlockingWebClient blocking() {
return blockingWebClient = new DefaultBlockingWebClient(this);
}

@Override
public RestClient asRestClient() {
if (restClient != null) {
return restClient;
}
return restClient = RestClient.of(this);
}

@Override
public HttpClient unwrap() {
return (HttpClient) super.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
*/
@UnstableApi
public final class FutureTransformingRequestPreparation<T>
implements RequestPreparationSetters<CompletableFuture<T>> {
implements WebRequestPreparationSetters<CompletableFuture<T>> {

private static final Logger logger = LoggerFactory.getLogger(FutureTransformingRequestPreparation.class);

Expand Down Expand Up @@ -291,6 +291,12 @@ public FutureTransformingRequestPreparation<T> headers(
return this;
}

@Override
public FutureTransformingRequestPreparation<T> trailer(CharSequence name, Object value) {
delegate.trailer(name, value);
return this;
}

@Override
public FutureTransformingRequestPreparation<T> trailers(
Iterable<? extends Entry<? extends CharSequence, String>> trailers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@

import java.time.Duration;

import com.linecorp.armeria.common.HttpRequestSetters;
import com.linecorp.armeria.common.HttpMessageSetters;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.PathAndQueryParamSetters;
import com.linecorp.armeria.common.annotation.UnstableApi;

import io.netty.util.AttributeKey;

interface RequestPreparationSetters<T> extends HttpRequestSetters, RequestOptionsSetters {

/**
* Builds and executes the request.
*/
T execute();
/**
* Provides the setters for building an {@link HttpRequest} and {@link RequestOptions}.
*/
@UnstableApi
public interface RequestPreparationSetters extends PathAndQueryParamSetters, HttpMessageSetters,
RequestOptionsSetters {

/**
* Sets the specified {@link RequestOptions} that could overwrite the previously configured values such as
* {@link #responseTimeout(Duration)}, {@link #writeTimeout(Duration)}, {@link #maxResponseLength(long)}
* and {@link #attr(AttributeKey, Object)}.
*/
RequestPreparationSetters<T> requestOptions(RequestOptions requestOptions);
RequestPreparationSetters requestOptions(RequestOptions requestOptions);
}
Loading

0 comments on commit f407e1f

Please sign in to comment.