Skip to content

Commit a959dbc

Browse files
committed
Add Async bean() support
1 parent b2267d0 commit a959dbc

File tree

5 files changed

+155
-2
lines changed

5 files changed

+155
-2
lines changed

client/src/main/java/io/avaje/http/client/DHttpAsync.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,10 @@ public CompletableFuture<HttpResponse<String>> asString() {
2525
.thenApply(request::afterAsync);
2626
}
2727

28+
@Override
29+
public <E> CompletableFuture<E> bean(Class<E> type) {
30+
return request
31+
.performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray())
32+
.thenApply(httpResponse -> request.asyncBean(type, httpResponse));
33+
}
2834
}

client/src/main/java/io/avaje/http/client/DHttpClientContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void checkResponse(HttpResponse<?> response) {
100100
}
101101
}
102102

103-
void check(HttpResponse<byte[]> response) {
103+
void checkMaybeThrow(HttpResponse<byte[]> response) {
104104
if (response.statusCode() >= 300) {
105105
throw new HttpException(this, response);
106106
}

client/src/main/java/io/avaje/http/client/DHttpClientRequest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ public HttpClientResponse TRACE() {
327327
private void readResponseContent() {
328328
final HttpResponse<byte[]> response = asByteArray();
329329
this.httpResponse = response;
330-
context.check(response);
330+
context.checkMaybeThrow(response);
331331
encodedResponseBody = context.readContent(response);
332332
}
333333

@@ -393,6 +393,15 @@ protected <T> CompletableFuture<HttpResponse<T>> performSendAsync(boolean loggab
393393
return context.sendAsync(httpRequest, responseHandler);
394394
}
395395

396+
protected <E> E asyncBean(Class<E> type, HttpResponse<byte[]> response) {
397+
requestTimeNanos = System.nanoTime() - startAsyncNanos;
398+
httpResponse = response;
399+
encodedResponseBody = context.readContent(response);
400+
context.afterResponse(this);
401+
context.checkMaybeThrow(response);
402+
return context.readBean(type, encodedResponseBody);
403+
}
404+
396405
public <E> HttpResponse<E> afterAsync(HttpResponse<E> response) {
397406
requestTimeNanos = System.nanoTime() - startAsyncNanos;
398407
httpResponse = response;

client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,37 @@ public interface HttpAsyncResponse {
1818
*/
1919
CompletableFuture<HttpResponse<String>> asString();
2020

21+
/**
22+
* Process expecting a (json) bean response body.
23+
* <p>
24+
* If the HTTP statusCode is 300 or above a HttpException is throw which
25+
* contains the HttpResponse.
26+
*
27+
* <pre>{@code
28+
*
29+
* clientContext.request()
30+
* ...
31+
* .POST().async()
32+
* .bean(HelloDto.class)
33+
* .whenComplete((helloDto, throwable) -> {
34+
*
35+
* if (throwable != null) {
36+
* HttpException httpException = (HttpException) throwable.getCause();
37+
* int statusCode = httpException.getStatusCode();
38+
*
39+
* // maybe convert json error response body to a bean (using Jackson/Gson)
40+
* MyErrorBean errorResponse = httpException.bean(MyErrorBean.class);
41+
* ..
42+
*
43+
* } else {
44+
* // use helloDto
45+
* ...
46+
* }
47+
*
48+
* });
49+
*
50+
*
51+
* }</pre>
52+
*/
53+
<E> CompletableFuture<E> bean(Class<E> type);
2154
}

client/src/test/java/io/avaje/http/client/HelloControllerTest.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
import java.util.List;
99
import java.util.Map;
1010
import java.util.concurrent.CompletableFuture;
11+
import java.util.concurrent.CompletionException;
1112
import java.util.concurrent.ExecutionException;
13+
import java.util.concurrent.atomic.AtomicInteger;
14+
import java.util.concurrent.atomic.AtomicReference;
1215
import java.util.stream.Collectors;
1316
import java.util.stream.Stream;
1417

@@ -106,6 +109,108 @@ void get_withPathParamAndQueryParam_returningBean() {
106109
assertThat(dto.otherParam).isEqualTo("other");
107110
}
108111

112+
@Test
113+
void async_whenComplete_returningBean() throws ExecutionException, InterruptedException {
114+
115+
final AtomicInteger counter = new AtomicInteger();
116+
final AtomicReference<HelloDto> ref = new AtomicReference<>();
117+
118+
final CompletableFuture<HelloDto> future = clientContext.request()
119+
.path("hello/43/2020-03-05").queryParam("otherParam", "other").queryParam("foo", null)
120+
.GET()
121+
.async().bean(HelloDto.class);
122+
123+
future.whenComplete((dto, throwable) -> {
124+
counter.incrementAndGet();
125+
ref.set(dto);
126+
127+
assertThat(throwable).isNull();
128+
assertThat(dto.id).isEqualTo(43L);
129+
assertThat(dto.name).isEqualTo("2020-03-05");
130+
assertThat(dto.otherParam).isEqualTo("other");
131+
});
132+
133+
// wait ...
134+
final HelloDto dto = future.get();
135+
assertThat(counter.incrementAndGet()).isEqualTo(2);
136+
assertThat(dto).isSameAs(ref.get());
137+
138+
assertThat(dto.id).isEqualTo(43L);
139+
assertThat(dto.name).isEqualTo("2020-03-05");
140+
assertThat(dto.otherParam).isEqualTo("other");
141+
}
142+
143+
@Test
144+
void async_whenComplete_throwingHttpException() {
145+
146+
AtomicReference<HttpException> causeRef = new AtomicReference<>();
147+
148+
final CompletableFuture<HelloDto> future = clientContext.request()
149+
.path("hello/saveform3")
150+
.formParam("name", "Bax")
151+
.formParam("email", "notValidEmail")
152+
.formParam("url", "notValidUrl")
153+
.formParam("startDate", "2030-12-03")
154+
.POST()
155+
.async()
156+
.bean(HelloDto.class)
157+
.whenComplete((helloDto, throwable) -> {
158+
// we get a throwable
159+
assertThat(throwable.getCause()).isInstanceOf(HttpException.class);
160+
assertThat(helloDto).isNull();
161+
162+
final HttpException httpException = (HttpException) throwable.getCause();
163+
causeRef.set(httpException);
164+
assertThat(httpException.getStatusCode()).isEqualTo(422);
165+
166+
// convert json error response body to a bean
167+
final ErrorResponse errorResponse = httpException.bean(ErrorResponse.class);
168+
169+
final Map<String, String> errorMap = errorResponse.getErrors();
170+
assertThat(errorMap.get("url")).isEqualTo("must be a valid URL");
171+
assertThat(errorMap.get("email")).isEqualTo("must be a well-formed email address");
172+
});
173+
174+
try {
175+
future.join();
176+
} catch (CompletionException e) {
177+
assertThat(e.getCause()).isSameAs(causeRef.get());
178+
}
179+
}
180+
181+
@Test
182+
void async_exceptionally_style() {
183+
184+
AtomicReference<HttpException> causeRef = new AtomicReference<>();
185+
186+
final CompletableFuture<HelloDto> future = clientContext.request()
187+
.path("hello/saveform3")
188+
.formParam("name", "Bax")
189+
.formParam("email", "notValidEmail")
190+
.formParam("url", "notValidUrl")
191+
.formParam("startDate", "2030-12-03")
192+
.POST()
193+
.async()
194+
.bean(HelloDto.class);
195+
196+
future.exceptionally(throwable -> {
197+
final HttpException httpException = (HttpException) throwable.getCause();
198+
causeRef.set(httpException);
199+
assertThat(httpException.getStatusCode()).isEqualTo(422);
200+
201+
return new HelloDto(0, "ErrorResponse", "");
202+
203+
}).thenAccept(helloDto -> {
204+
assertThat(helloDto.name).isEqualTo("ErrorResponse");
205+
});
206+
207+
try {
208+
future.join();
209+
} catch (CompletionException e) {
210+
assertThat(e.getCause()).isSameAs(causeRef.get());
211+
}
212+
}
213+
109214
@Test
110215
void post_bean_returningBean_usingExplicitConverters() {
111216

0 commit comments

Comments
 (0)