Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post modify the url from the client side #41

Merged
merged 2 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading