Apply review feedback to the multi-encoder and multi-decoder surface - #3534
Conversation
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
trumpetinc
left a comment
There was a problem hiding this comment.
@velo review is done. The functional code looks solid to me, most comments are small things about the documentation, one question about StringDecoder.
Super excited to see this getting added to Feign.
| import java.lang.reflect.Type; | ||
|
|
||
| public class DefaultDecoder extends StringDecoder { | ||
| public class DefaultDecoder extends StringDecoder implements PredicatedDecoder { |
There was a problem hiding this comment.
For completeness, should we make StringDecoder a PredicatedDecoder?
(PS - it is unfortunate that DefaultDecoder extends StringDecoder... inheritance vs composition mistake)
| || response.status() == 204 | ||
| || response.body() == null | ||
| || byte[].class.equals(type) | ||
| || String.class.equals(type); |
There was a problem hiding this comment.
If we make StringDecoder a PredictedDecoder, this should check super.canDecode() instead of hard coding the String.class.equals(type) check.
| * single unit: | ||
| * | ||
| * <pre> | ||
| * Feign.builder().decoders(StreamingFeign.decoders(), new JacksonDecoder()); |
There was a problem hiding this comment.
I want to call out that StreamingFeign.decoders() doesn't currently exist (streaming requires breaking changes, so is not part of head at this time).
It may be advisable to say "Given a hypothetical library called StreamingFeign that has a decoders() method that returns a MultiDecoder of it's various decoders, this is how the library's decoders would be added:"
Or maybe cleaner to just not talk about this at all until we have a proper example?
| for (PredicatedDecoder decoder : decoders) { | ||
| if (decoder instanceof MultiDecoder) { | ||
| message.append(indent).append("- MultiDecoder:"); | ||
| ((MultiDecoder) decoder).appendTo(message, indent + " "); |
There was a problem hiding this comment.
Good call on the indentation - that will make it very clean to diagnose issues.
|
|
||
| /** Builds the multi-decoder. */ | ||
| /** | ||
| * Adds a decoder that already declares itself, narrowed by the given predicate: both it and the |
There was a problem hiding this comment.
"that already declares itself" is a bit confusing. I think this could be replaced with:
"Adds a Decoder, narrowed by the given predicate. If the Decoder is itself a PredicatedDecoder, the provided predicate is applied in addition to the PredicatedDecoder's existing predicate."
| * .narrow(DecoderPredicate.status(200), new JacksonDecoder()) | ||
| * .add(new JacksonDecoder()) |
There was a problem hiding this comment.
Example is a little confusing. Should we specify a different decoder type for status=200 ? Maybe change the .add() line to new DefaultDecoder() ?
| * single unit: | ||
| * | ||
| * <pre> | ||
| * Feign.builder().encoders(StreamingFeign.encoders(), new JacksonEncoder()); |
There was a problem hiding this comment.
same comment as above about StreamingFeign.encoders() not existing.
| * <pre> | ||
| * MultiEncoder.builder() | ||
| * .narrow(EncoderPredicate.contentType("application/vnd.acme+json"), new GsonEncoder()) | ||
| * .add(new GsonEncoder()) |
There was a problem hiding this comment.
similar to comment above in MultiDecoder.narrow() - shold the .add() line call out a different encoder type so the example makes sense?
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
| * MultiEncoder.builder() | ||
| * .narrow(EncoderPredicate.contentType("application/vnd.acme+json"), new GsonEncoder()) | ||
| * .add(new GsonEncoder()) | ||
| * .add(EncoderPredicate.any(), new DefaultEncoder()) |
There was a problem hiding this comment.
For what it's worth, I don't like using EncoderPredicate.any() with DefaultEncoder. The reality is that .any() isn't really a good predicate for any Encoder or Decoder... I suggest:
.add(new DefaultEncoder())
@trumpetinc's review of #3527, applied to both sides of the codec surface. Everything here is still
@Experimental, so this is the moment to move it.Taken
A multi-codec is itself predicated.
MultiEncoderimplementsPredicatedEncoderandMultiDecoderimplementsPredicatedDecoder, accepting whatever any of their delegates accepts,so one nests inside another. That is the composition you asked for — a library can ship a set as a
single unit:
The winning delegate's
canEncodeis evaluated twice, once by the outeranyMatchand once by theinner loop. Predicates are already documented as cheap and free of side effects, so that is
acceptable, but it is worth saying out loud.
Nested sets are unfolded in the failure message, so contributing a bundle does not turn it into one
opaque line:
DefaultEncoderandDefaultDecoderdeclare themselves. You were right and I was wrong: thepredicate is not a yes-man, it is exactly what
encode/decodehandles. This makes rawpassthrough composable, which is genuinely useful — a
Stringbody reaching Jackson comes outquoted, so putting the default first fixes that:
One correction to the predicate you proposed:
DefaultEncoder.encodealso accepts a null body,which it sends as no body at all. Dropping that case would break
so the predicate is
bodyType == String.class || bodyType == byte[].class || object == null.DefaultDecodergets the same treatment, including 404/204 and a null body, which it handles forany type.
An empty builder fails at wiring time,
IllegalStateExceptionfrombuild(), rather thanproducing a codec that can only ever throw on the first request.
encoders()/decoders()with noarguments fail the same way.
Builder.narrow(predicate, codec), so the replace/AND pair sits side by side where thedifference is visible, and the javadoc on
ofandnarrowingnow says which is which in one line.More on that below.
The
formRequests()javadoc wording, and a note on theFormEncoder(Encoder)constructor thatchaining belongs in
encoders(...)and the delegate is expected to be deprecated once this surfacestops being experimental.
Pushed back on
narrowingis not redundant withadd(predicate, encoder).PredicatedEncoder.ofreplaceswhatever the encoder declares — deliberately, so you can point a
GsonEncoderat a vendor contenttype it would otherwise refuse.
narrowingis the AND. Both are needed. That said, the fact that itread as redundant is the finding: the names were not carrying the difference. Hence the builder
narrowand the sharper javadoc rather than a rename, since the statics are already published.The failure message reports
Content-TypeandAccept, not every header.EncodeExceptionmessages land in logs and the header map routinely holds
Authorization. The detail is not lost —every predicate carries a description and is printed alongside the codec it guards, so a predicate
keyed on an unusual header says so itself:
The hint wording is softened too, since
.any()is an implementation detail for anyone who came inthrough
encoders(...).On the decode side the second header is the request's
Accept, which is what the caller askedfor.
Not done
DefaultEncoderas aMultiEncoder— you flagged it as an observation rather than a proposal,and I agree it is one. It trades a ten-line class for a list allocation and makes the failure
message worse.
Centralizing content-type parsing on the parser in #3494 — agreed in principle, but that PR is
still open. The
Utilhelpers are@Experimental, so whichever lands second swaps the internals;no need to couple them now.
mvn clean installpasses.