diff --git a/README.md b/README.md index 1fb0294..934e9ea 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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(); ``` diff --git a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java index 5f31164..915402e 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java +++ b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java @@ -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; @@ -30,29 +31,32 @@ public class CleverClient { private final String urlBase = null; private final List headers; private final HttpClient httpClient; + private final Function 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 headers, HttpClient httpClient, - String endOfStream) { - if (isNullOrEmpty(baseUrl) && isNullOrEmpty(urlBase)) { + Function 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; @@ -61,8 +65,14 @@ public CleverClient(String baseUrl, String urlBase, @Singular List 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."); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java b/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java index 01ba735..77070fe 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java +++ b/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java @@ -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; @@ -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 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 headers, HttpClient httpClient) { - this.baseUrl = baseUrl; - this.headers = headers; - this.httpClient = httpClient; - } + private final HttpClient httpClient; + private final Function urlInterceptor; /** * Creates a generic dynamic proxy with this HttpProcessor object acting as an @@ -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(); diff --git a/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java b/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java index f3b6566..176bc94 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java @@ -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 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 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 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 getText(@Query("prefix") String prefix); + } } diff --git a/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java b/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java index e007c1e..230676f 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java @@ -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