Add MultiDecoder to select a decoder per response - #3528
Conversation
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
… predicate Signed-off-by: Marvin Froeder <velo.br@gmail.com>
Signed-off-by: Marvin Froeder <velo.br@gmail.com>
|
@velo The description above is still referencing first argument as the default, so I'm assuming this is still a work in progress? Let me know when this is solid enough that you would like a code review. |
|
@trumpetinc — that was a stale description, sorry: I rewrote it after the direction your review took #3527 but never pushed the edit here. Fixed now. The code that merged has no default slot: It did merge on the 20th, but the whole surface is |
|
@trumpetinc — your #3527 review is applied to both sides in #3534, ready for you. Taken: Pushed back on two, with reasoning in the PR description: Also sent you a repo invite so I can request you as a reviewer properly instead of @-mentioning you. |
The decoder counterpart of #3527. A single client sometimes has to read more than one format — JSON
for most endpoints, XML for a legacy one, plain text for a health check — and today that means one
Feign instance per format.
MultiDecoderhands each response to the first decoder that accepts it. Decoders come from twoplaces:
PredicatedDecoderdeclares its own applicability and can simply belisted;
DecoderPredicate, viaPredicatedDecoder.of(...).Decoders are consulted in registration order and that is the whole rule — there is no default slot
and no implicit fallback. A default is just a decoder with a yes-man predicate, registered last and
visibly so. This mirrors where #3527 ended up after @trumpetinc's review there.
What is in here
MultiDecoder,PredicatedDecoderandDecoderPredicateinfeign.codec, all@Experimental.BaseBuilder.decoders(PredicatedDecoder...), plural, as the shorthand.decoder(Decoder)isuntouched and not deprecated.
canDecodehas nodefault— an existing decoder cannot claim everything by slappingimplements PredicatedDecoderon its declaration.DecoderPredicateis the functionalinterface;
PredicatedDecoder.of(predicate, decoder)attaches a lambda to a decoder you do notown, and
PredicatedDecoder.narrowing(predicate, decoder)ANDs onto what the decoder alreadydeclares.
DecodeExceptionnames the status, thecontent type, the expected type and every decoder that was consulted, in order.
DecoderPredicate.any(),jsonContentType(),xmlContentType(),contentType(...),emptyBody(),status(...),returnType(...), composed withand/or/negate, plusdescribedAs("it is Tuesday", lambda)to name your own. Every predicate carries a description sothe failure message can read back what it considered.
Util.isJsonContentType(Response),Util.isXmlContentType(Response)andUtil.hasContentType(Response, String), so a decoder can declare itself without hand-rollingcontent-type parsing. Suffixed types such as
application/vnd.github+jsonandapplication/soap+xmlmatch.Fastjson2, JSON-java) and XML decoders (JAXB, JAXB Jakarta, SAX, SOAP, SOAP Jakarta) now declare
themselves.
OptionalDecoderand the metrics modules'MeteredDecoderforwardcanDecodeto the decoderthey wrap, so wrapping does not erase a delegate's self-declaration.
OptionalDecoderunwrapsOptional<T>before forwarding, matching what it does ondecode.Notes
consuming it in
canDecodewould leave nothing for the decoder that is eventually chosen.Predicates get the status, the headers and the expected return type; that is documented on both
DecoderPredicateandPredicatedDecoder, and covered by a test that asserts the selecteddecoder still receives an unread body.
Decoderitself is unchanged, so every existing decoder keeps working — one that does notimplement
PredicatedDecoderdeclares nothing, which is why it belongs behind an explicitpredicate rather than being listed bare.
Capabilitysees theMultiDecoderas a single decoder, not one per delegate.mvn clean install -Pdevpasses.