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

Provide a way to automatically deserialize non-OK JSON response #5002

Merged
merged 30 commits into from Apr 11, 2024

Conversation

my4-dev
Copy link
Contributor

@my4-dev my4-dev commented Jul 1, 2023

Motivation:

Please refer to #4382

Modifications:

  • Add some methods to specify what type of response is allowed
  • Add HttpStatusPredicate and HttpStatusClassPredicate class to deserialize non-OK JSON response for more shortcut methods.

Result:

// WebClient
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();

// RestClient
final ResponseEntity<MyResponse> res =
        RestClient.of(server.httpUri()).get("/")
                  .execute(ResponseAs.blocking()
                                     .<MyResponse>andThenJson(MyError.class, res -> res.status().isClientError())
                                     .andThenJson(EmptyMessage.class, res -> res.status().isInformational())
                                     .orElseJson(MyMessage.class));

@my4-dev
Copy link
Contributor Author

my4-dev commented Jul 1, 2023

Thank you @jrhee17 !
I almost quoted from your poc and added some test cases.
I think this would be better than my previous PR.

I haven't completed some JavaDocs yet.
But I want you to review the code at first.
I will fix them after reviewing.

Copy link
Contributor

@jrhee17 jrhee17 left a 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 👍

import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.util.Exceptions;

public class BlockingResponseAs implements ResponseAs<HttpResponse, AggregatedHttpResponse> {
Copy link
Contributor

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

Copy link
Contributor Author

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!

Copy link
Contributor

@jrhee17 jrhee17 left a 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();
Copy link
Contributor

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

Comment on lines 153 to 154
AggregatedResponseAs.json(clazz, predicate),
predicate);
Copy link
Contributor

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

  1. We can't guarantee that the predicate will always return the same result for a given input
  2. The predicate is executed twice, which may not be expected
Suggested change
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

Copy link
Contributor Author

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.

Copy link
Contributor

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> {
Copy link
Contributor

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]Predicates tie into the new APIs since. Can you show me an example?

Copy link
Contributor Author

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));
Copy link
Contributor

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?

Suggested change
return orElse(AggregatedResponseAs.json(clazz, SUCCESS_PREDICATE));
return orElse(AggregatedResponseAs.json(clazz,() -> true));

Copy link
Contributor Author

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.

/**
* Adds the mapping of {@link ResponseAs} and {@link Predicate} in order to return {@link ResponseAs} whose
* {@link Predicate} is evaluated as true.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*/
@UnstableApi

@jrhee17 jrhee17 added this to the 1.25.0 milestone Jul 20, 2023
Copy link
Contributor

@jrhee17 jrhee17 left a 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 +
Copy link
Contributor

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())
Copy link
Contributor

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) {
Copy link
Contributor

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:

  1. Renaming ConditionalResponseAs to DefaultConditionalResponseAs
  2. Modifying DefaultConditionalResponseAs to be package private
  3. Create an interface ConditionalResponseAs and define andThen, orElse methods
  4. Modify DefaultConditionalResponseAs to implement ConditionalResponseAs

Copy link
Contributor Author

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?

Copy link
Contributor

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.

@my4-dev
Copy link
Contributor Author

my4-dev commented Jul 21, 2023

@jrhee17 : Thank you for reviewing! I've applied your comments.

Copy link
Contributor

@jrhee17 jrhee17 left a 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 👍 🙇 🚀

@jrhee17 jrhee17 modified the milestones: 1.25.0, 1.26.0 Aug 16, 2023
@codecov
Copy link

codecov bot commented Aug 16, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 74.12%. Comparing base (b8eb810) to head (6f1cf3a).
Report is 136 commits behind head on main.

❗ Current head 6f1cf3a differs from pull request most recent head 146ba11. Consider uploading reports for the commit 146ba11 to get more accurate results

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.
📢 Have feedback on the report? Share it here.

@minwoox minwoox modified the milestones: 1.26.0, 1.27.0 Oct 13, 2023
* 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(
Copy link
Member

@minwoox minwoox Oct 13, 2023

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.

Copy link
Contributor Author

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.

Copy link
Member

@minwoox minwoox Oct 17, 2023

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.
  • Rename andThenJson to orElseJson
  • 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. 😉

@minwoox minwoox modified the milestones: 1.27.0, 1.26.0 Oct 19, 2023
@ikhoon ikhoon modified the milestones: 1.27.0, 1.28.0 Jan 16, 2024
Copy link
Contributor

@jrhee17 jrhee17 left a 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 👍 👍 👍

Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks useful. Thanks, @my4-dev and @jrhee17!

@jrhee17 jrhee17 merged commit b4e1f58 into line:main Apr 11, 2024
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide a way to automatically deserialize non-OK JSON response using WebClient and RestClient
4 participants