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 #5002
Conversation
Thank you @jrhee17 ! I haven't completed some JavaDocs yet. |
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.
Left some minor comments, but the overall direction looks good to me 👍
core/src/main/java/com/linecorp/armeria/client/AggregatedResponseAs.java
Show resolved
Hide resolved
import com.linecorp.armeria.common.HttpResponse; | ||
import com.linecorp.armeria.common.util.Exceptions; | ||
|
||
public class BlockingResponseAs implements ResponseAs<HttpResponse, AggregatedHttpResponse> { |
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.
Sorry about the misdirection, but what do you think of moving the methods here to overload methods in ResponseAs.java
?
static <T> BlockingConditionalResponseAs<T> json(Class<? extends T> clazz,
Predicate<AggregatedHttpResponse> predicate) {
requireNonNull(clazz, "clazz");
return new BlockingConditionalResponseAs<>(ResponseAs.blocking(),
AggregatedResponseAs.json(clazz), predicate);
}
And then users can just do this
WebClient.of(server.httpUri()).prepare().get("/json_generic_server_error")
.as(ResponseAs.json(new TypeReference<List<MyObject>>() {},
res -> res.status().isServerError())
By doing so, I don't think users have to type out blocking()
and just chain conditions immediately
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 think it's good because users can do the same thing more simply. Thanks!
…er for users to chain conditions more simply
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.
Looks good so far! Left some minor comments 🙇
return true; | ||
} | ||
}; | ||
static final BlockingResponseAs BLOCKING = new BlockingResponseAs(); |
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 think we can just remove BlockingResponseAs
and revert this change now
AggregatedResponseAs.json(clazz, predicate), | ||
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.
I think it might be better to just use a predicate that always returns true in this case.
I think
- We can't guarantee that the predicate will always return the same result for a given input
- The predicate is executed twice, which may not be expected
AggregatedResponseAs.json(clazz, predicate), | |
predicate); | |
AggregatedResponseAs.json(clazz, unused -> true), | |
predicate); |
I also think the unused -> true
predicate can be a static singleton.
Ditto for the other variants
} | ||
|
||
@UnstableApi | ||
static <V> BlockingConditionalResponseAs<V> andThenJson( |
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.
static <V> BlockingConditionalResponseAs<V> andThenJson( | |
static <V> BlockingConditionalResponseAs<V> json( |
I was rather thinking we could name this json
to minimize confusion:
ResponseAs.json(Object.class, res -> true)
.andThenJson(Object.class, res -> true)
.orElseJson(Object.class);
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.
Users can use json
as just same as before.
On the other hand, they also can use andThenJson
in case they want to apply a mapping by several conditions like this.
ResponseAs.<MyResponse>andThenJson(MyMessage.class, res -> res.status().isServerError())
.andThenJson(MyMessage.class, res -> res.status().isClientError())
.orElseJson(MyMessage.class)
In current implementation, I couldn't intend to chain andThenJson
after json
.
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.
Oh, I just meant that I think ResponseAs#andThenJson
can be renamed to ResponseAs#json
(this would mean ResponseAs#json
would be overloaded depending on the predicate parameter).
The reasoning was that uses can type ResponseAs.json(
and auto-complete suggests the API, which results in more exposure. Also, users don't have to remember ResponseAs.andThenJson()
and can just type .json()
.
This is a minor suggestion though 😄
import com.linecorp.armeria.common.HttpStatus; | ||
import com.linecorp.armeria.common.HttpStatusClass; | ||
|
||
final class HttpStatusClassPredicates implements Predicate<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.
I wasn't sure how this predicate plays into the current APIs as nothing is public 😅
I'm also curious how the HttpStatus[Class]Predicate
s tie into the new APIs since. Can you show me an example?
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 think the same too.
I'm not sure how to use HttpStatus[Class]Predicate
in the new APIs.
It would be unnecessary so that I would remove this class.
* {@link Predicate} is evaluated as true. | ||
*/ | ||
public ResponseAs<HttpResponse, ResponseEntity<V>> orElseJson(Class<? extends V> clazz) { | ||
return orElse(AggregatedResponseAs.json(clazz, SUCCESS_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.
Since we're already in a conditional, I think it's fine to just try and parse without checking if the response is successful or not. What do you think of just inputting an always true
predicate?
return orElse(AggregatedResponseAs.json(clazz, SUCCESS_PREDICATE)); | |
return orElse(AggregatedResponseAs.json(clazz,() -> true)); |
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.
It makes sense more! Thanks!
Users might use orElse
to map json response which isn't Success.
core/src/main/java/com/linecorp/armeria/client/BlockingConditionalResponseAs.java
Outdated
Show resolved
Hide resolved
/** | ||
* Adds the mapping of {@link ResponseAs} and {@link Predicate} in order to return {@link ResponseAs} whose | ||
* {@link Predicate} is evaluated as true. | ||
*/ |
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.
*/ | |
@UnstableApi |
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 think this will be the last of my comments 👍 Can you check the lint failures as well?
return new InvalidHttpResponseException( | ||
response, "status: " + response.status() + | ||
" (expect: the success class (2xx). response: " + response, null); | ||
" is not expected by predicate method. response: " + response + |
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; I think it's fine to revert this method since:
- The only way this exception is thrown if due to the status predicate (users can't set the predicate using our public API)
- I think the predicate method will likely not be
toString
friendly, which doesn't provide much information.
final ResponseEntity<List<MyObject>> ServerErrorResponseEntity = | ||
WebClient.of(server.httpUri()).prepare().get("/json_generic_mapper_server_error") | ||
.as(ResponseAs.json(new TypeReference<List<MyObject>>() {}, mapper, | ||
res -> res.status().isServerError()) |
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; do you mind re-indenting this file once?
@@ -182,4 +222,8 @@ public boolean requiresAggregation() { | |||
} | |||
}; | |||
} | |||
|
|||
default <V> ConditionalResponseAs<T, R, V> andThen(ResponseAs<R, V> responseAs, Predicate<R> 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.
Taking a second look, I think it's better if we don't expose the concrete class ConditionalResponseAs
and expose an interface instead.
What do you think of:
- Renaming
ConditionalResponseAs
toDefaultConditionalResponseAs
- Modifying
DefaultConditionalResponseAs
to be package private - Create an interface
ConditionalResponseAs
and defineandThen
,orElse
methods - Modify
DefaultConditionalResponseAs
to implementConditionalResponseAs
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.
Just curious.
In what situations do we consider renaming class to Default...
and make an interface?
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.
The motivation of my comment was to avoid making breaking changes if we wanted to change the implementation hierarchy.
@jrhee17 : Thank you for reviewing! I've applied your comments. |
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.
Thanks! Pushed a minor commit on documentation but looks good to me 👍 🙇 🚀
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #5002 +/- ##
============================================
+ Coverage 73.95% 74.12% +0.17%
- Complexity 20115 21030 +915
============================================
Files 1730 1820 +90
Lines 74161 77439 +3278
Branches 9465 9891 +426
============================================
+ Hits 54847 57404 +2557
- Misses 14837 15377 +540
- Partials 4477 4658 +181 ☔ View full report in Codecov by Sentry. |
* Adds a mapping such that the response content will be deserialized | ||
* to the specified {@link Class} if the {@link Predicate} is satisfied. | ||
*/ | ||
public BlockingConditionalResponseAs<V> andThenJson( |
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 think we should rename this to orElseJson
. andThen
is used when both instances are used. However, if predicate
returns false
, then it isn't converted.
Let me send a patch to rename these methods and address comments.
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.
Thanks.
Please send me your suggestion for these method's name.
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.
Hi, @my4-dev! I made a few changes:
- Do not block the inside of the converter
- I realized that this chaining converts also can be used by the
WebClient
. We must not block the inside of the converters.
- I realized that this chaining converts also can be used by the
- Rename
andThenJson
toorElseJson
andThen
is used when both instances are applied. e.g.armeria/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogWriter.java
Lines 79 to 83 in ed16bf6
try { AccessLogWriter.this.log(log); } finally { after.log(log); } - We apply the converter only when the
Predicate
passes and if it passes, the latter one is not applied. So we should rename it toorElse
instead.
- Remove
ConditionalResponseAs
interface because we don't need it at the moment.
PTAL and let me know if there are changes that I need to revert. 😉
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.
Changes still look good 👍 👍 👍
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.
Motivation:
Please refer to #4382
Modifications:
HttpStatusPredicate
andHttpStatusClassPredicate
class to deserialize non-OK JSON response for more shortcut methods.Result:
WebClient
andRestClient
#4382