Skip to content

Commit

Permalink
Merge pull request #41 from sashirestela/40-post-modify-the-url-from-…
Browse files Browse the repository at this point in the history
…the-client-side

Post modify the url from the client side
  • Loading branch information
sashirestela authored Feb 9, 2024
2 parents 10a52b9 + d3f4369 commit 6bb2594
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 101 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ Take in account that you need to use **Java 11 or greater**.

We have the following attributes to create a CleverClient object:

| Attribute | Description | Required |
| ----------- |----------------------------------------|-----------|
| baseUrl | Api's url | mandatory |
| headers | Pairs of headers name/value | optional |
| httpClient | Java HttpClient object | optional |
| endOfStream | Text used to mark the final of streams | optional |
| Attribute | Description | Required |
| -------------- |------------------------------------------|-----------|
| baseUrl | Api's url | mandatory |
| headers | Pairs of headers name/value | optional |
| httpClient | Java HttpClient object | optional |
| urlInterceptor | Function to modify the url once is built | optional |
| endOfStream | Text used to mark the final of streams | optional |

The attribute ```endOfStream``` is required when you have endpoints sending back streams of data (Server Sent Events - SSE).

Expand All @@ -124,6 +125,7 @@ var cleverClient = CleverClient.builder()
.baseUrl(BASE_URL)
.headers(Arrays.asList(HEADER_NAME, HEADER_VALUE))
.httpClient(httpClient)
.urlInterceptor(url -> url + (url.contains("?") ? "&" : "?") + "env=testing")
.endOfStream(END_OF_STREAM)
.build();
```
Expand Down
42 changes: 26 additions & 16 deletions src/main/java/io/github/sashirestela/cleverclient/CleverClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.http.HttpClient;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -30,29 +31,32 @@ public class CleverClient {
private final String urlBase = null;
private final List<String> headers;
private final HttpClient httpClient;
private final Function<String, String> urlInterceptor;
private final HttpProcessor httpProcessor;

/**
* Constructor to create an instance of CleverClient.
*
* @param baseUrl Root of the url of the API service to call.
* at least one of baseUrl and the deprecated urlBase is mandatory.
* in case both are specified and different baseUrl takes precedence
*
* @param urlBase [[ Deprecated ]] Root of the url of the API service to call.
* it is here for backward compatibility only. It will be removed in
* a future version. use `baseUrl()` instead.
* @param headers Http headers for all the API service. Header's name and
* value must be individual entries in the list. Optional.
* @param httpClient Custom Java's HttpClient component. One is created by
* default if none is passed. Optional.
* @param endOfStream Text used to mark the final of streams when handling
* server sent events (SSE). Optional.
* @param baseUrl Root of the url of the API service to call. At least
* one of baseUrl and the deprecated urlBase is mandatory.
* In case both are specified and different baseUrl takes
* precedence.
* @param urlBase [[ Deprecated ]] Root of the url of the API service to
* call. it is here for backward compatibility only. It
* will be removed in a future version. use `baseUrl()`
* instead.
* @param headers Http headers for all the API service. Header's name and
* value must be individual entries in the list. Optional.
* @param httpClient Custom Java's HttpClient component. One is created by
* default if none is passed. Optional.
* @param urlInterceptor Function to modify the url once it has been built.
* @param endOfStream Text used to mark the final of streams when handling
* server sent events (SSE). Optional.
*/
@Builder
public CleverClient(String baseUrl, String urlBase, @Singular List<String> headers, HttpClient httpClient,
String endOfStream) {
if (isNullOrEmpty(baseUrl) && isNullOrEmpty(urlBase)) {
Function<String, String> urlInterceptor, String endOfStream) {
if (isNullOrEmpty(baseUrl) && isNullOrEmpty(urlBase)) {
throw new CleverClientException("At least one of baseUrl and urlBase is mandatory.", null, null);
}
this.baseUrl = isNullOrEmpty(baseUrl) ? urlBase : baseUrl;
Expand All @@ -61,8 +65,14 @@ public CleverClient(String baseUrl, String urlBase, @Singular List<String> heade
throw new CleverClientException("Headers must be entered as pair of values in the list.", null, null);
}
this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient());
this.urlInterceptor = urlInterceptor;
CleverClientSSE.setEndOfStream(endOfStream);
this.httpProcessor = new HttpProcessor(this.baseUrl, this.headers, this.httpClient);
this.httpProcessor = HttpProcessor.builder()
.baseUrl(this.baseUrl)
.headers(this.headers)
.httpClient(this.httpClient)
.urlInterceptor(this.urlInterceptor)
.build();
logger.debug("CleverClient has been created.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.net.http.HttpClient;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -15,29 +16,19 @@
import io.github.sashirestela.cleverclient.metadata.InterfaceMetadataStore;
import io.github.sashirestela.cleverclient.util.Constant;
import io.github.sashirestela.cleverclient.util.ReflectUtil;
import lombok.Builder;

/**
* HttpProcessor orchestrates all the http interaction.
*/
@Builder
public class HttpProcessor implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(HttpProcessor.class);

private final HttpClient httpClient;
private final String baseUrl;
private final List<String> headers;

/**
* Constructor to create an instance of HttpProcessor.
*
* @param httpClient Java's HttpClient component that solves the http calling.
* @param baseUrl Root of the url of the API service to call.
* @param headers Http headers for all the API service.
*/
public HttpProcessor(String baseUrl, List<String> headers, HttpClient httpClient) {
this.baseUrl = baseUrl;
this.headers = headers;
this.httpClient = httpClient;
}
private final HttpClient httpClient;
private final Function<String, String> urlInterceptor;

/**
* Creates a generic dynamic proxy with this HttpProcessor object acting as an
Expand Down Expand Up @@ -107,6 +98,9 @@ private Object resolve(Method method, Object[] arguments) {
var methodMetadata = interfaceMetadata.getMethodBySignature().get(method.toString());
var urlMethod = interfaceMetadata.getFullUrlByMethod(methodMetadata);
var url = baseUrl + URLBuilder.one().build(urlMethod, methodMetadata, arguments);
if (urlInterceptor != null) {
url = urlInterceptor.apply(url);
}
var httpMethod = methodMetadata.getHttpAnnotationName();
var returnType = methodMetadata.getReturnType();
var isMultipart = methodMetadata.isMultipart();
Expand Down
171 changes: 107 additions & 64 deletions src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,125 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import io.github.sashirestela.cleverclient.annotation.GET;
import io.github.sashirestela.cleverclient.annotation.Query;
import io.github.sashirestela.cleverclient.annotation.Resource;
import io.github.sashirestela.cleverclient.support.CleverClientException;

class CleverClientTest {

@Test
void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties() {
var cleverClient = CleverClient.builder()
.baseUrl("https://test")
.build();
assertEquals(List.of(), cleverClient.getHeaders());
assertEquals(HttpClient.Version.HTTP_2, cleverClient.getHttpClient().version());
assertNotNull(cleverClient.getBaseUrl());
assertNotNull(cleverClient.getHttpProcessor());
assertNull(cleverClient.getUrlInterceptor());
}

@Test
void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties() {
var cleverClient = CleverClient.builder()
.baseUrl("https://test")
.build();
assertEquals(List.of(), cleverClient.getHeaders());
assertEquals(HttpClient.Version.HTTP_2, cleverClient.getHttpClient().version());
assertNotNull(cleverClient.getBaseUrl());
assertNotNull(cleverClient.getHttpProcessor());
}

@Test
void shouldBuildSuccessfullyUsingDeprecatedUrlBase() {
var baseUrl = "https://test";
var cleverClient = CleverClient.builder()
.urlBase(baseUrl)
.build();
assertEquals(List.of(), cleverClient.getHeaders());
assertEquals(HttpClient.Version.HTTP_2, cleverClient.getHttpClient().version());
// verify that baseUrl is set when building with the deprecated urlBase() method
assertEquals(cleverClient.getBaseUrl(), baseUrl);
assertNotNull(cleverClient.getHttpProcessor());
}

@Test
void shouldImplementInterfaceWhenCallingCreate() {
var cleverClient = CleverClient.builder()
.baseUrl("https://test")
.header("headerName")
.header("headerValue")
.httpClient(HttpClient.newHttpClient())
.endOfStream("[DONE]")
.build();
var test = cleverClient.create(TestCleverClient.class);
assertNotNull(test);
}

@Test
void shouldThrownExceptionWhenTryingToPassAnEmptyBaseUrlAndUrlBase() {
var cleverClientBuilder = CleverClient.builder()
.header("headerName")
.header("headerValue")
.httpClient(HttpClient.newHttpClient())
.endOfStream("[DONE]");
assertThrows(CleverClientException.class,
cleverClientBuilder::build);
}

@Test
void shouldThrownExceptionWhenTryingToPassAnOddNumbersOfHeaders() {
var cleverClientBuilder = CleverClient.builder()
.baseUrl("http://test")
.header("oneHeader");
Exception exception = assertThrows(CleverClientException.class,
cleverClientBuilder::build);
assertEquals("Headers must be entered as pair of values in the list.",
exception.getMessage());
}

interface TestCleverClient {

@GET("/api/")
CompletableFuture<String> getText();
}
@Test
void shouldBuildSuccessfullyUsingDeprecatedUrlBase() {
var baseUrl = "https://test";
var cleverClient = CleverClient.builder()
.urlBase(baseUrl)
.build();
assertEquals(List.of(), cleverClient.getHeaders());
assertEquals(HttpClient.Version.HTTP_2, cleverClient.getHttpClient().version());
// verify that baseUrl is set when building with the deprecated urlBase() method
assertEquals(cleverClient.getBaseUrl(), baseUrl);
assertNotNull(cleverClient.getHttpProcessor());
}

@Test
void shouldImplementInterfaceWhenCallingCreate() {
var cleverClient = CleverClient.builder()
.baseUrl("https://test")
.header("headerName")
.header("headerValue")
.httpClient(HttpClient.newHttpClient())
.endOfStream("[DONE]")
.build();
var test = cleverClient.create(TestCleverClient.class);
assertNotNull(test);
}

@Test
void shouldThrownExceptionWhenTryingToPassAnEmptyBaseUrlAndUrlBase() {
var cleverClientBuilder = CleverClient.builder()
.header("headerName")
.header("headerValue")
.httpClient(HttpClient.newHttpClient())
.endOfStream("[DONE]");
assertThrows(CleverClientException.class,
cleverClientBuilder::build);
}

@Test
void shouldThrownExceptionWhenTryingToPassAnOddNumbersOfHeaders() {
var cleverClientBuilder = CleverClient.builder()
.baseUrl("http://test")
.header("oneHeader");
Exception exception = assertThrows(CleverClientException.class,
cleverClientBuilder::build);
assertEquals("Headers must be entered as pair of values in the list.",
exception.getMessage());
}

@SuppressWarnings("unchecked")
@Test
void shouldModifyUrlWhenPassingUrlInterceptorFunction() {
var httpClient = mock(HttpClient.class);
Function<String, String> customUrlInterceptor = url -> {
// add a query parameter to url
url = url + (url.contains("?") ? "&" : "?") + "api-version=2024-01-31";
// remove '/vN' or '/vN.M' from url
url = url.replaceFirst("(\\/v\\d+\\.*\\d*)", "");
return url;
};
var cleverClient = CleverClient.builder()
.baseUrl("https://test")
.urlInterceptor(customUrlInterceptor)
.httpClient(httpClient)
.build();
when(httpClient.sendAsync(any(), any()))
.thenReturn(CompletableFuture.completedFuture(mock(HttpResponse.class)));

var test = cleverClient.create(TestCleverClient.class);
test.getText("geo");

ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient, times(1)).sendAsync(requestCaptor.capture(), any());

var actualUrl = requestCaptor.getValue().uri().toString();
var expectedUrl = "https://test/api/text?prefix=geo&api-version=2024-01-31";
assertEquals(expectedUrl, actualUrl);
}

@Resource("/v1.2/api")
interface TestCleverClient {

@GET("/text")
CompletableFuture<String> getText(@Query("prefix") String prefix);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ class HttpProcessorTest {

@BeforeEach
void init() {
httpProcessor = new HttpProcessor("https://api.demmo", List.of(), httpClient);
httpProcessor = HttpProcessor.builder()
.baseUrl("https://api.demo")
.headers(List.of())
.httpClient(httpClient)
.build();
}

@Test
Expand Down

0 comments on commit 6bb2594

Please sign in to comment.