-
Notifications
You must be signed in to change notification settings - Fork 896
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
Provide a way to automatically deserialize non-OK JSON response using WebClient and RestClient #4412
Conversation
I have read the CLA document and already signed it. |
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #4412 +/- ##
=======================================
Coverage ? 73.97%
Complexity ? 18210
=======================================
Files ? 1538
Lines ? 67592
Branches ? 8523
=======================================
Hits ? 50000
Misses ? 13524
Partials ? 4068 ☔ View full report in Codecov by Sentry. |
@@ -105,6 +105,22 @@ static <T> FutureResponseAs<ResponseEntity<T>> json(Class<? extends T> clazz) { | |||
return aggregateAndConvert(AggregatedResponseAs.json(clazz)); | |||
} | |||
|
|||
@UnstableApi | |||
static <T> FutureResponseAs<ResponseEntity<T>> json(Class<? extends T> clazz, | |||
HttpStatusPredicate predicate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Global comment:
We cannot expose HttpStatusPredicate
and HttpStatusClassPredicate
to the public API because it's package private classes.
Instead, could you add method variants that take HttpStatus
, HttpStatusClass
and Predicate<HttpStatus>
, please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, I was careless.
I'll address your comments quickly.
Thank you for your comments!
I addressed your comments, @minwoox . |
I'm sorry, I accidentally push re-request review btn. |
@@ -41,26 +44,73 @@ static <T> ResponseAs<AggregatedHttpResponse, ResponseEntity<T>> json(Class<? ex | |||
return response -> newJsonResponseEntity(response, bytes -> JacksonUtil.readValue(bytes, clazz)); | |||
} | |||
|
|||
static <T> ResponseAs<AggregatedHttpResponse, ResponseEntity<T>> json(Class<? extends T> clazz, | |||
Predicate<HttpStatus> predicate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Global comment: Should we add ? super
for the type parameter of Predicate
?
Predicate<HttpStatus> predicate) { | |
Predicate<? super HttpStatus> predicate) { |
@@ -41,26 +44,73 @@ static <T> ResponseAs<AggregatedHttpResponse, ResponseEntity<T>> json(Class<? ex | |||
return response -> newJsonResponseEntity(response, bytes -> JacksonUtil.readValue(bytes, clazz)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also use HttpStatusClassPredicate
for this?
private static final HttpStatusClassPredicate SUCCESS_PREDICATE = ...;
...
return json(clazz, SUCCESS_PREDICATE);
public <T> FutureTransformingRequestPreparation<ResponseEntity<T>> asJson(Class<? extends T> clazz, | ||
HttpStatusClass httpStatusClass) { | ||
requireNonNull(httpStatusClass, "httpStatusClass"); | ||
return asJson(clazz, new HttpStatusClassPredicate(httpStatusClass)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HttpStatusClass
is an enum so we can statistically create HttpStatusClassPredicate
s.
How about adding a static factory method for HttpStatusClassPredicate
?
// Returns a pre-populated object.
HttpStatusClassPredicate.of(httpStatusClass)
We may apply a similar approach to HttpStatusPrediate
…ientRequestPreparation
I have addressed your comment @ikhoon . |
new HttpStatusClassPredicates(HttpStatusClass.UNKNOWN); | ||
|
||
static HttpStatusClassPredicates of(HttpStatusClass httpStatusClass) { | ||
if (httpStatusClass == HttpStatusClass.INFORMATIONAL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should we use switch
statement?
AggregatedHttpResponse response) { | ||
return new InvalidHttpResponseException( | ||
response, "status: " + response.status() + | ||
" (expect: the success class (2xx). response: " + response, null); | ||
" is not expected by predicate method. response: " + response, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add predicate
to the error message. Some users might want to override toString()
for debugging purposes instead of using lambda expressions.
" is not expected by predicate method. response: " + response, null); | |
" is not expected by predicate method. response: " + response ", predicate: " + predicate, null); |
private static final Map<HttpStatus, HttpStatusPredicate> httpStatusPredicateMap; | ||
|
||
static { | ||
httpStatusPredicateMap = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer to use immutable collections if possible. Should we use Guavas
ImmutableMap.Builderto create
httpStatusPredicateMap`?
} | ||
|
||
static HttpStatusPredicate of(HttpStatus httpStatus) { | ||
return httpStatusPredicateMap.get(httpStatus); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A custom HttpStatus
can have a status code larger than 1000. Could we create a new HttpStatusPredicate
if httpStatusPredicateMap.get(httpStatus)
returns null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, it's my mistake.
I'm going to revise this as static factory method returns a new HttpStatusPredicate object when a status code is larger than 1000.
/** | ||
* Deserializes the JSON response content into the specified non-container type | ||
* using the default {@link ObjectMapper}. | ||
* {@link HttpStatus} type argument specify what type of response is allowed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
* {@link HttpStatus} type argument specify what type of response is allowed. | |
* {@link HttpStatus} type argument specifies what type of response is allowed. |
/** | ||
* Deserializes the JSON response content into the specified non-container type | ||
* using the default {@link ObjectMapper}. | ||
* {@link HttpStatusClass} type argument specify what type of response is allowed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* {@link HttpStatusClass} type argument specify what type of response is allowed. | |
* {@link HttpStatusClass} type argument specifies what type of response is allowed. |
/** | ||
* Deserializes the JSON content of the {@link HttpResponse} into the specified non-container type using | ||
* the default {@link ObjectMapper}. | ||
* {@link HttpStatus} type argument specify what type of response is allowed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about?
* {@link HttpStatus} type argument specify what type of response is allowed. | |
* Note that if the specified {@link HttpStatus} is different from the {@link HttpStatus} | |
* of the response, an {@link InvalidHttpResponseException} is raised. |
class HttpStatusClassPredicatesTest { | ||
|
||
@Test | ||
public void httpStatusClassIsEqualToTestArgument() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Remove public
from the methods?
class HttpStatusPredicateTest { | ||
|
||
@Test | ||
public void httpStatusIsEqualToTestArgument() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Remove public
from the methods?
private static final HttpStatusClassPredicates SUCCESS_PREDICATE | ||
= HttpStatusClassPredicates.of(HttpStatusClass.SUCCESS); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Should we place =
at the end of the first line?
private static final HttpStatusClassPredicates SUCCESS_PREDICATE | |
= HttpStatusClassPredicates.of(HttpStatusClass.SUCCESS); | |
private static final HttpStatusClassPredicates SUCCESS_PREDICATE = | |
HttpStatusClassPredicates.of(HttpStatusClass.SUCCESS); |
I have addressed your all comments @ikhoon again. |
Hi, @ikhoon! |
I'm sorry, @jrhee17. |
Sorry for the late review. 😅 |
Sorry about the delay. 😅 |
Hi, @jrhee17 ! |
Hi @my4-dev , really sorry about the late comment 😅 I think one of the concerns that was pointed out is that it is difficult to specify multiple mappings. For instance, it is possible that users want to apply a mapping in the following way:
With the current setup, it would be difficult to satisfy this kind of requirement. I would like to propose that we introduce a conditional at the ResponseAs.java <V> ConditionalResponseAs<T, R, V> andThen(ResponseAs<R, V> responseAs, Predicate<R> predicate) which would mean a Then we may chain conditions like the following: WebClient.of().prepare()
.as(ResponseAs.blocking()
.andThen(res -> "Unexpected server error", res -> res.status().isServerError())
.andThen(res -> "missing header", res -> !res.headers().contains("x-header"))
.orElse(AggregatedHttpObject::contentUtf8))
.execute(); We can do something similar for the blocking counterpart, and introduce a <V> BlockingConditionalResponseAs<V> andThenJson(Class<? extends V> clazz, Predicate<AggregatedHttpResponse> predicate) and apply json specific conditionals as well WebClient.of()
.prepare()
.as(ResponseAs.blocking()
.<MyResponse>andThenJson(MyError.class, res -> res.status().isError())
.andThenJson(EmptyMessage.class, res -> res.status().isInformational())
.orElseJson(MyMessage.class))
.execute(); Let me know if this doesn't make sense and sorry about the change in direction 😅 |
@ikhoon also gave the good idea that we may want to accept the predicate first when designing the API
|
Hi @jrhee17 , thank you for your suggestion! It gives me a new perspective.
But I think it would be difficult to specify a lambda function converting On the other hand, I believe that the following points you made should be addressed.
If you have an opinion on the above, I would love to hear it👍 |
To supplement what I had in mind, this is a poc I had worked on when leaving the above review. aed5603 I don't think this is necessarily the "correct" way, nor do I think the commit is refined enough for review, but this is just to give a basic idea of what I was thinking
I'm not sure I understood the intention of adding a predicate to the parameters of |
@jrhee17 : Thank you for sharing your poc! I could understand your idea and it would be better than before. I have some questions and please share your opinion. In your poc, we couldn't apply the mapping by HttpStatus in What do you think the necessity of |
I thought we could do something like the following:
https://github.com/jrhee17/armeria/tree/poc/responseas-if-3
I see. I think the issue is the current PR introduces new APIs in the core APIs (RestClient, BlockingClient), which makes us more cautious (since we don't want to introduce breaking changes). I'm fine with an alternative approach which is either:
Sure, I think the predicates can be used for more shortcut methods. |
This PR was closed and the new PR was suggested blow. |
Motivation:
Please refer to #4382
Modifications:
HttpStatusPredicate
andHttpStatusClassPredicate
class to deserialize non-OK JSON responseResult:
WebClient
andRestClient
#4382