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

Add inspector for Body request parameter #66

Merged
merged 3 commits into from
Mar 27, 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
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.sashirestela</groupId>
<artifactId>cleverclient</artifactId>
<version>1.3.3</version>
<version>1.4.0</version>
<packaging>jar</packaging>

<name>cleverclient</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -31,6 +32,7 @@ public class CleverClient {
private final Map<String, String> headers;
private final HttpClient httpClient;
private final UnaryOperator<HttpRequestData> requestInterceptor;
private final Consumer<Object> bodyInspector;
private final HttpProcessor httpProcessor;

/**
Expand All @@ -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<String, String> headers, HttpClient httpClient,
UnaryOperator<HttpRequestData> requestInterceptor, @Singular("endOfStream") List<String> endsOfStream) {
UnaryOperator<HttpRequestData> requestInterceptor, Consumer<Object> bodyInspector,
@Singular("endOfStream") List<String> 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()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -30,6 +31,7 @@ public class HttpProcessor implements InvocationHandler {
private final List<String> headers;
private final HttpClient httpClient;
private final UnaryOperator<HttpRequestData> requestInterceptor;
private final Consumer<Object> bodyInspector;

/**
* Creates a generic dynamic proxy with this HttpProcessor object acting as an InvocationHandler to
Expand Down Expand Up @@ -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));
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,6 +43,7 @@ void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties
assertNotNull(cleverClient.getBaseUrl());
assertNotNull(cleverClient.getHttpProcessor());
assertNull(cleverClient.getRequestInterceptor());
assertNull(cleverClient.getBodyInspector());
}

@Test
Expand Down Expand Up @@ -122,6 +125,44 @@ void shouldModifyRequestWhenPassingInterceptorFunction() {
assertEquals(expectedBody, actualBody);
}

@SuppressWarnings("unchecked")
@Test
void shouldNotThrownExceptionWhenBodyInspectorEndsSuccessfully() {
var httpClient = mock(HttpClient.class);
Consumer<Object> 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<Object> 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);
var sample = Sample.builder().id("1").build();
assertThrows(IllegalArgumentException.class, () -> testService.getText(sample, "math"));
}

@Value
@Builder
static class Sample {
Expand Down