diff --git a/README.md b/README.md index 2598aef..971c8e0 100644 --- a/README.md +++ b/README.md @@ -108,15 +108,16 @@ 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 | Map of headers (name/value) | optional | -| header | Single header as a name and a value | optional | -| httpClient | Java HttpClient object | optional | -| requestInterceptor | Function to modify the request once is built | optional | -| endsOfStream | List of texts used to mark the end of streams | optional | -| endOfStream | Text used to mark the end of streams | optional | +| Attribute | Description | Required | +| -------------------|---------------------------------------------------|-----------| +| baseUrl | Api's url | mandatory | +| headers | Map of headers (name/value) | optional | +| header | Single header as a name and a value | optional | +| httpClient | Java HttpClient object | optional | +| requestInterceptor | Function to modify the request once is built | optional | +| bodyInspector | Function to inspect the `@Body` request parameter | optional | +| endsOfStream | List of texts used to mark the end of streams | optional | +| endOfStream | Text used to mark the end of streams | optional | The attribute ```end(s)OfStream``` is required when you have endpoints sending back streams of data (Server Sent Events - SSE). diff --git a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java index 2acf2c1..37deea2 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java +++ b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.UnaryOperator; /** @@ -31,6 +32,7 @@ public class CleverClient { private final Map headers; private final HttpClient httpClient; private final UnaryOperator requestInterceptor; + private final Consumer bodyInspector; private final HttpProcessor httpProcessor; /** @@ -41,21 +43,25 @@ public class CleverClient { * @param httpClient Custom Java's HttpClient component. One is created by default if none * is passed. Optional. * @param requestInterceptor Function to modify the request once it has been built. + * @param bodyInspector Function to inspect the Body request parameter. * @param endsOfStream Texts used to mark the final of streams when handling server sent * events (SSE). Optional. */ @Builder public CleverClient(@NonNull String baseUrl, @Singular Map headers, HttpClient httpClient, - UnaryOperator requestInterceptor, @Singular("endOfStream") List endsOfStream) { + UnaryOperator requestInterceptor, Consumer bodyInspector, + @Singular("endOfStream") List endsOfStream) { this.baseUrl = baseUrl; this.headers = Optional.ofNullable(headers).orElse(Map.of()); this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient()); this.requestInterceptor = requestInterceptor; + this.bodyInspector = bodyInspector; this.httpProcessor = HttpProcessor.builder() .baseUrl(this.baseUrl) .headers(CommonUtil.mapToListOfString(this.headers)) .httpClient(this.httpClient) .requestInterceptor(this.requestInterceptor) + .bodyInspector(bodyInspector) .build(); Configurator.builder() .endsOfStream(Optional.ofNullable(endsOfStream).orElse(Arrays.asList())) 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 c99a03c..a700203 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java +++ b/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java @@ -16,6 +16,7 @@ import java.net.http.HttpClient; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import java.util.function.UnaryOperator; /** @@ -30,6 +31,7 @@ public class HttpProcessor implements InvocationHandler { private final List headers; private final HttpClient httpClient; private final UnaryOperator requestInterceptor; + private final Consumer bodyInspector; /** * Creates a generic dynamic proxy with this HttpProcessor object acting as an InvocationHandler to @@ -99,8 +101,9 @@ private Object resolve(Method method, Object[] arguments) { var url = baseUrl + URLBuilder.one().build(urlMethod, methodMetadata, arguments); var httpMethod = methodMetadata.getHttpAnnotationName(); var returnType = methodMetadata.getReturnType(); - var bodyObject = calculateBodyObject(methodMetadata, arguments); var contentType = methodMetadata.getContentType(); + var body = getAndInspectBody(methodMetadata, arguments); + var bodyObject = getBodyObject(body, contentType); var fullHeaders = new ArrayList<>(this.headers); fullHeaders.addAll(calculateHeaderContentType(contentType)); fullHeaders.addAll(interfaceMetadata.getFullHeadersByMethod(methodMetadata)); @@ -117,14 +120,24 @@ private Object resolve(Method method, Object[] arguments) { return httpConnector.sendRequest(); } - private Object calculateBodyObject(MethodMetadata methodMetadata, Object[] arguments) { + private Object getAndInspectBody(MethodMetadata methodMetadata, Object[] arguments) { var bodyIndex = methodMetadata.getBodyIndex(); - var bodyObject = bodyIndex >= 0 ? arguments[bodyIndex] : null; - if (bodyObject != null) { - if (methodMetadata.getContentType() == ContentType.MULTIPART_FORMDATA) { - bodyObject = JsonUtil.objectToMap(bodyObject); - } else if (methodMetadata.getContentType() == ContentType.APPLICATION_JSON) { - bodyObject = JsonUtil.objectToJson(bodyObject); + var body = bodyIndex >= 0 ? arguments[bodyIndex] : null; + + if (body != null && bodyInspector != null) { + bodyInspector.accept(body); + } + + return body; + } + + private Object getBodyObject(Object body, ContentType contentType) { + Object bodyObject = null; + if (body != null) { + if (contentType == ContentType.MULTIPART_FORMDATA) { + bodyObject = JsonUtil.objectToMap(body); + } else if (contentType == ContentType.APPLICATION_JSON) { + bodyObject = JsonUtil.objectToJson(body); } } return bodyObject; diff --git a/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java b/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java index 66c868a..1b2ca90 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java @@ -17,8 +17,10 @@ import java.net.http.HttpResponse; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.function.UnaryOperator; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -41,6 +43,7 @@ void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties assertNotNull(cleverClient.getBaseUrl()); assertNotNull(cleverClient.getHttpProcessor()); assertNull(cleverClient.getRequestInterceptor()); + assertNull(cleverClient.getBodyInspector()); } @Test @@ -122,6 +125,44 @@ void shouldModifyRequestWhenPassingInterceptorFunction() { assertEquals(expectedBody, actualBody); } + @SuppressWarnings("unchecked") + @Test + void shouldNotThrownExceptionWhenBodyInspectorEndsSuccessfully() { + var httpClient = mock(HttpClient.class); + Consumer bodyInspector = body -> { + var sample = (Sample) body; + if (sample.getModel() == null) { + throw new IllegalArgumentException("The parameter model must not be null."); + } + }; + var cleverClient = CleverClient.builder() + .baseUrl("https://test") + .bodyInspector(bodyInspector) + .httpClient(httpClient) + .build(); + when(httpClient.sendAsync(any(), any())) + .thenReturn(CompletableFuture.completedFuture(mock(HttpResponse.class))); + var testService = cleverClient.create(TestCleverClient.class); + assertDoesNotThrow(() -> testService.getText(Sample.builder().model("abc").build(), "math")); + } + + @Test + void shouldThrownExceptionWhenBodyInspectorFails() { + Consumer bodyInspector = body -> { + var sample = (Sample) body; + if (sample.getModel() == null) { + throw new IllegalArgumentException("The parameter model must not be null."); + } + }; + var cleverClient = CleverClient.builder() + .baseUrl("https://test") + .bodyInspector(bodyInspector) + .build(); + var testService = cleverClient.create(TestCleverClient.class); + assertThrows(IllegalArgumentException.class, + () -> testService.getText(Sample.builder().build(), "math")); + } + @Value @Builder static class Sample {