Repository: kptfh/feign-reactive Branch: master Commit: ae31dddc5fef Files: 154 Total size: 370.5 KB Directory structure: gitextract_wskod754/ ├── .gitignore ├── LICENSE ├── README.md ├── feign-reactor-cloud/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── reactivefeign/ │ │ └── cloud/ │ │ ├── CloudReactiveFeign.java │ │ ├── methodhandler/ │ │ │ ├── HystrixMethodHandler.java │ │ │ └── HystrixMethodHandlerFactory.java │ │ └── publisher/ │ │ └── RibbonPublisherClient.java │ └── test/ │ └── java/ │ └── reactivefeign/ │ └── cloud/ │ ├── HystrixReactiveHttpClientTest.java │ └── LoadBalancingReactiveHttpClientTest.java ├── feign-reactor-core/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── reactivefeign/ │ │ ├── ReactiveContract.java │ │ ├── ReactiveFeign.java │ │ ├── ReactiveInvocationHandler.java │ │ ├── ReactiveOptions.java │ │ ├── ReactiveRetryPolicy.java │ │ ├── ReactiveRetryers.java │ │ ├── client/ │ │ │ ├── DelegatingReactiveHttpResponse.java │ │ │ ├── InterceptorReactiveHttpClient.java │ │ │ ├── LoggerReactiveHttpClient.java │ │ │ ├── ReactiveHttpClient.java │ │ │ ├── ReactiveHttpRequest.java │ │ │ ├── ReactiveHttpRequestInterceptor.java │ │ │ ├── ReactiveHttpResponse.java │ │ │ ├── ReadTimeoutException.java │ │ │ ├── ResponseMappers.java │ │ │ ├── StatusHandlerReactiveHttpClient.java │ │ │ └── statushandler/ │ │ │ ├── CompositeStatusHandler.java │ │ │ ├── ReactiveStatusHandler.java │ │ │ └── ReactiveStatusHandlers.java │ │ ├── methodhandler/ │ │ │ ├── DefaultMethodHandler.java │ │ │ ├── FluxMethodHandler.java │ │ │ ├── MethodHandler.java │ │ │ ├── MethodHandlerFactory.java │ │ │ ├── MonoMethodHandler.java │ │ │ ├── PublisherClientMethodHandler.java │ │ │ └── ReactiveMethodHandlerFactory.java │ │ ├── publisher/ │ │ │ ├── FluxPublisherHttpClient.java │ │ │ ├── FluxRetryPublisherHttpClient.java │ │ │ ├── MonoPublisherHttpClient.java │ │ │ ├── MonoRetryPublisherHttpClient.java │ │ │ ├── PublisherClientFactory.java │ │ │ ├── PublisherHttpClient.java │ │ │ └── RetryPublisherHttpClient.java │ │ └── utils/ │ │ ├── FeignUtils.java │ │ ├── HttpUtils.java │ │ ├── MultiValueMapUtils.java │ │ └── Pair.java │ └── test/ │ └── java/ │ └── reactivefeign/ │ ├── CompressionTest.java │ ├── ConnectionTimeoutTest.java │ ├── ContractTest.java │ ├── DefaultMethodTest.java │ ├── LoggerTest.java │ ├── NotFoundTest.java │ ├── ReactivityTest.java │ ├── ReadTimeoutTest.java │ ├── RequestInterceptorTest.java │ ├── RetryingTest.java │ ├── SmokeTest.java │ ├── StatusHandlerTest.java │ ├── TestUtils.java │ ├── allfeatures/ │ │ ├── AllFeaturesApi.java │ │ ├── AllFeaturesController.java │ │ └── AllFeaturesTest.java │ ├── resttemplate/ │ │ ├── CompressionTest.java │ │ ├── ConnectionTimeoutTest.java │ │ ├── ContractTest.java │ │ ├── DefaultMethodTest.java │ │ ├── LoggerTest.java │ │ ├── NotFoundTest.java │ │ ├── ReactivityTest.java │ │ ├── ReadTimeoutTest.java │ │ ├── RequestInterceptorTest.java │ │ ├── RetryingTest.java │ │ ├── SmokeTest.java │ │ ├── StatusHandlerTest.java │ │ └── client/ │ │ ├── RestTemplateFakeReactiveFeign.java │ │ └── RestTemplateFakeReactiveHttpClient.java │ └── testcase/ │ ├── IcecreamServiceApi.java │ ├── IcecreamServiceApiBroken.java │ ├── IcecreamServiceApiBrokenByCopy.java │ └── domain/ │ ├── Bill.java │ ├── Flavor.java │ ├── IceCreamOrder.java │ ├── Mixin.java │ └── OrderGenerator.java ├── feign-reactor-jetty/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── reactivefeign/ │ │ └── jetty/ │ │ ├── JettyReactiveFeign.java │ │ ├── client/ │ │ │ ├── JettyReactiveHttpClient.java │ │ │ └── JettyReactiveHttpResponse.java │ │ └── utils/ │ │ └── ProxyPostProcessor.java │ └── test/ │ ├── java/ │ │ └── reactivefeign/ │ │ └── jetty/ │ │ ├── CompressionTest.java │ │ ├── ConnectionTimeoutTest.java │ │ ├── ContractTest.java │ │ ├── DefaultMethodTest.java │ │ ├── LoggerTest.java │ │ ├── NotFoundTest.java │ │ ├── ReactivityTest.java │ │ ├── ReadTimeoutTest.java │ │ ├── RequestInterceptorTest.java │ │ ├── RetryingTest.java │ │ ├── SmokeTest.java │ │ ├── StatusHandlerTest.java │ │ └── allfeatures/ │ │ └── AllFeaturesTest.java │ └── resources/ │ └── log4j2.xml ├── feign-reactor-rx2/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── reactivefeign/ │ │ └── rx2/ │ │ ├── Rx2Contract.java │ │ ├── Rx2ReactiveFeign.java │ │ ├── client/ │ │ │ └── statushandler/ │ │ │ ├── Rx2ReactiveStatusHandler.java │ │ │ ├── Rx2StatusHandler.java │ │ │ └── Rx2StatusHandlers.java │ │ └── methodhandler/ │ │ ├── Rx2MethodHandler.java │ │ ├── Rx2MethodHandlerFactory.java │ │ └── Rx2PublisherClientMethodHandler.java │ └── test/ │ └── java/ │ └── reactivefeign/ │ └── rx2/ │ ├── ContractTest.java │ ├── DefaultMethodTest.java │ ├── LoggerTest.java │ ├── NotFoundTest.java │ ├── ReactivityTest.java │ ├── ReadTimeoutTest.java │ ├── RequestInterceptorTest.java │ ├── SmokeTest.java │ ├── StatusHandlerTest.java │ ├── TestUtils.java │ └── testcase/ │ ├── IcecreamServiceApi.java │ ├── IcecreamServiceApiBroken.java │ └── domain/ │ ├── Bill.java │ ├── Flavor.java │ ├── IceCreamOrder.java │ ├── Mixin.java │ └── OrderGenerator.java ├── feign-reactor-webclient/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── reactivefeign/ │ │ └── webclient/ │ │ ├── WebReactiveFeign.java │ │ └── client/ │ │ ├── WebReactiveHttpClient.java │ │ └── WebReactiveHttpResponse.java │ └── test/ │ ├── java/ │ │ └── reactivefeign/ │ │ └── webclient/ │ │ ├── CompressionTest.java │ │ ├── ConnectionTimeoutTest.java │ │ ├── ContractTest.java │ │ ├── DefaultMethodTest.java │ │ ├── LoggerTest.java │ │ ├── NotFoundTest.java │ │ ├── ReactivityTest.java │ │ ├── ReadTimeoutTest.java │ │ ├── RequestInterceptorTest.java │ │ ├── RetryingTest.java │ │ ├── SmokeTest.java │ │ ├── StatusHandlerTest.java │ │ └── allfeatures/ │ │ ├── AllFeaturesTest.java │ │ ├── WebClientFeaturesApi.java │ │ ├── WebClientFeaturesController.java │ │ └── WebClientFeaturesTest.java │ └── resources/ │ └── log4j2.xml ├── pom.xml └── settings.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ target/ !.mvn/wrapper/maven-wrapper.jar ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ build/ nbbuild/ dist/ nbdist/ .nb-gradle/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ Happy to announce that from now Java Reactive Feign client is officially backed by Playtika. All development will be conducted in Playtika fork https://github.com/Playtika/feign-reactive [Subscribe to stay up to date 🙂](https://www.youtube.com/channel/UCAIRpdkVAj1RT6butHUV9yg) # feign-reactive [ ![Download](https://api.bintray.com/packages/kptfh/feign-reactive/client/images/download.svg) ](https://bintray.com/kptfh/feign-reactive/client/_latestVersion) Use Feign with Spring WebFlux ## Overview Implementation of Feign on Spring WebClient. Brings you the best of two worlds together : concise syntax of Feign to write client side API on fast, asynchronous and non-blocking HTTP client of Spring WebClient. ## Modules **_feign-reactor-core_** : base classes and interfaces that should allow to implement alternative reactor Feign **_feign-reactor-webclient_** : Spring WebClient based implementation of reactor Feign **_feign-reactor-cloud_** : Spring Cloud implementation of reactor Feign (Ribbon/Hystrix) **_feign-reactor-rx2_** : Rx2 compatible implementation of reactor Feign (depends on feign-reactor-webclient) **_feign-reactor-jetty_** : experimental Reactive Jetty client based implementation of reactor Feign (doesn't depend on feign-reactor-webclient). In future will allow to write pure Rx2 version. - have greater reactivity level then Spring WebClient. By default don't collect body to list instead starts sending request body as stream. - starts receiving reactive response before all reactive request body has been sent - process Flux<`String`> correctly in request and response body ## Usage Write Feign API as usual, but every method of interface - may accept `org.reactivestreams.Publisher` as body - must return `reactor.core.publisher.Mono` or `reactor.core.publisher.Flux`. ```java @Headers({ "Accept: application/json" }) public interface IcecreamServiceApi { @RequestLine("GET /icecream/flavors") Flux getAvailableFlavors(); @RequestLine("GET /icecream/mixins") Flux getAvailableMixins(); @RequestLine("POST /icecream/orders") @Headers("Content-Type: application/json") Mono makeOrder(IceCreamOrder order); @RequestLine("GET /icecream/orders/{orderId}") Mono findOrder(@Param("orderId") int orderId); @RequestLine("POST /icecream/bills/pay") @Headers("Content-Type: application/json") Mono payBill(Publisher bill); } ``` Build the client : ```java /* Create instance of your API */ IcecreamServiceApi client = ReactiveFeign .builder() .target(IcecreamServiceApi.class, "http://www.icecreame.com") /* Execute nonblocking requests */ Flux flavors = icecreamApi.getAvailableFlavors(); Flux mixins = icecreamApi.getAvailableMixins(); ``` or cloud aware client : ```java IcecreamServiceApi client = CloudReactiveFeign.builder() .setFallback(new TestInterface() { @Override public Mono get() { return Mono.just("fallback"); } }) .setLoadBalancerCommand( LoadBalancerCommand.builder() .withLoadBalancer(AbstractLoadBalancer.class.cast(getNamedLoadBalancer(serviceName))) .withRetryHandler(new DefaultLoadBalancerRetryHandler(1, 1, true)) .build() ) .target(IcecreamServiceApi.class, "http://" + serviceName); /* Execute nonblocking requests */ Flux flavors = icecreamApi.getAvailableFlavors(); Flux mixins = icecreamApi.getAvailableMixins(); ``` ## Rx2 Usage Write Feign API as usual, but every method of interface - may accept `Flowable`, `Observable`, `Single` or `Maybe` as body - must return `Flowable`, `Observable`, `Single` or `Maybe`. ```java @Headers({"Accept: application/json"}) public interface IcecreamServiceApi { @RequestLine("GET /icecream/flavors") Flowable getAvailableFlavors(); @RequestLine("GET /icecream/mixins") Observable getAvailableMixins(); @RequestLine("POST /icecream/orders") @Headers("Content-Type: application/json") Single makeOrder(IceCreamOrder order); @RequestLine("GET /icecream/orders/{orderId}") Maybe findOrder(@Param("orderId") int orderId); @RequestLine("POST /icecream/bills/pay") @Headers("Content-Type: application/json") Single payBill(Bill bill); ``` Build the client : ```java /* Create instance of your API */ IcecreamServiceApi client = Rx2ReactiveFeign .builder() .target(IcecreamServiceApi.class, "http://www.icecreame.com") /* Execute nonblocking requests */ Flowable flavors = icecreamApi.getAvailableFlavors(); Observable mixins = icecreamApi.getAvailableMixins(); ``` ## Maven ```xml bintray-kptfh-feign-reactive bintray https://dl.bintray.com/kptfh/feign-reactive ... ... io.github.reactivefeign feign-reactor-cloud 1.0.0 or if you don't need cloud specific functionality io.github.reactivefeign feign-reactor-webclient 1.0.0 or if you tend to use Rx2 interfaces io.github.reactivefeign feign-reactor-rx2 1.0.0 ... ``` ## License Library distributed under Apache License Version 2.0. ================================================ FILE: feign-reactor-cloud/pom.xml ================================================ 4.0.0 io.github.reactivefeign feign-reactor 1.0.0-SNAPSHOT feign-reactor-cloud io.github.reactivefeign feign-reactor-webclient com.netflix.hystrix hystrix-core com.netflix.archaius archaius-core com.netflix.ribbon ribbon-core com.netflix.ribbon ribbon-loadbalancer io.reactivex rxjava io.reactivex rxjava-reactive-streams io.github.openfeign feign-core org.slf4j slf4j-api org.springframework.boot spring-boot-starter-test test io.projectreactor reactor-test test junit junit test org.assertj assertj-core test com.github.tomakehurst wiremock test com.google.guava guava test ================================================ FILE: feign-reactor-cloud/src/main/java/reactivefeign/cloud/CloudReactiveFeign.java ================================================ package reactivefeign.cloud; import com.netflix.client.ClientFactory; import com.netflix.client.RetryHandler; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixObservableCommand; import com.netflix.loadbalancer.reactive.LoadBalancerCommand; import feign.Contract; import feign.InvocationHandlerFactory; import feign.MethodMetadata; import feign.Target; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.reactive.function.client.WebClient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.ReactiveRetryPolicy; import reactivefeign.client.ReactiveHttpRequestInterceptor; import reactivefeign.client.ReactiveHttpResponse; import reactivefeign.client.statushandler.ReactiveStatusHandler; import reactivefeign.cloud.methodhandler.HystrixMethodHandlerFactory; import reactivefeign.cloud.publisher.RibbonPublisherClient; import reactivefeign.methodhandler.MethodHandlerFactory; import reactivefeign.publisher.PublisherClientFactory; import reactivefeign.publisher.PublisherHttpClient; import reactivefeign.webclient.WebReactiveFeign; import java.net.URI; import java.net.URISyntaxException; import java.util.function.BiFunction; import java.util.function.Function; import static reactivefeign.utils.FeignUtils.returnPublisherType; /** * Allows to specify ribbon {@link LoadBalancerCommand} * and HystrixObservableCommand.Setter with fallback factory. * * @author Sergii Karpenko */ public class CloudReactiveFeign extends ReactiveFeign { private static final Logger logger = LoggerFactory.getLogger(CloudReactiveFeign.class); private CloudReactiveFeign(ReactiveFeign.ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { super(targetToHandlersByName, factory); } public static Builder builder() { return new Builder<>(); } public static Builder builder(WebClient webClient) { return new Builder<>(webClient); } public static class Builder extends WebReactiveFeign.Builder { private boolean hystrixEnabled = true; private SetterFactory commandSetterFactory = new DefaultSetterFactory(); private Function fallbackFactory; private Function> loadBalancerCommandFactory = s -> null; protected Builder() { super(); } protected Builder(WebClient webClient) { super(webClient); } public void disableHystrix() { this.hystrixEnabled = false; } public Builder setHystrixCommandSetterFactory(SetterFactory commandSetterFactory) { this.commandSetterFactory = commandSetterFactory; return this; } public Builder setFallback(T fallback) { return setFallbackFactory(throwable -> fallback); } public Builder setFallbackFactory(Function fallbackFactory) { this.fallbackFactory = fallbackFactory; return this; } public Builder enableLoadBalancer(){ return setLoadBalancerCommandFactory(serviceName -> LoadBalancerCommand.builder() .withLoadBalancer(ClientFactory.getNamedLoadBalancer(serviceName)) .build()); } public Builder enableLoadBalancer(RetryHandler retryHandler){ if(retryHandler.getMaxRetriesOnSameServer() > 0){ logger.warn("Use retryWhen(ReactiveRetryPolicy retryPolicy) " + "as it allow to configure retry delays (backoff)"); } return setLoadBalancerCommandFactory(serviceName -> LoadBalancerCommand.builder() .withLoadBalancer(ClientFactory.getNamedLoadBalancer(serviceName)) .withRetryHandler(retryHandler) .build()); } public Builder setLoadBalancerCommandFactory( Function> loadBalancerCommandFactory) { this.loadBalancerCommandFactory = loadBalancerCommandFactory; return this; } @Override protected MethodHandlerFactory buildReactiveMethodHandlerFactory() { MethodHandlerFactory methodHandlerFactory = super.buildReactiveMethodHandlerFactory(); return hystrixEnabled ? new HystrixMethodHandlerFactory( methodHandlerFactory, commandSetterFactory, (Function) fallbackFactory) : methodHandlerFactory; } @Override protected PublisherClientFactory buildReactiveClientFactory() { PublisherClientFactory publisherClientFactory = super.buildReactiveClientFactory(); return methodMetadata -> { PublisherHttpClient publisherClient = publisherClientFactory.apply(methodMetadata); String serviceName = extractServiceName(target.url()); return new RibbonPublisherClient(loadBalancerCommandFactory.apply(serviceName), publisherClient, returnPublisherType(methodMetadata)); }; } private String extractServiceName(String url){ try { return new URI(url).getHost(); } catch (URISyntaxException e) { throw new IllegalArgumentException("Can't extract service name from url", e); } } @Override public Builder contract(final Contract contract) { super.contract(contract); return this; } @Override public Builder requestInterceptor(ReactiveHttpRequestInterceptor requestInterceptor) { super.requestInterceptor(requestInterceptor); return this; } @Override public Builder decode404() { super.decode404(); return this; } @Override public Builder statusHandler(ReactiveStatusHandler statusHandler) { super.statusHandler(statusHandler); return this; } @Override public ReactiveFeign.Builder responseMapper( BiFunction responseMapper) { super.responseMapper(responseMapper); return this; } @Override public Builder retryWhen(ReactiveRetryPolicy retryPolicy){ super.retryWhen(retryPolicy); return this; } @Override public Builder options(final ReactiveOptions options) { super.options(options); return this; } } public interface SetterFactory { HystrixObservableCommand.Setter create(Target target, MethodMetadata methodMetadata); } public static class DefaultSetterFactory implements SetterFactory { @Override public HystrixObservableCommand.Setter create(Target target, MethodMetadata methodMetadata) { String groupKey = target.name(); String commandKey = methodMetadata.configKey(); return HystrixObservableCommand.Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey)); } } } ================================================ FILE: feign-reactor-cloud/src/main/java/reactivefeign/cloud/methodhandler/HystrixMethodHandler.java ================================================ package reactivefeign.cloud.methodhandler; import com.netflix.hystrix.HystrixObservableCommand; import feign.MethodMetadata; import feign.Target; import org.reactivestreams.Publisher; import org.springframework.lang.Nullable; import reactivefeign.cloud.CloudReactiveFeign; import reactivefeign.methodhandler.MethodHandler; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Observable; import rx.RxReactiveStreams; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; import java.util.function.Function; import static feign.Feign.configKey; import static feign.Util.checkNotNull; /** * @author Sergii Karpenko */ public class HystrixMethodHandler implements MethodHandler { private final Method method; private final Type returnPublisherType; private final MethodHandler methodHandler; private final Function fallbackFactory; private final HystrixObservableCommand.Setter hystrixObservableCommandSetter; HystrixMethodHandler( Target target, MethodMetadata methodMetadata, MethodHandler methodHandler, CloudReactiveFeign.SetterFactory setterFactory, @Nullable Function fallbackFactory) { checkNotNull(target, "target must be not null"); checkNotNull(methodMetadata, "methodMetadata must be not null"); method = Arrays.stream(target.type().getMethods()) .filter(method -> configKey(target.type(), method).equals(methodMetadata.configKey())) .findFirst().orElseThrow(() -> new IllegalArgumentException()); method.setAccessible(true); returnPublisherType = ((ParameterizedType) methodMetadata.returnType()).getRawType(); this.methodHandler = checkNotNull(methodHandler, "methodHandler must be not null"); this.fallbackFactory = fallbackFactory; checkNotNull(setterFactory, "setterFactory must be not null"); hystrixObservableCommandSetter = setterFactory.create(target, methodMetadata); } @Override @SuppressWarnings("unchecked") public Publisher invoke(final Object[] argv) { Observable observable = new HystrixObservableCommand(hystrixObservableCommandSetter) { @Override protected Observable construct() { Publisher publisher; try { publisher = (Publisher) methodHandler.invoke(argv); } catch (Throwable throwable) { publisher = Mono.error(throwable); } return RxReactiveStreams.toObservable(publisher); } @Override protected Observable resumeWithFallback() { if (fallbackFactory != null) { Object fallback = fallbackFactory.apply(getExecutionException()); try { Object fallbackValue = getFallbackValue(fallback, method, argv); return RxReactiveStreams.toObservable((Publisher) fallbackValue); } catch (Throwable e) { return Observable.error(e); } } else { return super.resumeWithFallback(); } } }.toObservable(); if(returnPublisherType == Mono.class){ return Mono.from(RxReactiveStreams.toPublisher(observable.toSingle())); } else if(returnPublisherType == Flux.class){ return Flux.from(RxReactiveStreams.toPublisher(observable)); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + returnPublisherType); } } protected Object getFallbackValue(Object target, Method method, Object[] argv) throws Throwable { return method.invoke(target, argv); } } ================================================ FILE: feign-reactor-cloud/src/main/java/reactivefeign/cloud/methodhandler/HystrixMethodHandlerFactory.java ================================================ package reactivefeign.cloud.methodhandler; import feign.MethodMetadata; import feign.Target; import org.springframework.lang.Nullable; import reactivefeign.cloud.CloudReactiveFeign; import reactivefeign.methodhandler.MethodHandler; import reactivefeign.methodhandler.MethodHandlerFactory; import java.lang.reflect.Method; import java.util.function.Function; import static feign.Util.checkNotNull; public class HystrixMethodHandlerFactory implements MethodHandlerFactory { private final MethodHandlerFactory methodHandlerFactory; private final CloudReactiveFeign.SetterFactory commandSetterFactory; private final Function fallbackFactory; public HystrixMethodHandlerFactory(MethodHandlerFactory methodHandlerFactory, CloudReactiveFeign.SetterFactory commandSetterFactory, @Nullable Function fallbackFactory) { this.methodHandlerFactory = checkNotNull(methodHandlerFactory, "methodHandlerFactory must not be null"); this.commandSetterFactory = checkNotNull(commandSetterFactory, "hystrixObservableCommandSetter must not be null"); this.fallbackFactory = fallbackFactory; } @Override public MethodHandler create(final Target target, final MethodMetadata metadata) { return new HystrixMethodHandler( target, metadata, methodHandlerFactory.create(target, metadata), commandSetterFactory, fallbackFactory); } @Override public MethodHandler createDefault(Method method) { return methodHandlerFactory.createDefault(method); } } ================================================ FILE: feign-reactor-cloud/src/main/java/reactivefeign/cloud/publisher/RibbonPublisherClient.java ================================================ package reactivefeign.cloud.publisher; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.reactive.LoadBalancerCommand; import org.reactivestreams.Publisher; import org.springframework.lang.Nullable; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.publisher.PublisherHttpClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Observable; import rx.RxReactiveStreams; import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; /** * @author Sergii Karpenko */ public class RibbonPublisherClient implements PublisherHttpClient { private final LoadBalancerCommand loadBalancerCommand; private final PublisherHttpClient publisherClient; private final Type publisherType; public RibbonPublisherClient(@Nullable LoadBalancerCommand loadBalancerCommand, PublisherHttpClient publisherClient, Type publisherType) { this.loadBalancerCommand = loadBalancerCommand; this.publisherClient = publisherClient; this.publisherType = publisherType; } @Override public Publisher executeRequest(ReactiveHttpRequest request) { if (loadBalancerCommand != null) { Observable observable = loadBalancerCommand.submit(server -> { ReactiveHttpRequest lbRequest = loadBalanceRequest(request, server); Publisher publisher = (Publisher)publisherClient.executeRequest(lbRequest); return RxReactiveStreams.toObservable(publisher); }); Publisher publisher = RxReactiveStreams.toPublisher(observable); if(publisherType == Mono.class){ return Mono.from(publisher); } else if(publisherType == Flux.class){ return Flux.from(publisher); } else { throw new IllegalArgumentException("Unknown publisherType: " + publisherType); } } else { return publisherClient.executeRequest(request); } } protected ReactiveHttpRequest loadBalanceRequest(ReactiveHttpRequest request, Server server) { URI uri = request.uri(); try { URI lbUrl = new URI(uri.getScheme(), uri.getUserInfo(), server.getHost(), server.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); return new ReactiveHttpRequest(request.method(), lbUrl, request.headers(), request.body()); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } } ================================================ FILE: feign-reactor-cloud/src/test/java/reactivefeign/cloud/HystrixReactiveHttpClientTest.java ================================================ package reactivefeign.cloud; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import com.netflix.hystrix.*; import com.netflix.hystrix.exception.HystrixRuntimeException; import feign.MethodMetadata; import feign.Target; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import reactor.core.publisher.Mono; import java.io.IOException; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; /** * @author Sergii Karpenko */ public class HystrixReactiveHttpClientTest { public static final int SLEEP_WINDOW = 100; public static final int VOLUME_THRESHOLD = 1; public static final String FALLBACK = "fallback"; public static final String SUCCESS = "success!"; @ClassRule public static WireMockClassRule server = new WireMockClassRule(wireMockConfig().dynamicPort()); @Rule public ExpectedException expectedException = ExpectedException.none(); private static int testNo = 0; private AtomicReference lastCommandKey = new AtomicReference<>(); @Before public void resetServers() { server.resetAll(); testNo++; } @Test public void shouldFailAsNoFallback() { expectedException.expect(HystrixRuntimeException.class); expectedException.expectMessage(containsString("failed and no fallback available")); String body = "success!"; LoadBalancingReactiveHttpClientTest.mockSuccessAfterSeveralAttempts(server, "/", 1, 598, aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(body)); LoadBalancingReactiveHttpClientTest.TestInterface client = CloudReactiveFeign.builder() .setHystrixCommandSetterFactory(getSetterFactory(testNo)) .target(LoadBalancingReactiveHttpClientTest.TestInterface.class, "http://localhost:" + server.port()); client.get().block(); } @Test public void shouldNotFailDueToFallback() { String body = "success!"; LoadBalancingReactiveHttpClientTest.mockSuccessAfterSeveralAttempts(server, "/", 1, 598, aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(body)); LoadBalancingReactiveHttpClientTest.TestInterface client = CloudReactiveFeign.builder() .setHystrixCommandSetterFactory(getSetterFactory(testNo)) .setFallback(() -> Mono.just(FALLBACK)) .target(LoadBalancingReactiveHttpClientTest.TestInterface.class, "http://localhost:" + server.port()); String result = client.get().block(); assertThat(result).isEqualTo(FALLBACK); } @Test public void shouldOpenCircuitBreakerAndCloseAfterSleepTime() throws InterruptedException { int callsNo = VOLUME_THRESHOLD + 1; LoadBalancingReactiveHttpClientTest.mockSuccessAfterSeveralAttempts(server, "/", VOLUME_THRESHOLD, SC_SERVICE_UNAVAILABLE, aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(SUCCESS)); LoadBalancingReactiveHttpClientTest.TestInterface client = CloudReactiveFeign.builder() .setHystrixCommandSetterFactory(getSetterFactory(testNo)) .target(LoadBalancingReactiveHttpClientTest.TestInterface.class, "http://localhost:" + server.port()); //check that circuit breaker get opened on volume threshold List results = IntStream.range(0, callsNo).mapToObj(i -> { try { return client.get().block(); } catch (Throwable t) { return t; } }).collect(Collectors.toList()); assertThat(server.getAllServeEvents().size()).isLessThan(callsNo); Throwable firstError = (Throwable) results.get(0); assertThat(firstError).isInstanceOf(HystrixRuntimeException.class); assertThat(firstError.getMessage()) .contains("and no fallback available") .doesNotContain("short-circuited"); assertThat(HystrixCircuitBreaker.Factory.getInstance(lastCommandKey.get()) .isOpen()) .isTrue(); Throwable lastError = (Throwable) results.get(results.size() - 1); assertThat(lastError).isInstanceOf(HystrixRuntimeException.class); assertThat(lastError.getMessage()) .contains("short-circuited and no fallback available."); //wait to circuit breaker get closed again Thread.sleep(SLEEP_WINDOW); //check that circuit breaker get closed again List resultsAfterSleep = IntStream.range(0, callsNo).mapToObj(i -> { try { return client.get().block(); } catch (Throwable t) { return t; } }).collect(Collectors.toList()); assertThat(resultsAfterSleep).containsOnly(SUCCESS); assertThat(HystrixCircuitBreaker.Factory.getInstance(lastCommandKey.get()) .isOpen()) .isFalse(); } CloudReactiveFeign.SetterFactory getSetterFactory(int testNo) { return new CloudReactiveFeign.SetterFactory() { @Override public HystrixObservableCommand.Setter create(Target target, MethodMetadata methodMetadata) { String groupKey = target.name(); HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(methodMetadata.configKey() + testNo); lastCommandKey.set(commandKey); return HystrixObservableCommand.Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(commandKey) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withCircuitBreakerRequestVolumeThreshold(VOLUME_THRESHOLD) .withExecutionTimeoutEnabled(false) .withCircuitBreakerSleepWindowInMilliseconds(SLEEP_WINDOW) .withMetricsHealthSnapshotIntervalInMilliseconds(10) ); } }; } } ================================================ FILE: feign-reactor-cloud/src/test/java/reactivefeign/cloud/LoadBalancingReactiveHttpClientTest.java ================================================ package reactivefeign.cloud; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import com.netflix.client.*; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixCommandProperties; import com.netflix.hystrix.HystrixObservableCommand; import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; import feign.RequestLine; import feign.RetryableException; import org.junit.*; import org.junit.rules.ExpectedException; import reactivefeign.publisher.RetryPublisherHttpClient; import reactor.core.publisher.Mono; import java.util.stream.Stream; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.*; import static reactivefeign.ReactiveRetryers.retry; /** * @author Sergii Karpenko */ public class LoadBalancingReactiveHttpClientTest { @ClassRule public static WireMockClassRule server1 = new WireMockClassRule(wireMockConfig().dynamicPort()); @ClassRule public static WireMockClassRule server2 = new WireMockClassRule(wireMockConfig().dynamicPort()); @Rule public ExpectedException expectedException = ExpectedException.none(); private static String serviceName = "LoadBalancingTargetTest-loadBalancingDefaultPolicyRoundRobin"; @BeforeClass public static void setupServersList() throws ClientException { DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); clientConfig.loadDefaultValues(); clientConfig.setProperty(CommonClientConfigKey.NFLoadBalancerClassName, BaseLoadBalancer.class.getName()); ILoadBalancer lb = ClientFactory.registerNamedLoadBalancerFromclientConfig(serviceName, clientConfig); lb.addServers(asList(new Server("localhost", server1.port()), new Server("localhost", server2.port()))); } @Before public void resetServers() { server1.resetAll(); server2.resetAll(); } @Test public void shouldLoadBalanceRequests() { String body = "success!"; mockSuccess(server1, body); mockSuccess(server2, body); TestInterface client = CloudReactiveFeign.builder() .enableLoadBalancer() .setHystrixCommandSetterFactory(getSetterFactoryWithTimeoutDisabled()) .target(TestInterface.class, "http://" + serviceName); String result1 = client.get().block(); String result2 = client.get().block(); assertThat(result1) .isEqualTo(result2) .isEqualTo(body); server1.verify(1, getRequestedFor(urlEqualTo("/"))); server2.verify(1, getRequestedFor(urlEqualTo("/"))); } @Test public void shouldFailAsPolicyWoRetries() { expectedException.expect(RuntimeException.class); expectedException.expectCause(allOf(isA(RetryPublisherHttpClient.OutOfRetriesException.class), hasProperty("cause", isA(RetryableException.class)))); try { loadBalancingWithRetry(2, 0, 0); } catch (Throwable t) { assertThat(server1.getAllServeEvents().size() == 1 ^ server2.getAllServeEvents().size() == 1); throw t; } } @Test public void shouldRetryOnSameAndFail() { expectedException.expect(RuntimeException.class); expectedException.expectCause(allOf(isA(RetryPublisherHttpClient.OutOfRetriesException.class), hasProperty("cause", isA(RetryableException.class)))); try { loadBalancingWithRetry(2, 1, 0); } catch (Throwable t) { assertThat(server1.getAllServeEvents().size() == 2 ^ server2.getAllServeEvents().size() == 2); throw t; } } @Test public void shouldRetryOnNextAndFail() { expectedException.expect(RuntimeException.class); expectedException.expectCause(isA(ClientException.class)); try { loadBalancingWithRetry(2, 1, 1); } catch (Throwable t) { assertThat(server1.getAllServeEvents().size() == 2 && server2.getAllServeEvents().size() == 2); throw t; } } @Test public void shouldRetryOnSameAndSuccess() { loadBalancingWithRetry(2, 2, 0); assertThat(server1.getAllServeEvents().size() == 3 ^ server2.getAllServeEvents().size() == 3); } private void loadBalancingWithRetry(int failedAttemptsNo, int retryOnSame, int retryOnNext) { String body = "success!"; Stream.of(server1, server2).forEach(server -> { mockSuccessAfterSeveralAttempts(server, "/", failedAttemptsNo, 503, aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(body)); }); RetryHandler retryHandler = new RequestSpecificRetryHandler(true, true, new DefaultLoadBalancerRetryHandler(0, retryOnNext, true), null); TestInterface client = CloudReactiveFeign.builder() .retryWhen(retry(retryOnSame)) .enableLoadBalancer(retryHandler) .setHystrixCommandSetterFactory(getSetterFactoryWithTimeoutDisabled()) .target(TestInterface.class, "http://" + serviceName); String result = client.get().block(); assertThat(result).isEqualTo(body); } private CloudReactiveFeign.SetterFactory getSetterFactoryWithTimeoutDisabled() { return (target, methodMetadata) -> { String groupKey = target.name(); HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(methodMetadata.configKey()); return HystrixObservableCommand.Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(commandKey) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withExecutionTimeoutEnabled(false) ); }; } static void mockSuccess(WireMockClassRule server, String body) { server.stubFor(get(urlEqualTo("/")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(body))); } static void mockSuccessAfterSeveralAttempts(WireMockClassRule server, String url, int failedAttemptsNo, int errorCode, ResponseDefinitionBuilder response) { String state = STARTED; for (int attempt = 0; attempt < failedAttemptsNo; attempt++) { String nextState = "attempt" + attempt; server.stubFor(get(urlEqualTo(url)) .inScenario("testScenario") .whenScenarioStateIs(state) .willReturn(aResponse() .withStatus(errorCode) .withHeader("Retry-After", "1")) .willSetStateTo(nextState)); state = nextState; } server.stubFor(get(urlEqualTo(url)) .inScenario("testScenario") .whenScenarioStateIs(state) .willReturn(response)); } interface TestInterface { @RequestLine("GET /") Mono get(); } } ================================================ FILE: feign-reactor-core/pom.xml ================================================ 4.0.0 io.github.reactivefeign feign-reactor 1.0.0-SNAPSHOT feign-reactor-core jar Feign Reactive Core ${project.basedir}/.. io.projectreactor reactor-core io.github.openfeign feign-core org.slf4j slf4j-api commons-httpclient commons-httpclient io.projectreactor reactor-test test junit junit test org.assertj assertj-core test com.github.tomakehurst wiremock test com.fasterxml.jackson.datatype jackson-datatype-jsr310 test org.hamcrest hamcrest-library test org.mockito mockito-all test org.awaitility awaitility test org.apache.logging.log4j log4j-slf4j-impl test org.springframework.boot spring-boot-starter-webflux spring-boot-starter-logging org.springframework.boot test org.springframework.boot spring-boot-starter-test test org.apache.maven.plugins maven-jar-plugin 3.1.0 test-jar ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/ReactiveContract.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import feign.Contract; import feign.MethodMetadata; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import static feign.Util.checkNotNull; import static reactivefeign.utils.FeignUtils.bodyActualType; import static reactivefeign.utils.FeignUtils.returnActualType; /** * Contract allowing only {@link Mono} and {@link Flux} return type. * * @author Sergii Karpenko */ public class ReactiveContract implements Contract { private final Contract delegate; public ReactiveContract(final Contract delegate) { this.delegate = checkNotNull(delegate, "delegate must not be null"); } @Override public List parseAndValidatateMetadata(final Class targetType) { final List methodsMetadata = this.delegate.parseAndValidatateMetadata(targetType); for (final MethodMetadata metadata : methodsMetadata) { final Type type = metadata.returnType(); if (!isReactorType(type)) { throw new IllegalArgumentException(String.format( "Method %s of contract %s doesn't returns reactor.core.publisher.Mono or reactor.core.publisher.Flux", metadata.configKey(), targetType.getSimpleName())); } if(returnActualType(metadata) == byte[].class || bodyActualType(metadata) == byte[].class){ throw new IllegalArgumentException(String.format( "Method %s of contract %s will cause data to be copied, use ByteBuffer instead", metadata.configKey(), targetType.getSimpleName())); } } return methodsMetadata; } private boolean isReactorType(final Type type) { return (type instanceof ParameterizedType) && (((ParameterizedType) type).getRawType() == Mono.class || ((ParameterizedType) type).getRawType() == Flux.class); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/ReactiveFeign.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import feign.*; import feign.codec.ErrorDecoder; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequestInterceptor; import reactivefeign.client.ReactiveHttpResponse; import reactivefeign.client.statushandler.ReactiveStatusHandler; import reactivefeign.client.statushandler.ReactiveStatusHandlers; import reactivefeign.methodhandler.MethodHandler; import reactivefeign.methodhandler.DefaultMethodHandler; import reactivefeign.methodhandler.MethodHandlerFactory; import reactivefeign.methodhandler.ReactiveMethodHandlerFactory; import reactivefeign.publisher.*; import reactivefeign.utils.Pair; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import static feign.Util.checkNotNull; import static feign.Util.isDefault; import static reactivefeign.client.InterceptorReactiveHttpClient.intercept; import static reactivefeign.client.LoggerReactiveHttpClient.log; import static reactivefeign.client.ResponseMappers.ignore404; import static reactivefeign.client.ResponseMappers.mapResponse; import static reactivefeign.client.StatusHandlerReactiveHttpClient.handleStatus; import static reactivefeign.utils.FeignUtils.returnPublisherType; import static reactivefeign.utils.MultiValueMapUtils.addOrdered; /** * Allows Feign interfaces to accept {@link Publisher} as body and return reactive {@link Mono} or * {@link Flux}. * * @author Sergii Karpenko */ public class ReactiveFeign { private final ParseHandlersByName targetToHandlersByName; private final InvocationHandlerFactory factory; protected ReactiveFeign( final ParseHandlersByName targetToHandlersByName, final InvocationHandlerFactory factory) { this.targetToHandlersByName = targetToHandlersByName; this.factory = factory; } @SuppressWarnings("unchecked") public T newInstance(Target target) { final Map nameToHandler = targetToHandlersByName.apply(target); final Map methodToHandler = new LinkedHashMap<>(); final List defaultMethodHandlers = new LinkedList<>(); for (final Method method : target.type().getMethods()) { if (isDefault(method)) { final DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } final InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[] {target.type()}, handler); for (final DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } /** * ReactiveFeign builder. */ public abstract static class Builder { protected Contract contract; protected Function clientFactory; protected ReactiveHttpRequestInterceptor requestInterceptor; protected BiFunction responseMapper; protected ReactiveStatusHandler statusHandler = ReactiveStatusHandlers.defaultFeign(new ErrorDecoder.Default()); protected InvocationHandlerFactory invocationHandlerFactory = new ReactiveInvocationHandler.Factory(); protected boolean decode404 = false; protected Target target; private Function, Flux> retryFunction; protected Builder(){ contract(new Contract.Default()); } abstract public Builder options(ReactiveOptions options); protected Builder clientFactory(Function clientFactory) { this.clientFactory = clientFactory; return this; } /** * Sets contract. Provided contract will be wrapped in {@link ReactiveContract} * * @param contract contract. * @return this builder */ public Builder contract(final Contract contract) { this.contract = new ReactiveContract(contract); return this; } public Builder addHeaders(List> headers) { this.requestInterceptor = request -> { headers.forEach(header -> addOrdered(request.headers(), header.left, header.right)); return request; }; return this; } public Builder requestInterceptor(ReactiveHttpRequestInterceptor requestInterceptor) { this.requestInterceptor = requestInterceptor; return this; } /** * This flag indicates that the reactive feign client should process responses with 404 status, * specifically returning empty {@link Mono} or {@link Flux} instead of throwing * {@link FeignException}. *

*

* This flag only works with 404, as opposed to all or arbitrary status codes. This was an * explicit decision: 404 - empty is safe, common and doesn't complicate redirection, retry or * fallback policy. * * @return this builder */ public Builder decode404() { this.decode404 = true; return this; } public Builder statusHandler(ReactiveStatusHandler statusHandler) { this.statusHandler = statusHandler; return this; } /** * The most common way to introduce custom logic on handling http response * * @param responseMapper * @return */ public Builder responseMapper(BiFunction responseMapper) { this.responseMapper = responseMapper; return this; } public Builder retryWhen(Function, Flux> retryFunction) { this.retryFunction = retryFunction; return this; } public Builder retryWhen(ReactiveRetryPolicy retryPolicy) { return retryWhen(retryPolicy.toRetryFunction()); } /** * Defines target and builds client. * * @param apiType API interface * @param url base URL * @return built client */ public T target(final Class apiType, final String url) { return target(new Target.HardCodedTarget<>(apiType, url)); } /** * Defines target and builds client. * * @param target target instance * @return built client */ public T target(final Target target) { this.target = target; return build().newInstance(target); } protected ReactiveFeign build() { final ParseHandlersByName handlersByName = new ParseHandlersByName( contract, buildReactiveMethodHandlerFactory()); return new ReactiveFeign(handlersByName, invocationHandlerFactory); } protected MethodHandlerFactory buildReactiveMethodHandlerFactory() { return new ReactiveMethodHandlerFactory(buildReactiveClientFactory()); } protected PublisherClientFactory buildReactiveClientFactory() { return methodMetadata -> { checkNotNull(clientFactory, "clientFactory wasn't provided in ReactiveFeign builder"); ReactiveHttpClient reactiveClient = clientFactory.apply(methodMetadata); if (requestInterceptor != null) { reactiveClient = intercept(reactiveClient, requestInterceptor); } reactiveClient = log(reactiveClient, methodMetadata); if (responseMapper != null) { reactiveClient = mapResponse(reactiveClient, methodMetadata, responseMapper); } if (decode404) { reactiveClient = mapResponse(reactiveClient, methodMetadata, ignore404()); } if (statusHandler != null) { reactiveClient = handleStatus(reactiveClient, methodMetadata, statusHandler); } reactivefeign.publisher.PublisherHttpClient publisherClient = toPublisher(reactiveClient, methodMetadata); if (retryFunction != null) { publisherClient = retry(publisherClient, methodMetadata, retryFunction); } return publisherClient; }; } protected PublisherHttpClient retry( PublisherHttpClient publisherClient, MethodMetadata methodMetadata, Function, Flux> retryFunction) { Type returnPublisherType = returnPublisherType(methodMetadata); if(returnPublisherType == Mono.class){ return new MonoRetryPublisherHttpClient( (MonoPublisherHttpClient)publisherClient, methodMetadata, retryFunction); } else if(returnPublisherType == Flux.class) { return new FluxRetryPublisherHttpClient( (FluxPublisherHttpClient)publisherClient, methodMetadata, retryFunction); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + returnPublisherType); } } protected PublisherHttpClient toPublisher(ReactiveHttpClient reactiveHttpClient, MethodMetadata methodMetadata){ Type returnPublisherType = returnPublisherType(methodMetadata); if(returnPublisherType == Mono.class){ return new MonoPublisherHttpClient(reactiveHttpClient); } else if(returnPublisherType == Flux.class){ return new FluxPublisherHttpClient(reactiveHttpClient); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + returnPublisherType); } } } public static final class ParseHandlersByName { private final Contract contract; private final MethodHandlerFactory factory; ParseHandlersByName(final Contract contract, final MethodHandlerFactory factory) { this.contract = contract; this.factory = factory; } Map apply(final Target target) { Map metadata = contract.parseAndValidatateMetadata(target.type()) .stream() .collect(Collectors.toMap( MethodMetadata::configKey, md -> md )); Map configKeyToMethod = Stream.of(target.type().getMethods()) .collect(Collectors.toMap( method -> Feign.configKey(target.type(), method), method -> method )); final Map result = new LinkedHashMap<>(); for (final Map.Entry entry : configKeyToMethod.entrySet()) { String configKey = entry.getKey(); MethodMetadata md = metadata.get(configKey); MethodHandler methodHandler = md != null ? factory.create(target, md) : factory.createDefault(entry.getValue()); //isDefault(entry.getValue()) result.put(configKey, methodHandler); } return result; } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/ReactiveInvocationHandler.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import feign.InvocationHandlerFactory; import feign.InvocationHandlerFactory.MethodHandler; import feign.Target; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Map; import static feign.Util.checkNotNull; /** * {@link InvocationHandler} implementation that transforms calls to methods of feign contract into * asynchronous HTTP requests via spring WebClient. * * @author Sergii Karpenko */ public final class ReactiveInvocationHandler implements InvocationHandler { private final Target target; private final Map dispatch; private ReactiveInvocationHandler(final Target target, final Map dispatch) { this.target = checkNotNull(target, "target must not be null"); this.dispatch = checkNotNull(dispatch, "dispatch must not be null"); defineObjectMethodsHandlers(); } private void defineObjectMethodsHandlers() { try { dispatch.put(Object.class.getMethod("equals", Object.class), args -> { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); }); dispatch.put(Object.class.getMethod("hashCode"), args -> hashCode()); dispatch.put(Object.class.getMethod("toString"), args -> toString()); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { return dispatch.get(method).invoke(args); } @Override public boolean equals(final Object other) { if (other instanceof ReactiveInvocationHandler) { final ReactiveInvocationHandler otherHandler = (ReactiveInvocationHandler) other; return this.target.equals(otherHandler.target); } return false; } @Override public int hashCode() { return target.hashCode(); } @Override public String toString() { return target.toString(); } /** * Factory for ReactiveInvocationHandler. */ public static final class Factory implements InvocationHandlerFactory { @Override public InvocationHandler create(final Target target, final Map dispatch) { return new ReactiveInvocationHandler(target, dispatch); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/ReactiveOptions.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; /** * @author Sergii Karpenko */ public class ReactiveOptions { private final Long connectTimeoutMillis; private final Long readTimeoutMillis; private final Boolean tryUseCompression; private ReactiveOptions(Long connectTimeoutMillis, Long readTimeoutMillis, Boolean tryUseCompression) { this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; this.tryUseCompression = tryUseCompression; } public Long getConnectTimeoutMillis() { return connectTimeoutMillis; } public Long getReadTimeoutMillis() { return readTimeoutMillis; } public Boolean isTryUseCompression() { return tryUseCompression; } public boolean isEmpty() { return connectTimeoutMillis == null && readTimeoutMillis == null && tryUseCompression == null; } public static class Builder { private Long connectTimeoutMillis; private Long readTimeoutMillis; private Boolean tryUseCompression; public Builder() {} public Builder setConnectTimeoutMillis(long connectTimeoutMillis) { this.connectTimeoutMillis = connectTimeoutMillis; return this; } public Builder setReadTimeoutMillis(long readTimeoutMillis) { this.readTimeoutMillis = readTimeoutMillis; return this; } public Builder setTryUseCompression(boolean tryUseCompression) { this.tryUseCompression = tryUseCompression; return this; } public ReactiveOptions build() { return new ReactiveOptions(connectTimeoutMillis, readTimeoutMillis, tryUseCompression); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/ReactiveRetryPolicy.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuples; import java.time.Duration; import java.util.function.Function; public interface ReactiveRetryPolicy { /** * @param error * @param attemptNo * @return -1 if should not be retried, 0 if retry immediately */ long retryDelay(Throwable error, int attemptNo); default Function, Flux> toRetryFunction() { return errors -> errors .zipWith(Flux.range(1, Integer.MAX_VALUE), (error, index) -> { long delay = retryDelay(error, index); if (delay >= 0) { return Tuples.of(delay, error); } else { throw Exceptions.propagate(error); } }).flatMap( tuple2 -> tuple2.getT1() > 0 ? Mono.delay(Duration.ofMillis(tuple2.getT1())) .map(time -> tuple2.getT2()) : Mono.just(tuple2.getT2())); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/ReactiveRetryers.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import feign.RetryableException; import java.util.Date; /** * @author Sergii Karpenko */ public class ReactiveRetryers { public static ReactiveRetryPolicy retry(int maxRetries) { return (error, attemptNo) -> attemptNo <= maxRetries ? 0 : -1; } public static ReactiveRetryPolicy retryWithBackoff(int maxRetries, long periodInMs) { return (error, attemptNo) -> { if (attemptNo <= maxRetries) { long delay; Date retryAfter; // "Retry-After" header set if (error instanceof RetryableException && (retryAfter = ((RetryableException) error) .retryAfter()) != null) { delay = retryAfter.getTime() - System.currentTimeMillis(); delay = Math.min(delay, periodInMs); delay = Math.max(delay, 0); } else { delay = periodInMs; } return delay; } else { return -1; } }; } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/DelegatingReactiveHttpResponse.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; /** * @author Sergii Karpenko */ abstract public class DelegatingReactiveHttpResponse implements ReactiveHttpResponse { private final ReactiveHttpResponse response; protected DelegatingReactiveHttpResponse(ReactiveHttpResponse response) { this.response = response; } protected ReactiveHttpResponse getResponse() { return response; } @Override public int status() { return response.status(); } @Override public Map> headers() { return response.headers(); } @Override public Mono bodyData() { throw new UnsupportedOperationException(); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/InterceptorReactiveHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; /** * Used to modify request before call. May be used to set auth headers to all requests. * * @author Sergii Karpenko */ public class InterceptorReactiveHttpClient { public static ReactiveHttpClient intercept(ReactiveHttpClient reactiveHttpClient, ReactiveHttpRequestInterceptor interceptor) { return request -> reactiveHttpClient.executeRequest(interceptor.apply(request)); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/LoggerReactiveHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import feign.MethodMetadata; import org.reactivestreams.Publisher; import org.slf4j.LoggerFactory; import reactivefeign.utils.Pair; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import static reactivefeign.utils.FeignUtils.methodTag; import static reactor.core.publisher.Mono.just; /** * Wraps {@link ReactiveHttpClient} with log logic * * @author Sergii Karpenko */ public class LoggerReactiveHttpClient implements ReactiveHttpClient { private final org.slf4j.Logger logger = LoggerFactory.getLogger(LoggerReactiveHttpClient.class); private final ReactiveHttpClient reactiveClient; private final String methodTag; public static ReactiveHttpClient log(ReactiveHttpClient reactiveClient, MethodMetadata methodMetadata) { return new LoggerReactiveHttpClient(reactiveClient, methodMetadata); } private LoggerReactiveHttpClient(ReactiveHttpClient reactiveClient, MethodMetadata methodMetadata) { this.reactiveClient = reactiveClient; this.methodTag = methodTag(methodMetadata); } @Override public Mono executeRequest(ReactiveHttpRequest request) { AtomicLong start = new AtomicLong(-1); return Mono .defer(() -> { start.set(System.currentTimeMillis()); return just(request); }) .flatMap(req -> { req = logRequest(methodTag, req); return reactiveClient.executeRequest(req) .doOnNext(resp -> logResponseHeaders(methodTag, resp, System.currentTimeMillis() - start.get())); }) .map(resp -> new LoggerReactiveHttpResponse(resp, start)); } private ReactiveHttpRequest logRequest( String feignMethodTag, ReactiveHttpRequest request) { if (logger.isDebugEnabled()) { logger.debug("[{}]--->{} {} HTTP/1.1", feignMethodTag, request.method(), request.uri()); } if (logger.isTraceEnabled()) { logger.trace("[{}] REQUEST HEADERS\n{}", feignMethodTag, msg(() -> request.headers().entrySet().stream() .map(entry -> String.format("%s:%s", entry.getKey(), entry.getValue())) .collect(Collectors.joining("\n")))); if(request.body() != null) { Publisher bodyLogged; if (request.body() instanceof Mono) { bodyLogged = ((Mono) request.body()).doOnNext(body -> logger.trace( "[{}] REQUEST BODY\n{}", feignMethodTag, body)); } else if (request.body() instanceof Flux) { bodyLogged = ((Flux) request.body()).doOnNext(body -> logger.trace( "[{}] REQUEST BODY ELEMENT\n{}", feignMethodTag, body)); } else { throw new IllegalArgumentException("Unsupported publisher type: " + request.body().getClass()); } return new ReactiveHttpRequest(request, bodyLogged); } } return request; } private void logResponseHeaders(String feignMethodTag, ReactiveHttpResponse httpResponse, long elapsedTime) { if (logger.isTraceEnabled()) { logger.trace("[{}] RESPONSE HEADERS\n{}", feignMethodTag, msg(() -> httpResponse.headers().entrySet().stream() .flatMap(entry -> entry.getValue().stream() .map(value -> new Pair<>(entry.getKey(), value))) .map(pair -> String.format("%s:%s", pair.left, pair.right)) .collect(Collectors.joining("\n")))); } if (logger.isDebugEnabled()) { logger.debug("[{}]<--- headers takes {} milliseconds", feignMethodTag, elapsedTime); } } private void logResponseBodyAndTime(String feignMethodTag, Object response, long elapsedTime) { if (logger.isTraceEnabled()) { logger.trace("[{}] RESPONSE BODY\n{}", feignMethodTag, response); } if (logger.isDebugEnabled()) { logger.debug("[{}]<--- body takes {} milliseconds", feignMethodTag, elapsedTime); } } private class LoggerReactiveHttpResponse extends DelegatingReactiveHttpResponse { private final AtomicLong start; private LoggerReactiveHttpResponse(ReactiveHttpResponse response, AtomicLong start) { super(response); this.start = start; } @Override public Publisher body() { Publisher publisher = getResponse().body(); if (publisher instanceof Mono) { return ((Mono) publisher).doOnNext(responseBodyLogger(start)); } else { return ((Flux) publisher).doOnNext(responseBodyLogger(start)); } } @Override public Mono bodyData() { Mono publisher = getResponse().bodyData(); return publisher.doOnNext(responseBodyLogger(start)); } private Consumer responseBodyLogger(AtomicLong start) { return result -> logResponseBodyAndTime(methodTag, result, System.currentTimeMillis() - start.get()); } } private static MessageSupplier msg(Supplier supplier) { return new MessageSupplier(supplier); } static class MessageSupplier { private Supplier supplier; public MessageSupplier(Supplier supplier) { this.supplier = supplier; } @Override public String toString() { return supplier.get().toString(); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/ReactiveHttpClient.java ================================================ package reactivefeign.client; import reactor.core.publisher.Mono; public interface ReactiveHttpClient { Mono executeRequest(ReactiveHttpRequest request); } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/ReactiveHttpRequest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import org.reactivestreams.Publisher; import java.net.URI; import java.util.List; import java.util.Map; import static feign.Util.checkNotNull; /** * An immutable reactive request to an http server. * * @author Sergii Karpenko */ public final class ReactiveHttpRequest { private final String method; private final URI uri; private final Map> headers; private final Publisher body; /** * No parameters can be null except {@code body}. All parameters must be effectively immutable, * via safe copies, not mutating or otherwise. */ public ReactiveHttpRequest(String method, URI uri, Map> headers, Publisher body) { this.method = checkNotNull(method, "method of %s", uri); this.uri = checkNotNull(uri, "url"); this.headers = checkNotNull(headers, "headers of %s %s", method, uri); this.body = body; // nullable } public ReactiveHttpRequest(ReactiveHttpRequest request, Publisher body){ this(request.method, request.uri, request.headers, body); } /* Method to invoke on the server. */ public String method() { return method; } /* Fully resolved URL including query. */ public URI uri() { return uri; } /* Ordered list of headers that will be sent to the server. */ public Map> headers() { return headers; } /** * If present, this is the replayable body to send to the server. */ public Publisher body() { return body; } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/ReactiveHttpRequestInterceptor.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import java.util.function.Function; /** * Used to modify request before call. May be used to set auth headers to all requests. * * @author Sergii Karpenko * */ public interface ReactiveHttpRequestInterceptor extends Function { } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/ReactiveHttpResponse.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; /** * Reactive response from an http server. * * @author Sergii Karpenko */ public interface ReactiveHttpResponse { int status(); Map> headers(); Publisher body(); /** * used by error decoders * * @return error message data */ Mono bodyData(); } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/ReadTimeoutException.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; public class ReadTimeoutException extends RuntimeException { public ReadTimeoutException(Throwable cause) { super(cause); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/ResponseMappers.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import feign.MethodMetadata; import org.apache.commons.httpclient.HttpStatus; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import java.util.function.BiFunction; /** * Maps 404 error response to successful empty response * * @author Sergii Karpenko */ public class ResponseMappers { public static BiFunction ignore404() { return (MethodMetadata methodMetadata, ReactiveHttpResponse response) -> { if (response.status() == HttpStatus.SC_NOT_FOUND) { return new DelegatingReactiveHttpResponse(response) { @Override public int status() { return HttpStatus.SC_OK; } @Override public Publisher body() { return Mono.empty(); } }; } return response; }; } public static ReactiveHttpClient mapResponse( ReactiveHttpClient reactiveHttpClient, MethodMetadata methodMetadata, BiFunction responseMapper) { return request -> reactiveHttpClient.executeRequest(request) .map(response -> responseMapper.apply(methodMetadata, response)); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/StatusHandlerReactiveHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client; import feign.MethodMetadata; import org.reactivestreams.Publisher; import reactivefeign.client.statushandler.ReactiveStatusHandler; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static reactivefeign.utils.FeignUtils.methodTag; /** * Uses statusHandlers to process status of http response * * @author Sergii Karpenko */ public class StatusHandlerReactiveHttpClient implements ReactiveHttpClient { private final ReactiveHttpClient reactiveClient; private final String methodTag; private final ReactiveStatusHandler statusHandler; public static ReactiveHttpClient handleStatus( ReactiveHttpClient reactiveClient, MethodMetadata methodMetadata, ReactiveStatusHandler statusHandler) { return new StatusHandlerReactiveHttpClient(reactiveClient, methodMetadata, statusHandler); } private StatusHandlerReactiveHttpClient(ReactiveHttpClient reactiveClient, MethodMetadata methodMetadata, ReactiveStatusHandler statusHandler) { this.reactiveClient = reactiveClient; this.methodTag = methodTag(methodMetadata); this.statusHandler = statusHandler; } @Override public Mono executeRequest(ReactiveHttpRequest request) { return reactiveClient.executeRequest(request).map(response -> { if (statusHandler.shouldHandle(response.status())) { return new ErrorReactiveHttpResponse(response, statusHandler.decode(methodTag, response)); } else { return response; } }); } private class ErrorReactiveHttpResponse extends DelegatingReactiveHttpResponse { private final Mono error; protected ErrorReactiveHttpResponse(ReactiveHttpResponse response, Mono error) { super(response); this.error = error; } @Override public Publisher body() { if (getResponse().body() instanceof Mono) { return error.flatMap(Mono::error); } else { return error.flatMapMany(Flux::error); } } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/statushandler/CompositeStatusHandler.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client.statushandler; import reactivefeign.client.ReactiveHttpResponse; import reactor.core.publisher.Mono; import java.util.List; import static java.util.Arrays.asList; /** * @author Sergii Karpenko */ public class CompositeStatusHandler implements ReactiveStatusHandler { private final List handlers; public static CompositeStatusHandler compose(ReactiveStatusHandler... handlers) { return new CompositeStatusHandler(asList(handlers)); } private CompositeStatusHandler(List handlers) { this.handlers = handlers; } @Override public boolean shouldHandle(int status) { return handlers.stream().anyMatch(handler -> handler.shouldHandle(status)); } @Override public Mono decode(String methodKey, ReactiveHttpResponse response) { return handlers.stream() .filter(statusHandler -> statusHandler .shouldHandle(response.status())) .findFirst() .map(statusHandler -> statusHandler.decode(methodKey, response)) .orElse(null); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/statushandler/ReactiveStatusHandler.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client.statushandler; import reactivefeign.client.ReactiveHttpResponse; import reactor.core.publisher.Mono; /** * @author Sergii Karpenko */ public interface ReactiveStatusHandler { boolean shouldHandle(int status); Mono decode(String methodKey, ReactiveHttpResponse response); } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/client/statushandler/ReactiveStatusHandlers.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.client.statushandler; import feign.Response; import feign.codec.ErrorDecoder; import org.apache.commons.httpclient.HttpStatus; import reactivefeign.client.ReactiveHttpResponse; import reactor.core.publisher.Mono; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Collectors; import static reactivefeign.utils.HttpUtils.familyOf; public class ReactiveStatusHandlers { public static ReactiveStatusHandler defaultFeign(ErrorDecoder errorDecoder) { return new ReactiveStatusHandler() { @Override public boolean shouldHandle(int status) { return familyOf(status).isError(); } @Override public Mono decode(String methodTag, ReactiveHttpResponse response) { return response.bodyData() .defaultIfEmpty(new byte[0]) .map(bodyData -> errorDecoder.decode(methodTag, Response.builder().status(response.status()) .reason(HttpStatus.getStatusText(response.status())) .headers(response.headers().entrySet() .stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) .body(bodyData).build())); } }; } public static ReactiveStatusHandler throwOnStatus( Predicate statusPredicate, BiFunction errorFunction) { return new ReactiveStatusHandler() { @Override public boolean shouldHandle(int status) { return statusPredicate.test(status); } @Override public Mono decode(String methodKey, ReactiveHttpResponse response) { return Mono.just(errorFunction.apply(methodKey, response)); } }; } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/DefaultMethodHandler.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.methodhandler; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * Handles default methods by directly invoking the default method code on the interface. The bindTo * method must be called on the result before invoke is called. */ public final class DefaultMethodHandler implements MethodHandler { // Uses Java 7 MethodHandle based reflection. As default methods will only exist when // run on a Java 8 JVM this will not affect use on legacy JVMs. private final MethodHandle unboundHandle; // handle is effectively final after bindTo has been called. private MethodHandle handle; public DefaultMethodHandler(Method defaultMethod) { try { Class declaringClass = defaultMethod.getDeclaringClass(); Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP"); field.setAccessible(true); Lookup lookup = (Lookup) field.get(null); this.unboundHandle = lookup.unreflectSpecial(defaultMethod, declaringClass); } catch (NoSuchFieldException | IllegalAccessException ex) { throw new IllegalStateException(ex); } } /** * Bind this handler to a proxy object. After bound, DefaultMethodHandler#invoke will act as if it * was called on the proxy object. Must be called once and only once for a given instance of * DefaultMethodHandler */ public void bindTo(Object proxy) { if (handle != null) { throw new IllegalStateException( "Attempted to rebind a default method handler that was already bound"); } handle = unboundHandle.bindTo(proxy); } /** * Invoke this method. DefaultMethodHandler#bindTo must be called before the first time invoke is * called. */ @Override public Object invoke(Object[] argv) throws Throwable { if (handle == null) { throw new IllegalStateException( "Default method handler invoked before proxy has been bound."); } return handle.invokeWithArguments(argv); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/FluxMethodHandler.java ================================================ package reactivefeign.methodhandler; import reactor.core.publisher.Flux; public class FluxMethodHandler implements MethodHandler { private final MethodHandler methodHandler; public FluxMethodHandler(MethodHandler methodHandler) { this.methodHandler = methodHandler; } @Override @SuppressWarnings("unchecked") public Flux invoke(final Object[] argv) { try { return (Flux)methodHandler.invoke(argv); } catch (Throwable throwable) { return Flux.error(throwable); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/MethodHandler.java ================================================ package reactivefeign.methodhandler; import feign.InvocationHandlerFactory; public interface MethodHandler extends InvocationHandlerFactory.MethodHandler{ } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/MethodHandlerFactory.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.methodhandler; import feign.MethodMetadata; import feign.Target; import reactivefeign.methodhandler.MethodHandler; import java.lang.reflect.Method; public interface MethodHandlerFactory { MethodHandler create(final Target target, final MethodMetadata metadata); MethodHandler createDefault(Method method); } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/MonoMethodHandler.java ================================================ package reactivefeign.methodhandler; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class MonoMethodHandler implements MethodHandler { private final MethodHandler methodHandler; public MonoMethodHandler(MethodHandler methodHandler) { this.methodHandler = methodHandler; } @Override @SuppressWarnings("unchecked") public Mono invoke(final Object[] argv) { try { return (Mono)methodHandler.invoke(argv); } catch (Throwable throwable) { return Mono.error(throwable); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/PublisherClientMethodHandler.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.methodhandler; import feign.MethodMetadata; import feign.Target; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.publisher.PublisherHttpClient; import reactivefeign.utils.Pair; import reactor.core.publisher.Mono; import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; import java.util.*; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import static feign.Util.checkNotNull; import static java.util.stream.Collectors.*; import static reactivefeign.utils.FeignUtils.returnPublisherType; import static reactivefeign.utils.MultiValueMapUtils.*; /** * Method handler for asynchronous HTTP requests via {@link PublisherHttpClient}. * * Transforms method invocation into request that executed by {@link ReactiveHttpClient}. * * @author Sergii Karpenko */ public class PublisherClientMethodHandler implements MethodHandler { private final Target target; private final MethodMetadata methodMetadata; private final PublisherHttpClient publisherClient; private final Function, String> pathExpander; private final Map, String>>> headerExpanders; private final Map> queriesAll; private final Map, String>>> queryExpanders; public PublisherClientMethodHandler(Target target, MethodMetadata methodMetadata, PublisherHttpClient publisherClient) { this.target = checkNotNull(target, "target must be not null"); this.methodMetadata = checkNotNull(methodMetadata, "methodMetadata must be not null"); this.publisherClient = checkNotNull(publisherClient, "client must be not null"); this.pathExpander = buildExpandFunction(methodMetadata.template().url()); this.headerExpanders = buildExpanders(methodMetadata.template().headers()); this.queriesAll = new HashMap<>(methodMetadata.template().queries()); if (methodMetadata.formParams() != null) { methodMetadata.formParams() .forEach(param -> add(queriesAll, param, "{" + param + "}")); } this.queryExpanders = buildExpanders(queriesAll); } @Override @SuppressWarnings("unchecked") public Publisher invoke(final Object[] argv) { final ReactiveHttpRequest request = buildRequest(argv); return publisherClient.executeRequest(request); } protected ReactiveHttpRequest buildRequest(Object[] argv) { Map substitutionsMap = methodMetadata.indexToName().entrySet().stream() .flatMap(e -> e.getValue().stream() .map(v -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v))) .collect(Collectors.toMap(Map.Entry::getValue, entry -> argv[entry.getKey()])); try { String path = pathExpander.apply(substitutionsMap); Map> queries = queries(argv, substitutionsMap); Map> headers = headers(argv, substitutionsMap); URI uri = new URI(target.url() + path + queryLine(queries)); return new ReactiveHttpRequest(methodMetadata.template().method(), uri, headers, body(argv)); } catch (URISyntaxException e) { throw new RuntimeException(e); } } private String queryLine(Map> queries) { if (queries.isEmpty()) { return ""; } StringBuilder queryBuilder = new StringBuilder(); for (Map.Entry> query : queries.entrySet()) { String field = query.getKey(); for (String value : query.getValue()) { queryBuilder.append('&'); queryBuilder.append(field); if (value != null) { queryBuilder.append('='); if (!value.isEmpty()) { queryBuilder.append(value); } } } } queryBuilder.deleteCharAt(0); return queryBuilder.insert(0, '?').toString(); } protected Map> queries(Object[] argv, Map substitutionsMap) { Map> queries = new LinkedHashMap<>(); // queries from template queriesAll.keySet() .forEach(queryName -> addAll(queries, queryName, queryExpanders.get(queryName).stream() .map(expander -> expander.apply(substitutionsMap)) .collect(toList()))); // queries from args if (methodMetadata.queryMapIndex() != null) { ((Map) argv[methodMetadata.queryMapIndex()]) .forEach((key, value) -> { if (value instanceof Iterable) { ((Iterable) value).forEach(element -> add(queries, key, element.toString())); } else { add(queries, key, value.toString()); } }); } return queries; } protected Map> headers(Object[] argv, Map substitutionsMap) { Map> headers = new LinkedHashMap<>(); // headers from template methodMetadata.template().headers().keySet() .forEach(headerName -> addAllOrdered(headers, headerName, headerExpanders.get(headerName).stream() .map(expander -> expander.apply(substitutionsMap)) .collect(toList()))); // headers from args if (methodMetadata.headerMapIndex() != null) { ((Map) argv[methodMetadata.headerMapIndex()]) .forEach((key, value) -> { if (value instanceof Iterable) { ((Iterable) value) .forEach(element -> addOrdered(headers, key, element.toString())); } else { addOrdered(headers, key, value.toString()); } }); } return headers; } protected Publisher body(Object[] argv) { if (methodMetadata.bodyIndex() != null) { return body(argv[methodMetadata.bodyIndex()]); } else { return Mono.empty(); } } protected Publisher body(Object body) { if (body instanceof Publisher) { return (Publisher) body; } else { return Mono.just(body); } } private static Map, String>>> buildExpanders( Map> templates) { Stream> headersFlattened = templates.entrySet().stream() .flatMap(e -> e.getValue().stream() .map(v -> new Pair<>(e.getKey(), v))); return headersFlattened.collect(groupingBy( entry -> entry.left, mapping(entry -> buildExpandFunction(entry.right), toList()))); } /** * * @param template * @return function that able to map substitutions map to actual value for specified template */ private static final Pattern PATTERN = Pattern.compile("\\{([^}]+)\\}"); private static Function, String> buildExpandFunction(String template) { List, String>> chunks = new ArrayList<>(); Matcher matcher = PATTERN.matcher(template); int previousMatchEnd = 0; while (matcher.find()) { String textChunk = template.substring(previousMatchEnd, matcher.start()); if (textChunk.length() > 0) { chunks.add(data -> textChunk); } String substitute = matcher.group(1); chunks.add(data -> { Object substitution = data.get(substitute); if (substitution != null) { return substitution.toString(); } else { return substitute; } }); previousMatchEnd = matcher.end(); } String textChunk = template.substring(previousMatchEnd, template.length()); if (textChunk.length() > 0) { chunks.add(data -> textChunk); } return traceData -> chunks.stream().map(chunk -> chunk.apply(traceData)) .collect(Collectors.joining()); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/methodhandler/ReactiveMethodHandlerFactory.java ================================================ package reactivefeign.methodhandler; import feign.MethodMetadata; import feign.Target; import reactivefeign.publisher.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.lang.reflect.Type; import static feign.Util.checkNotNull; import static reactivefeign.utils.FeignUtils.returnPublisherType; public class ReactiveMethodHandlerFactory implements MethodHandlerFactory { private final PublisherClientFactory publisherClientFactory; public ReactiveMethodHandlerFactory(final PublisherClientFactory publisherClientFactory) { this.publisherClientFactory = checkNotNull(publisherClientFactory, "client must not be null"); } @Override public MethodHandler create(Target target, MethodMetadata metadata) { MethodHandler methodHandler = new PublisherClientMethodHandler( target, metadata, publisherClientFactory.apply(metadata)); Type returnPublisherType = returnPublisherType(metadata); if(returnPublisherType == Mono.class){ return new MonoMethodHandler(methodHandler); } else if(returnPublisherType == Flux.class) { return new FluxMethodHandler(methodHandler); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + returnPublisherType); } } @Override public MethodHandler createDefault(Method method) { MethodHandler defaultMethodHandler = new DefaultMethodHandler(method); if(method.getReturnType() == Mono.class){ return new MonoMethodHandler(defaultMethodHandler); } else if(method.getReturnType() == Flux.class) { return new FluxMethodHandler(defaultMethodHandler); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + method.getReturnType()); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/FluxPublisherHttpClient.java ================================================ package reactivefeign.publisher; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.client.ReactiveHttpResponse; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * Wraps {@link PublisherHttpClient} * * @author Sergii Karpenko */ public class FluxPublisherHttpClient implements PublisherHttpClient { private final ReactiveHttpClient reactiveHttpClient; public FluxPublisherHttpClient(ReactiveHttpClient reactiveHttpClient) { this.reactiveHttpClient = reactiveHttpClient; } @Override public Flux executeRequest(ReactiveHttpRequest request) { Mono response = reactiveHttpClient.executeRequest(request); return response.flatMapMany(ReactiveHttpResponse::body); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/FluxRetryPublisherHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.publisher; import feign.MethodMetadata; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpRequest; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.function.Function; /** * Wraps {@link PublisherHttpClient} with retry logic provided by retryFunction * * @author Sergii Karpenko */ public class FluxRetryPublisherHttpClient extends RetryPublisherHttpClient { public FluxRetryPublisherHttpClient( FluxPublisherHttpClient publisherClient, MethodMetadata methodMetadata, Function, Flux> retryFunction) { super(publisherClient, methodMetadata, retryFunction); } @Override public Publisher executeRequest(ReactiveHttpRequest request) { Flux response = publisherClient.executeRequest(request); return response.retryWhen(retryFunction).onErrorMap(outOfRetries()); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/MonoPublisherHttpClient.java ================================================ package reactivefeign.publisher; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.client.ReactiveHttpResponse; import reactor.core.publisher.Mono; /** * Wraps {@link PublisherHttpClient} * * @author Sergii Karpenko */ public class MonoPublisherHttpClient implements PublisherHttpClient { private final ReactiveHttpClient reactiveHttpClient; public MonoPublisherHttpClient(ReactiveHttpClient reactiveHttpClient) { this.reactiveHttpClient = reactiveHttpClient; } @Override public Mono executeRequest(ReactiveHttpRequest request) { Mono response = reactiveHttpClient.executeRequest(request); return response.flatMap(resp -> Mono.from(resp.body())); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/MonoRetryPublisherHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.publisher; import feign.MethodMetadata; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpRequest; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.function.Function; /** * Wraps {@link PublisherHttpClient} with retry logic provided by retryFunction * * @author Sergii Karpenko */ public class MonoRetryPublisherHttpClient extends RetryPublisherHttpClient { public MonoRetryPublisherHttpClient( MonoPublisherHttpClient publisherClient, MethodMetadata methodMetadata, Function, Flux> retryFunction) { super(publisherClient, methodMetadata, retryFunction); } @Override public Publisher executeRequest(ReactiveHttpRequest request) { Mono response = publisherClient.executeRequest(request); return response.retryWhen(retryFunction).onErrorMap(outOfRetries()); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/PublisherClientFactory.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.publisher; import feign.MethodMetadata; import java.util.function.Function; /** * @author Sergii Karpenko */ public interface PublisherClientFactory extends Function { } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/PublisherHttpClient.java ================================================ package reactivefeign.publisher; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpRequest; import java.lang.reflect.Type; /** * @author Sergii Karpenko */ public interface PublisherHttpClient { Publisher executeRequest(ReactiveHttpRequest request); } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/publisher/RetryPublisherHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.publisher; import feign.MethodMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import java.util.function.Function; import static reactivefeign.utils.FeignUtils.methodTag; /** * Wraps {@link PublisherHttpClient} with retry logic provided by retryFunction * * @author Sergii Karpenko */ abstract public class RetryPublisherHttpClient

implements PublisherHttpClient { private static final Logger logger = LoggerFactory.getLogger(RetryPublisherHttpClient.class); private final String feignMethodTag; protected final P publisherClient; protected final Function, Flux> retryFunction; protected RetryPublisherHttpClient(P publisherClient, MethodMetadata methodMetadata, Function, Flux> retryFunction) { this.publisherClient = publisherClient; this.feignMethodTag = methodTag(methodMetadata); this.retryFunction = wrapWithLog(retryFunction, feignMethodTag); } protected Function outOfRetries() { return throwable -> { logger.debug("[{}]---> USED ALL RETRIES", feignMethodTag, throwable); return new OutOfRetriesException(throwable, feignMethodTag); }; } protected static Function, Flux> wrapWithLog( Function, Flux> retryFunction, String feignMethodTag) { return throwableFlux -> retryFunction.apply(throwableFlux) .doOnNext(throwable -> { if (logger.isDebugEnabled()) { logger.debug("[{}]---> RETRYING on error", feignMethodTag, throwable); } }); } public static class OutOfRetriesException extends Exception { OutOfRetriesException(Throwable cause, String feignMethodTag) { super("All retries used for: " + feignMethodTag, cause); } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/utils/FeignUtils.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.utils; import feign.MethodMetadata; import org.reactivestreams.Publisher; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import static feign.Util.resolveLastTypeParameter; import static java.util.Optional.ofNullable; public class FeignUtils { public static String methodTag(MethodMetadata methodMetadata) { return methodMetadata.configKey().substring(0, methodMetadata.configKey().indexOf('(')); } public static Class returnPublisherType(MethodMetadata methodMetadata) { final Type returnType = methodMetadata.returnType(); return (Class)((ParameterizedType) returnType).getRawType(); } public static Type returnActualType(MethodMetadata methodMetadata) { return resolveLastTypeParameter(methodMetadata.returnType(), returnPublisherType(methodMetadata)); } public static Type bodyActualType(MethodMetadata methodMetadata) { return getBodyActualType(methodMetadata.bodyType()); } public static Type getBodyActualType(Type bodyType) { return ofNullable(bodyType).map(type -> { if (type instanceof ParameterizedType) { Class bodyClass = (Class) ((ParameterizedType) type).getRawType(); if (Publisher.class.isAssignableFrom(bodyClass)) { return resolveLastTypeParameter(bodyType, bodyClass); } else { return type; } } else { return type; } }).orElse(null); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/utils/HttpUtils.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.utils; import static reactivefeign.utils.HttpUtils.StatusCodeFamily.*; public class HttpUtils { public static StatusCodeFamily familyOf(final int statusCode) { switch (statusCode / 100) { case 1: return INFORMATIONAL; case 2: return SUCCESSFUL; case 3: return REDIRECTION; case 4: return CLIENT_ERROR; case 5: return SERVER_ERROR; default: return OTHER; } } public enum StatusCodeFamily { INFORMATIONAL(false), SUCCESSFUL(false), REDIRECTION(false), CLIENT_ERROR(true), SERVER_ERROR( true), OTHER(false); private final boolean error; StatusCodeFamily(boolean error) { this.error = error; } public boolean isError() { return error; } } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/utils/MultiValueMapUtils.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.utils; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; public class MultiValueMapUtils { public static void addAllOrdered(Map> multiMap, K key, List values) { multiMap.compute(key, (key_, values_) -> { List valuesMerged = values_ != null ? values_ : new ArrayList<>(values.size()); valuesMerged.addAll(values); return valuesMerged; }); } public static void addOrdered(Map> multiMap, K key, V value) { multiMap.compute(key, (key_, values_) -> { List valuesMerged = values_ != null ? values_ : new ArrayList<>(1); valuesMerged.add(value); return valuesMerged; }); } public static void addAll(Map> multiMap, K key, Collection values) { multiMap.compute(key, (key_, values_) -> { Collection valuesMerged = values_ != null ? values_ : new ArrayList<>(values.size()); valuesMerged.addAll(values); return valuesMerged; }); } public static void add(Map> multiMap, K key, V value) { multiMap.compute(key, (key_, values_) -> { Collection valuesMerged = values_ != null ? values_ : new ArrayList<>(1); valuesMerged.add(value); return valuesMerged; }); } } ================================================ FILE: feign-reactor-core/src/main/java/reactivefeign/utils/Pair.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.utils; public class Pair { public final L left; public final R right; public Pair(L left, R right) { this.left = left; this.right = right; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/CompressionTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.common.Gzip; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.Bill; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.OrderGenerator; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static reactivefeign.TestUtils.equalsComparingFieldByFieldRecursively; /** * Test the new capability of Reactive Feign client to support both Feign Request.Options * (regression) and the new ReactiveOptions configuration. * * @author Sergii Karpenko */ abstract public class CompressionTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); @Test public void testCompression() throws JsonProcessingException { IceCreamOrder order = new OrderGenerator().generate(20); Bill billExpected = Bill.makeBill(order); wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) .withHeader("Accept-Encoding", containing("gzip")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withHeader("Content-Encoding", "gzip") .withBody(Gzip.gzip(TestUtils.MAPPER.writeValueAsString(billExpected))))); IcecreamServiceApi client = builder( new ReactiveOptions.Builder() .setTryUseCompression(true) .build()) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Mono bill = client.makeOrder(order); StepVerifier.create(bill) .expectNextMatches(equalsComparingFieldByFieldRecursively(billExpected)) .verifyComplete(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/ConnectionTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import org.hamcrest.Matchers; import org.junit.*; import org.junit.rules.ExpectedException; import reactivefeign.testcase.IcecreamServiceApi; import java.io.IOException; import java.net.ConnectException; import java.net.ServerSocket; import java.net.Socket; /** * @author Sergii Karpenko */ abstract public class ConnectionTimeoutTest { @Rule public ExpectedException expectedException = ExpectedException.none(); private ServerSocket serverSocket; private Socket socket; private int port; abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); @Before public void before() throws IOException { // server socket with single element backlog queue (1) and dynamicaly allocated // port (0) serverSocket = new ServerSocket(0, 1); // just get the allocated port port = serverSocket.getLocalPort(); // fill backlog queue by this request so consequent requests will be blocked socket = new Socket(); socket.connect(serverSocket.getLocalSocketAddress()); } @After public void after() throws IOException { // some cleanup if (serverSocket != null && !serverSocket.isClosed()) { serverSocket.close(); } } // TODO investigate why doesn't work on codecov.io but works locally @Ignore @Test public void shouldFailOnConnectionTimeout() { expectedException.expectCause( Matchers.any(ConnectException.class)); IcecreamServiceApi client = builder( new ReactiveOptions.Builder() .setConnectTimeoutMillis(300) .setReadTimeoutMillis(100) .build()) .target(IcecreamServiceApi.class, "http://localhost:" + port); client.findOrder(1).block(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/ContractTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.IcecreamServiceApiBroken; import reactivefeign.testcase.IcecreamServiceApiBrokenByCopy; import static org.hamcrest.Matchers.containsString; /** * @author Sergii Karpenko */ abstract public class ContractTest { @Rule public ExpectedException expectedException = ExpectedException.none(); abstract protected ReactiveFeign.Builder builder(); @Test public void shouldFailOnBrokenContract() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage(containsString("Broken Contract")); this.builder() .contract(targetType -> { throw new IllegalArgumentException("Broken Contract"); }) .target(IcecreamServiceApi.class, "http://localhost:8888"); } @Test public void shouldFailIfNotReactiveContract() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage(containsString("IcecreamServiceApiBroken#findOrderBlocking(int)")); this.builder() .target(IcecreamServiceApiBroken.class, "http://localhost:8888"); } @Test public void shouldFailIfMethodOperatesWithByteArray() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage(containsString("IcecreamServiceApiBrokenByCopy#findOrderCopy(int)")); this.builder() .target(IcecreamServiceApiBrokenByCopy.class, "http://localhost:8888"); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/DefaultMethodTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.RequestLine; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.OrderGenerator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static reactivefeign.TestUtils.equalsComparingFieldByFieldRecursively; /** * @author Sergii Karpenko */ abstract public class DefaultMethodTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); @Before public void resetServers() { wireMockRule.resetAll(); } abstract protected ReactiveFeign.Builder builder(); abstract protected ReactiveFeign.Builder builder(Class apiClass); abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); @Test public void shouldProcessDefaultMethodOnProxy() throws JsonProcessingException { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(orderStr))); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findFirstOrder()) .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) .verifyComplete(); } @Test(expected = RuntimeException.class) public void shouldNotWrapException() { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.throwsException().onErrorReturn( throwable -> throwable.equals(IcecreamServiceApi.RUNTIME_EXCEPTION), orderGenerated).block(); } @Test public void shouldOverrideEquals() { IcecreamServiceApi client = builder( new ReactiveOptions.Builder() .setConnectTimeoutMillis(300) .setReadTimeoutMillis(100).build()) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); IcecreamServiceApi clientWithSameTarget = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client).isEqualTo(clientWithSameTarget); IcecreamServiceApi clientWithOtherPort = builder() .target(IcecreamServiceApi.class, "http://localhost:" + (wireMockRule.port() + 1)); Assertions.assertThat(client).isNotEqualTo(clientWithOtherPort); OtherApi clientWithOtherInterface = builder(OtherApi.class) .target(OtherApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client).isNotEqualTo(clientWithOtherInterface); } interface OtherApi { @RequestLine("GET /icecream/flavors") Mono method(String arg); } @Test public void shouldOverrideHashcode() { IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); IcecreamServiceApi otherClientWithSameTarget = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client.hashCode()).isEqualTo(otherClientWithSameTarget.hashCode()); } @Test public void shouldOverrideToString() { IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client.toString()) .isEqualTo("HardCodedTarget(type=IcecreamServiceApi, " + "url=http://localhost:" + wireMockRule.port() + ")"); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/LoggerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; import org.assertj.core.api.Condition; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import reactivefeign.client.LoggerReactiveHttpClient; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.Bill; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.OrderGenerator; import reactor.core.publisher.Mono; import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; /** * @author Sergii Karpenko */ abstract public class LoggerTest { public static final String LOGGER_NAME = LoggerReactiveHttpClient.class.getName(); @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig() .asynchronousResponseEnabled(true) .dynamicPort()); abstract protected ReactiveFeign.Builder builder(); protected Appender appender; @Test public void shouldLog() throws Exception { Level originalLevel = setLogLevel(Level.TRACE); IceCreamOrder order = new OrderGenerator().generate(20); Bill billExpected = Bill.makeBill(order); wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(billExpected)))); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Mono billMono = client.makeOrder(order); // no logs before subscription ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(LogEvent.class); Mockito.verify(appender, never()).append(argumentCaptor.capture()); billMono.block(); Mockito.verify(appender, times(7)).append(argumentCaptor.capture()); List logEvents = argumentCaptor.getAllValues(); assertLogEvent(logEvents, 0, Level.DEBUG, "[IcecreamServiceApi#makeOrder]--->POST http://localhost"); assertLogEvent(logEvents, 1, Level.TRACE, "[IcecreamServiceApi#makeOrder] REQUEST HEADERS\n" + "Accept:[application/json]"); assertLogEvent(logEvents, 2, Level.TRACE, "[IcecreamServiceApi#makeOrder] REQUEST BODY\n" + "IceCreamOrder{ id=20, balls="); assertLogEvent(logEvents, 3, Level.TRACE, "[IcecreamServiceApi#makeOrder] RESPONSE HEADERS", "Content-Type:application/json"); assertLogEvent(logEvents, 4, Level.DEBUG, "[IcecreamServiceApi#makeOrder]<--- headers takes"); assertLogEvent(logEvents, 5, Level.TRACE, "[IcecreamServiceApi#makeOrder] RESPONSE BODY\n" + "reactivefeign.testcase.domain.Bill"); assertLogEvent(logEvents, 6, Level.DEBUG, "[IcecreamServiceApi#makeOrder]<--- body takes"); setLogLevel(originalLevel); } private void assertLogEvent(List events, int index, Level level, String message) { assertThat(events).element(index) .hasFieldOrPropertyWithValue("level", level) .extracting("message") .extractingResultOf("getFormattedMessage") .have(new Condition<>(o -> ((String) o).contains(message), "check message")); } private void assertLogEvent(List events, int index, Level level, String message1, String message2) { assertThat(events).element(index) .hasFieldOrPropertyWithValue("level", level) .extracting("message") .extractingResultOf("getFormattedMessage") .have(new Condition<>(o -> ((String) o).contains(message1), "check message1")) .have(new Condition<>(o -> ((String) o).contains(message2), "check message2"));; } @Before public void before() { appender = Mockito.mock(Appender.class); when(appender.getName()).thenReturn("TestAppender"); when(appender.isStarted()).thenReturn(true); getLoggerConfig().addAppender(appender, Level.ALL, null); } private static Level setLogLevel(Level logLevel) { LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); Configuration configuration = loggerContext.getConfiguration(); LoggerConfig loggerConfig = configuration.getLoggerConfig(LOGGER_NAME); Level previousLevel = loggerConfig.getLevel(); loggerConfig.setLevel(logLevel); loggerContext.updateLoggers(); return previousLevel; } private static LoggerConfig getLoggerConfig() { LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); Configuration configuration = loggerContext.getConfiguration(); configuration.addLogger(LOGGER_NAME, new LoggerConfig()); return configuration.getLoggerConfig(LOGGER_NAME); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/NotFoundTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.testcase.IcecreamServiceApi; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; /** * @author Sergii Karpenko */ public abstract class NotFoundTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); abstract protected ReactiveFeign.Builder builder(); @Test public void shouldReturnEmptyMono() { String orderUrl = "/icecream/orders/2"; wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_NOT_FOUND))); IcecreamServiceApi client = builder() .decode404() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findOrder(2)) .expectNextCount(0) .verifyComplete(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/ReactivityTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.awaitility.Duration; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.OrderGenerator; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.awaitility.Awaitility.waitAtMost; /** * @author Sergii Karpenko */ abstract public class ReactivityTest { public static final int DELAY_IN_MILLIS = 500; public static final int CALLS_NUMBER = 500; public static final int REACTIVE_GAIN_RATIO = 10; @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig() .asynchronousResponseEnabled(true) .dynamicPort()); abstract protected ReactiveFeign.Builder builder(); @Test public void shouldRunReactively() throws JsonProcessingException { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(orderStr) .withFixedDelay(DELAY_IN_MILLIS))); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); AtomicInteger counter = new AtomicInteger(); new Thread(() -> { for (int i = 0; i < CALLS_NUMBER; i++) { client.findFirstOrder() .doOnNext(order -> counter.incrementAndGet()) .subscribe(); } }).start(); waitAtMost(new Duration(timeToCompleteReactively(), TimeUnit.MILLISECONDS)) .until(() -> counter.get() == CALLS_NUMBER); } public static int timeToCompleteReactively() { return CALLS_NUMBER * DELAY_IN_MILLIS / REACTIVE_GAIN_RATIO; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/ReadTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.client.ReadTimeoutException; import reactivefeign.testcase.IcecreamServiceApi; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; /** * @author Sergii Karpenko */ abstract public class ReadTimeoutTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); @Test public void shouldFailOnReadTimeout() { String orderUrl = "/icecream/orders/1"; wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withFixedDelay(200))); IcecreamServiceApi client = builder( new ReactiveOptions.Builder() .setConnectTimeoutMillis(300) .setReadTimeoutMillis(100) .build()) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findOrder(1)) .expectError(ReadTimeoutException.class) .verify(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/RequestInterceptorTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.FeignException; import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.OrderGenerator; import reactivefeign.utils.Pair; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.util.Collections.singletonList; import static reactivefeign.TestUtils.equalsComparingFieldByFieldRecursively; /** * @author Sergii Karpenko */ abstract public class RequestInterceptorTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); abstract protected ReactiveFeign.Builder builder(); @Test public void shouldInterceptRequestAndSetAuthHeader() throws JsonProcessingException { String orderUrl = "/icecream/orders/1"; IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_UNAUTHORIZED))) .setPriority(100); wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .withHeader("Authorization", equalTo("Bearer mytoken123")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(orderStr))) .setPriority(1); IcecreamServiceApi clientWithoutAuth = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(clientWithoutAuth.findFirstOrder()) .expectError(notAuthorizedException()) .verify(); IcecreamServiceApi clientWithAuth = builder() .addHeaders(singletonList(new Pair<>("Authorization", "Bearer mytoken123"))) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(clientWithAuth.findFirstOrder()) .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) .expectComplete(); } protected Class notAuthorizedException() { return FeignException.class; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/RetryingTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.RetryableException; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.publisher.RetryPublisherHttpClient; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.Mixin; import reactivefeign.testcase.domain.OrderGenerator; import reactor.test.StepVerifier; import java.util.Arrays; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static org.apache.http.HttpHeaders.RETRY_AFTER; import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.isA; import static reactivefeign.TestUtils.equalsComparingFieldByFieldRecursively; /** * @author Sergii Karpenko */ public abstract class RetryingTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); abstract protected ReactiveFeign.Builder builder(); @Before public void resetServers() { wireMockRule.resetAll(); } @Test public void shouldSuccessOnRetriesMono() throws JsonProcessingException { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success", "/icecream/orders/1", aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"), aResponse().withStatus(200).withHeader("Content-Type", "application/json") .withBody(orderStr)); IcecreamServiceApi client = builder() .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0)) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findOrder(1)) .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) .verifyComplete(); } @Test public void shouldSuccessOnRetriesFlux() throws JsonProcessingException { String mixinsStr = TestUtils.MAPPER.writeValueAsString(Mixin.values()); mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success", "/icecream/mixins", aResponse().withStatus(SC_SERVICE_UNAVAILABLE).withHeader(RETRY_AFTER, "1"), aResponse().withStatus(SC_OK) .withHeader("Content-Type", "application/json") .withBody(mixinsStr)); IcecreamServiceApi client = builder() .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0)) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.getAvailableMixins()) .expectNextSequence(Arrays.asList(Mixin.values())) .verifyComplete(); } @Test public void shouldSuccessOnRetriesWoRetryAfter() throws JsonProcessingException { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success", "/icecream/orders/1", aResponse().withStatus(SC_SERVICE_UNAVAILABLE), aResponse().withStatus(SC_OK) .withHeader("Content-Type", "application/json") .withBody(orderStr)); IcecreamServiceApi client = builder() .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0)) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findOrder(1)) .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) .verifyComplete(); } private static void mockResponseAfterSeveralAttempts(WireMockClassRule rule, int failedAttemptsNo, String scenario, String url, ResponseDefinitionBuilder failResponse, ResponseDefinitionBuilder response) { String state = STARTED; for (int attempt = 0; attempt < failedAttemptsNo; attempt++) { String nextState = "attempt" + attempt; rule.stubFor( get(urlEqualTo(url)) .inScenario(scenario).whenScenarioStateIs(state) .willReturn(failResponse).willSetStateTo(nextState)); state = nextState; } rule.stubFor(get(urlEqualTo(url)) .inScenario(scenario) .whenScenarioStateIs(state).willReturn(response)); } @Test public void shouldFailAsNoMoreRetries() { String orderUrl = "/icecream/orders/1"; wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"))); IcecreamServiceApi client = builder() .retryWhen(ReactiveRetryers.retry(3)) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findOrder(1)) .expectErrorMatches(throwable -> throwable instanceof RetryPublisherHttpClient.OutOfRetriesException && hasProperty("cause", isA(RetryableException.class)).matches(throwable)) .verify(); } @Test public void shouldFailAsNoMoreRetriesWithBackoff() { String orderUrl = "/icecream/orders/1"; wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"))); IcecreamServiceApi client = builder() .retryWhen(ReactiveRetryers.retryWithBackoff(7, 5)) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findOrder(1)) .expectErrorMatches(throwable -> throwable instanceof RetryPublisherHttpClient.OutOfRetriesException && hasProperty("cause", isA(RetryableException.class)).matches(throwable)) .verify(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/SmokeTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import reactivefeign.testcase.IcecreamServiceApi; import reactivefeign.testcase.domain.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.Map; import java.util.stream.Collectors; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.util.Arrays.asList; import static reactivefeign.TestUtils.equalsComparingFieldByFieldRecursively; /** * @author Sergii Karpenko */ abstract public class SmokeTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); @Before public void resetServers() { wireMockRule.resetAll(); } abstract protected ReactiveFeign.Builder builder(); private IcecreamServiceApi client; private OrderGenerator generator = new OrderGenerator(); private Map orders = generator.generateRange(10).stream() .collect(Collectors.toMap(IceCreamOrder::getId, o -> o)); @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { String targetUrl = "http://localhost:" + wireMockRule.port(); client = builder() .decode404() .target(IcecreamServiceApi.class, targetUrl); } @Test public void testSimpleGet_success() throws JsonProcessingException { wireMockRule.stubFor(get(urlEqualTo("/icecream/flavors")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(Flavor.values())))); wireMockRule.stubFor(get(urlEqualTo("/icecream/mixins")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(Mixin.values())))); Flux flavors = client.getAvailableFlavors(); Flux mixins = client.getAvailableMixins(); StepVerifier.create(flavors) .expectNextSequence(asList(Flavor.values())) .verifyComplete(); StepVerifier.create(mixins) .expectNextSequence(asList(Mixin.values())) .verifyComplete(); } @Test public void testFindOrder_success() throws JsonProcessingException { IceCreamOrder orderExpected = orders.get(1); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(orderExpected)))); Mono order = client.findOrder(1); StepVerifier.create(order) .expectNextMatches(equalsComparingFieldByFieldRecursively(orderExpected)) .verifyComplete(); } @Test public void testFindOrder_empty() { Mono orderEmpty = client.findOrder(123); StepVerifier.create(orderEmpty) .expectNextCount(0) .verifyComplete(); } @Test public void testMakeOrder_success() throws JsonProcessingException { IceCreamOrder order = new OrderGenerator().generate(20); Bill billExpected = Bill.makeBill(order); wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(billExpected)))); Mono bill = client.makeOrder(order); StepVerifier.create(bill) .expectNextMatches(equalsComparingFieldByFieldRecursively(billExpected)) .verifyComplete(); } @Test public void testPayBill_success() throws JsonProcessingException { Bill bill = Bill.makeBill(new OrderGenerator().generate(30)); wireMockRule.stubFor(post(urlEqualTo("/icecream/bills/pay")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(bill))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json"))); Mono result = client.payBill(bill); StepVerifier.create(result) .expectNextCount(0) .verifyComplete(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/StatusHandlerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.RetryableException; import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.testcase.IcecreamServiceApi; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static reactivefeign.client.statushandler.CompositeStatusHandler.compose; import static reactivefeign.client.statushandler.ReactiveStatusHandlers.throwOnStatus; /** * @author Sergii Karpenko */ public abstract class StatusHandlerTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); abstract protected ReactiveFeign.Builder builder(); @Before public void resetServers() { wireMockRule.resetAll(); } @Test public void shouldThrowRetryException() { wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))); IcecreamServiceApi client = builder() .statusHandler(throwOnStatus( status -> status == HttpStatus.SC_SERVICE_UNAVAILABLE, (methodTag, response) -> new RetryableException("Should retry on next node", null))) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findFirstOrder()) .expectError(RetryableException.class); } @Test public void shouldThrowOnStatusCode() { wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/2")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_UNAUTHORIZED))); IcecreamServiceApi client = builder() .statusHandler(compose( throwOnStatus( status -> status == HttpStatus.SC_SERVICE_UNAVAILABLE, (methodTag, response) -> new RetryableException("Should retry on next node", null)), throwOnStatus( status -> status == HttpStatus.SC_UNAUTHORIZED, (methodTag, response) -> new RuntimeException("Should login", null)))) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); StepVerifier.create(client.findFirstOrder()) .expectError(RetryableException.class) .verify(); StepVerifier.create(client.findOrder(2)) .expectError(RuntimeException.class) .verify(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/TestUtils.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.util.function.Predicate; /** * Helper methods for tests. */ class TestUtils { static final ObjectMapper MAPPER; static { MAPPER = new ObjectMapper(); MAPPER.registerModule(new JavaTimeModule()); } public static Predicate equalsComparingFieldByFieldRecursively(T rhs) { return lhs -> { try { return MAPPER.writeValueAsString(lhs).equals(MAPPER.writeValueAsString(rhs)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/allfeatures/AllFeaturesApi.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.allfeatures; import feign.*; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; import java.util.Map; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; @Headers({ "Accept: application/json" }) public interface AllFeaturesApi { @RequestLine("GET /mirrorParameters/{parameterInPathPlaceholder}?paramInUrl={paramInQueryPlaceholder}") Mono> mirrorParameters( @Param("parameterInPathPlaceholder") long paramInPath, @Param("paramInQueryPlaceholder") long paramInQuery, @QueryMap Map paramMap); @RequestLine("GET /mirrorParametersNew?paramInUrl={paramInUrlPlaceholder}") Mono> mirrorParametersNew( @Param("paramInUrlPlaceholder") long paramInUrl, @Param("dynamicParam") long dynamicParam, @QueryMap Map paramMap); @RequestLine("GET /mirrorHeaders") @Headers({ "Method-Header: {headerValue}" }) Mono> mirrorHeaders(@Param("headerValue") long param, @HeaderMap Map paramMap); @RequestLine("POST " + "/mirrorBody") Mono mirrorBody(String body); @RequestLine("POST " + "/mirrorBodyMap") @Headers({ "Content-Type: application/json" }) Mono> mirrorBodyMap(Map body); @RequestLine("POST " + "/mirrorBodyReactive") @Headers({ "Content-Type: application/json" }) Mono mirrorBodyReactive(Publisher body); @RequestLine("POST " + "/mirrorBodyMapReactive") @Headers({ "Content-Type: application/json" }) Mono> mirrorBodyMapReactive(Publisher> body); @RequestLine("POST " + "/mirrorBodyStream") @Headers({ "Content-Type: "+APPLICATION_STREAM_JSON_VALUE, "Accept: "+APPLICATION_STREAM_JSON_VALUE}) Flux mirrorBodyStream(Publisher bodyStream); @RequestLine("POST " + "/mirrorIntegerBodyStream") @Headers({ "Content-Type: "+APPLICATION_STREAM_JSON_VALUE, "Accept: "+APPLICATION_STREAM_JSON_VALUE}) Flux mirrorIntegerBodyStream(Flux body); @RequestLine("POST " + "/mirrorStringBodyStream") @Headers({ "Content-Type: "+TEXT_EVENT_STREAM_VALUE, "Accept: "+TEXT_EVENT_STREAM_VALUE}) Flux mirrorStringBodyStream(Flux body); @RequestLine("GET /empty") @Headers({ "Method-Header: {headerValue}" }) Mono empty(); @RequestLine("POST " + "/mirrorBodyWithDelay") Mono mirrorBodyWithDelay(String body); @RequestLine("POST " + "/mirrorStreamingBinaryBodyReactive") @Headers({ "Content-Type: "+APPLICATION_OCTET_STREAM_VALUE }) Flux mirrorStreamingBinaryBodyReactive(Publisher body); default Mono mirrorDefaultBody() { return mirrorBody("default"); } class TestObject { public String payload; public TestObject() { } public TestObject(String payload) { this.payload = payload; } } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/allfeatures/AllFeaturesController.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.allfeatures; import org.reactivestreams.Publisher; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; import java.time.Duration; import java.util.Map; import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON_VALUE; import static reactor.core.publisher.Mono.just; @RestController public class AllFeaturesController implements AllFeaturesApi { @GetMapping(path = "/mirrorParameters/{paramInPath}") @Override public Mono> mirrorParameters( @PathVariable("paramInPath") long paramInPath, @RequestParam("paramInUrl") long paramInUrl, @RequestParam Map paramMap) { paramMap.put("paramInPath", Long.toString(paramInPath)); paramMap.put("paramInUrl", Long.toString(paramInUrl)); return just(paramMap); } @GetMapping(path = "/mirrorParametersNew") @Override public Mono> mirrorParametersNew( @RequestParam("paramInUrl") long paramInUrl, @RequestParam("dynamicParam") long dynamicParam, @RequestParam Map paramMap) { paramMap.put("paramInUrl", Long.toString(paramInUrl)); paramMap.put("dynamicParam", Long.toString(dynamicParam)); return just(paramMap); } @GetMapping(path = "/mirrorHeaders") @Override public Mono> mirrorHeaders( @RequestHeader("Method-Header") long param, @RequestHeader Map headersMap) { return just(headersMap); } @PostMapping(path = "/mirrorBody") @Override public Mono mirrorBody(@RequestBody String body) { return just(body); } @PostMapping(path = "/mirrorBodyMap") @Override public Mono> mirrorBodyMap( @RequestBody Map body) { return just(body); } @PostMapping(path = "/mirrorBodyReactive") @Override public Mono mirrorBodyReactive(@RequestBody Publisher body) { return Mono.from(body); } @PostMapping(path = "/mirrorBodyMapReactive") @Override public Mono> mirrorBodyMapReactive( @RequestBody Publisher> body) { return Mono.from(body); } @PostMapping(path = "/mirrorBodyStream") @Override public Flux mirrorBodyStream( @RequestBody Publisher bodyStream) { return Flux.from(bodyStream); } @PostMapping(path = "/mirrorIntegerBodyStream") @Override public Flux mirrorIntegerBodyStream( @RequestBody Flux body){ return body; } @PostMapping(path = "/mirrorStringBodyStream") @Override public Flux mirrorStringBodyStream( @RequestBody Flux body){ return body; } @PostMapping(path = "/mirrorBodyWithDelay") @Override public Mono mirrorBodyWithDelay(@RequestBody String body) { return just(body).delayElement(Duration.ofMillis(500)); } @Override public Mono empty() { return Mono.empty(); } @PostMapping(path = "/mirrorStreamingBinaryBodyReactive") @Override public Flux mirrorStreamingBinaryBodyReactive(@RequestBody Publisher body) { return Flux.from(body); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/allfeatures/AllFeaturesTest.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.allfeatures; import org.awaitility.Duration; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit4.SpringRunner; import reactivefeign.ReactiveFeign; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import static java.nio.ByteBuffer.wrap; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.waitAtMost; import static reactivefeign.ReactivityTest.CALLS_NUMBER; import static reactivefeign.ReactivityTest.timeToCompleteReactively; import static reactor.core.publisher.Flux.empty; import static reactor.core.publisher.Mono.fromFuture; import static reactor.core.publisher.Mono.just; /** * @author Sergii Karpenko * * Tests ReactiveFeign in conjunction with WebFlux rest controller. */ @RunWith(SpringRunner.class) @SpringBootTest( properties = {"spring.main.web-application-type=reactive"}, classes = {AllFeaturesController.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @EnableAutoConfiguration(exclude = {ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class}) abstract public class AllFeaturesTest { private AllFeaturesApi client; @LocalServerPort private int port; @Rule public ExpectedException expectedException = ExpectedException.none(); abstract protected ReactiveFeign.Builder builder(); @Before public void setUp() { client = builder() .decode404() .target(AllFeaturesApi.class, "http://localhost:" + port); } @Test public void shouldReturnAllPassedParameters() { Map paramMap = new HashMap() { { put("paramKey", "paramValue"); } }; Map returned = client.mirrorParameters(555,777, paramMap).block(); assertThat(returned).containsEntry("paramInPath", "555"); assertThat(returned).containsEntry("paramInUrl", "777"); assertThat(returned).containsAllEntriesOf(paramMap); } @Test public void shouldReturnAllPassedParametersNew() { Map paramMap = new HashMap() { { put("paramKey", "paramValue"); } }; Map returned = client.mirrorParametersNew(777, 888, paramMap) .block(); assertThat(returned).containsEntry("paramInUrl", "777"); assertThat(returned).containsEntry("dynamicParam", "888"); assertThat(returned).containsAllEntriesOf(paramMap); } @Test public void shouldReturnAllPassedHeaders() { Map headersMap = new HashMap() { { put("headerKey1", "headerValue1"); put("headerKey2", "headerValue2"); } }; Map returned = client.mirrorHeaders(777, headersMap).block(); assertThat(returned).containsEntry("Method-Header", "777"); assertThat(returned).containsAllEntriesOf(headersMap); assertThat(returned).containsKey("Accept"); } @Test public void shouldReturnBody() { String returned = client.mirrorBody("Test Body").block(); assertThat(returned).isEqualTo("Test Body"); } @Test public void shouldReturnBodyMap() { Map bodyMap = new HashMap() { { put("key1", "value1"); put("key2", "value2"); } }; Map returned = client.mirrorBodyMap(bodyMap).block(); assertThat(returned).containsAllEntriesOf(bodyMap); } @Test public void shouldReturnBodyReactive() { String returned = client.mirrorBodyReactive(just("Test Body")).block(); assertThat(returned).isEqualTo("Test Body"); } @Test public void shouldReturnBodyMapReactive() { Map bodyMap = new HashMap() { { put("key1", "value1"); put("key2", "value2"); } }; Mono> publisher = client.mirrorBodyMapReactive(just(bodyMap)); StepVerifier.create(publisher) .consumeNextWith(map -> assertThat(map).containsAllEntriesOf(bodyMap)) .verifyComplete(); } @Test public void shouldReturnFirstResultBeforeSecondSent() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); AtomicInteger sentCount = new AtomicInteger(); AtomicInteger receivedCount = new AtomicInteger(); CompletableFuture firstReceived = new CompletableFuture<>(); Flux returned = client .mirrorBodyStream(Flux.just(new AllFeaturesApi.TestObject("testMessage1"), new AllFeaturesApi.TestObject("testMessage2")) .delayUntil(testObject -> sentCount.get() == 1 ? fromFuture(firstReceived) : empty()) .doOnNext(sent -> sentCount.incrementAndGet()) ); returned.doOnNext(received -> { receivedCount.incrementAndGet(); firstReceived.complete(received); countDownLatch.countDown(); }).subscribe(); countDownLatch.await(); } @Test public void shouldReturnEmpty() { Optional returned = client.empty().blockOptional(); assertThat(!returned.isPresent()); } @Test public void shouldReturnDefaultBody() { String returned = client.mirrorDefaultBody().block(); assertThat(returned).isEqualTo("default"); } @Test public void shouldRunReactively() { AtomicInteger counter = new AtomicInteger(); for (int i = 0; i < CALLS_NUMBER; i++) { client.mirrorBodyWithDelay("testBody") .doOnNext(order -> counter.incrementAndGet()) .subscribe(); } waitAtMost(new Duration(timeToCompleteReactively(), TimeUnit.MILLISECONDS)) .until(() -> counter.get() == CALLS_NUMBER); } @Test public void shouldMirrorIntegerStreamBody() { Flux result = client.mirrorIntegerBodyStream( Flux.fromArray(new Integer[]{1, 3, 5, 7})); StepVerifier.create(result) .expectNext(1) .expectNext(3) .expectNext(5) .expectNext(7) .verifyComplete(); } @Test public void shouldMirrorStringStreamBody() { Flux result = client.mirrorStringBodyStream( Flux.fromArray(new String[]{"a", "b", "c"})); StepVerifier.create(result) .expectNext("a") .expectNext("b") .expectNext("c") .verifyComplete(); } @Test public void shouldMirrorStreamingBinaryBodyReactive() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); AtomicInteger sentCount = new AtomicInteger(); ConcurrentLinkedQueue receivedAll = new ConcurrentLinkedQueue<>(); CompletableFuture firstReceived = new CompletableFuture<>(); Flux returned = client .mirrorStreamingBinaryBodyReactive(Flux.just( fromByteArray(new byte[]{1,2,3}), fromByteArray(new byte[]{4,5,6}))) .delayUntil(testObject -> sentCount.get() == 1 ? fromFuture(firstReceived) : empty()) .doOnNext(sent -> sentCount.incrementAndGet()); returned.doOnNext(received -> { byte[] dataReceived = new byte[received.limit()]; received.get(dataReceived); receivedAll.add(dataReceived); assertThat(receivedAll.size()).isEqualTo(sentCount.get()); firstReceived.complete(received); countDownLatch.countDown(); }).subscribe(); countDownLatch.await(); assertThat(receivedAll).containsExactly(new byte[]{1,2,3}, new byte[]{4,5,6}); } private static ByteBuffer fromByteArray(byte[] data){ return ByteBuffer.wrap(data); } @Configuration public static class TestConfiguration{ @Bean public ReactiveWebServerFactory reactiveWebServerFactory(){ return new NettyReactiveWebServerFactory(); } } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/CompressionTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class CompressionTest extends reactivefeign.CompressionTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return RestTemplateFakeReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/ConnectionTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class ConnectionTimeoutTest extends reactivefeign.ConnectionTimeoutTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return RestTemplateFakeReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/ContractTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; /** * @author Sergii Karpenko */ public class ContractTest extends reactivefeign.ContractTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/DefaultMethodTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class DefaultMethodTest extends reactivefeign.DefaultMethodTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } @Override protected ReactiveFeign.Builder builder(Class apiClass) { return RestTemplateFakeReactiveFeign.builder(); } @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return RestTemplateFakeReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/LoggerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class LoggerTest extends reactivefeign.LoggerTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/NotFoundTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class NotFoundTest extends reactivefeign.NotFoundTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/ReactivityTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import com.fasterxml.jackson.core.JsonProcessingException; import org.awaitility.core.ConditionTimeoutException; import org.junit.Before; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; public class ReactivityTest extends reactivefeign.ReactivityTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } @Test(expected = ConditionTimeoutException.class) @Override public void shouldRunReactively() throws JsonProcessingException { super.shouldRunReactively(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/ReadTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class ReadTimeoutTest extends reactivefeign.ReadTimeoutTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return RestTemplateFakeReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/RequestInterceptorTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class RequestInterceptorTest extends reactivefeign.RequestInterceptorTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/RetryingTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class RetryingTest extends reactivefeign.RetryingTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/SmokeTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class SmokeTest extends reactivefeign.SmokeTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/StatusHandlerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate; import reactivefeign.ReactiveFeign; import reactivefeign.resttemplate.client.RestTemplateFakeReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class StatusHandlerTest extends reactivefeign.StatusHandlerTest { @Override protected ReactiveFeign.Builder builder() { return RestTemplateFakeReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/client/RestTemplateFakeReactiveFeign.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate.client; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import static java.util.Optional.ofNullable; /** * {@link RestTemplate} based implementation * * @author Sergii Karpenko */ public class RestTemplateFakeReactiveFeign { public static ReactiveFeign.Builder builder() { return new ReactiveFeign.Builder(){ { clientFactory(methodMetadata -> new RestTemplateFakeReactiveHttpClient( methodMetadata, new RestTemplate(), false)); } @Override public ReactiveFeign.Builder options(ReactiveOptions options) { HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( HttpClientBuilder.create().build()); if (options.getConnectTimeoutMillis() != null) { requestFactory.setConnectTimeout(options.getConnectTimeoutMillis().intValue()); } if (options.getReadTimeoutMillis() != null) { requestFactory.setReadTimeout(options.getReadTimeoutMillis().intValue()); } this.clientFactory(methodMetadata -> { boolean acceptGzip = ofNullable(options.isTryUseCompression()).orElse(false); return new RestTemplateFakeReactiveHttpClient( methodMetadata, new RestTemplate(requestFactory), acceptGzip); }); return this; } }; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/resttemplate/client/RestTemplateFakeReactiveHttpClient.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.resttemplate.client; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import feign.MethodMetadata; import org.reactivestreams.Publisher; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.client.ReactiveHttpResponse; import reactivefeign.client.ReadTimeoutException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.SocketTimeoutException; import java.util.List; import java.util.Map; import static feign.Util.resolveLastTypeParameter; import static org.springframework.core.ParameterizedTypeReference.forType; public class RestTemplateFakeReactiveHttpClient implements ReactiveHttpClient { private final RestTemplate restTemplate; private final boolean acceptGzip; private final Type returnPublisherType; private final ParameterizedTypeReference returnActualType; RestTemplateFakeReactiveHttpClient(MethodMetadata methodMetadata, RestTemplate restTemplate, boolean acceptGzip) { this.restTemplate = restTemplate; this.acceptGzip = acceptGzip; ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(mapper); restTemplate.getMessageConverters().add(0, converter); final Type returnType = methodMetadata.returnType(); returnPublisherType = ((ParameterizedType) returnType).getRawType(); returnActualType = forType( resolveLastTypeParameter(returnType, (Class) returnPublisherType)); } @Override public Mono executeRequest(ReactiveHttpRequest request) { Mono bodyMono; if (request.body() instanceof Mono) { bodyMono = ((Mono) request.body()); } else if (request.body() instanceof Flux) { bodyMono = ((Flux) request.body()).collectList(); } else { bodyMono = Mono.just(request.body()); } bodyMono = bodyMono.switchIfEmpty(Mono.just(new byte[0])); return bodyMono.flatMap(body -> { MultiValueMap headers = new LinkedMultiValueMap<>(request.headers()); if (acceptGzip) { headers.add("Accept-Encoding", "gzip"); } ResponseEntity response = restTemplate.exchange(request.uri().toString(), HttpMethod.valueOf(request.method()), new HttpEntity<>(body, headers), responseType()); return Mono.just(new FakeReactiveHttpResponse(response, returnPublisherType)); }) .onErrorMap(ex -> ex instanceof ResourceAccessException && ex.getCause() instanceof SocketTimeoutException, ReadTimeoutException::new) .onErrorResume(HttpStatusCodeException.class, ex -> Mono.just(new ErrorReactiveHttpResponse(ex))); } private ParameterizedTypeReference responseType(){ if (returnPublisherType == Mono.class) { return returnActualType; } else { return forType(new ParameterizedType() { @Override public Type[] getActualTypeArguments() { return new Type[] {returnActualType.getType()}; } @Override public Type getRawType() { return List.class; } @Override public Type getOwnerType() { return null; } }); } } private static class FakeReactiveHttpResponse implements ReactiveHttpResponse{ private final ResponseEntity response; private final Type returnPublisherType; private FakeReactiveHttpResponse(ResponseEntity response, Type returnPublisherType) { this.response = response; this.returnPublisherType = returnPublisherType; } @Override public int status() { return response.getStatusCodeValue(); } @Override public Map> headers() { return response.getHeaders(); } @Override public Publisher body() { if (returnPublisherType == Mono.class) { return Mono.justOrEmpty(response.getBody()); } else { return Flux.fromIterable((List)response.getBody()); } } @Override public Mono bodyData() { return Mono.just(new byte[0]); } } private static class ErrorReactiveHttpResponse implements ReactiveHttpResponse{ private final HttpStatusCodeException ex; private ErrorReactiveHttpResponse(HttpStatusCodeException ex) { this.ex = ex; } @Override public int status() { return ex.getStatusCode().value(); } @Override public Map> headers() { return ex.getResponseHeaders(); } @Override public Publisher body() { return Mono.empty(); } @Override public Mono bodyData() { return Mono.just(ex.getResponseBodyAsByteArray()); } } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/IcecreamServiceApi.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase; import feign.Headers; import feign.Param; import feign.RequestLine; import reactivefeign.testcase.domain.Bill; import reactivefeign.testcase.domain.Flavor; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.Mixin; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * API of an iceream web service. * * @author Sergii Karpenko */ @Headers({"Accept: application/json"}) public interface IcecreamServiceApi { RuntimeException RUNTIME_EXCEPTION = new RuntimeException("tests exception"); @RequestLine("GET /icecream/flavors") Flux getAvailableFlavors(); @RequestLine("GET /icecream/mixins") Flux getAvailableMixins(); @RequestLine("POST /icecream/orders") @Headers("Content-Type: application/json") Mono makeOrder(IceCreamOrder order); @RequestLine("GET /icecream/orders/{orderId}") Mono findOrder(@Param("orderId") int orderId); @RequestLine("POST /icecream/bills/pay") @Headers("Content-Type: application/json") Mono payBill(Bill bill); default Mono findFirstOrder() { return findOrder(1); } default Mono throwsException() { throw RUNTIME_EXCEPTION; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/IcecreamServiceApiBroken.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase; import feign.Headers; import feign.Param; import feign.RequestLine; import reactivefeign.ReactiveContract; import reactivefeign.testcase.domain.Bill; import reactivefeign.testcase.domain.Flavor; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.Mixin; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; /** * API of an iceream web service with one method that doesn't returns {@link Mono} or {@link Flux} * and violates {@link ReactiveContract}s rules. * * @author Sergii Karpenko */ public interface IcecreamServiceApiBroken { @RequestLine("GET /icecream/orders/{orderId}") Mono findOrder(@Param("orderId") int orderId); /** * Method that doesn't respects contract. */ @RequestLine("GET /icecream/orders/{orderId}") IceCreamOrder findOrderBlocking(@Param("orderId") int orderId); } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/IcecreamServiceApiBrokenByCopy.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase; import feign.Headers; import feign.Param; import feign.RequestLine; import reactivefeign.ReactiveContract; import reactivefeign.testcase.domain.Bill; import reactivefeign.testcase.domain.Flavor; import reactivefeign.testcase.domain.IceCreamOrder; import reactivefeign.testcase.domain.Mixin; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; import java.util.Collection; /** * API of an iceream web service with one method that returns {@link Mono} or {@link Flux} of byte array * and violates {@link ReactiveContract}s rules. * * @author Sergii Karpenko */ public interface IcecreamServiceApiBrokenByCopy { @RequestLine("GET /icecream/orders/{orderId}") Mono findOrder(@Param("orderId") int orderId); /** * Method that doesn't respects contract. */ @RequestLine("GET /icecream/orders/{orderId}") Mono findOrderCopy(@Param("orderId") int orderId); } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/domain/Bill.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase.domain; import java.util.HashMap; import java.util.Map; /** * Bill for consumed ice cream. */ public class Bill { private static final Map PRICES = new HashMap<>(); static { PRICES.put(1, (float) 2.00); // two euros for one ball (expensive!) PRICES.put(3, (float) 2.85); // 2.85€ for 3 balls PRICES.put(5, (float) 4.30); // 4.30€ for 5 balls PRICES.put(7, (float) 5); // only five euros for seven balls! Wow } private static final float MIXIN_PRICE = (float) 0.6; // price per mixin private Float price; public Bill() {} public Bill(final Float price) { this.price = price; } public Float getPrice() { return price; } public void setPrice(final Float price) { this.price = price; } /** * Makes a bill from an order. * * @param order ice cream order * @return bill */ public static Bill makeBill(final IceCreamOrder order) { int nbBalls = order.getBalls().values().stream().mapToInt(Integer::intValue) .sum(); Float price = PRICES.get(nbBalls) + order.getMixins().size() * MIXIN_PRICE; return new Bill(price); } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/domain/Flavor.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase.domain; /** * Ice cream flavors. */ public enum Flavor { STRAWBERRY, CHOCOLATE, BANANA, PISTACHIO, MELON, VANILLA } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/domain/IceCreamOrder.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase.domain; import java.time.Instant; import java.util.*; /** * Give me some ice-cream! :p */ public class IceCreamOrder { private static Random random = new Random(); private int id; // order id private Map balls; // how much balls of flavor private Set mixins; // and some mixins ... private Instant orderTimestamp; // and give it to me right now ! IceCreamOrder() {} IceCreamOrder(int id) { this(id, Instant.now()); } IceCreamOrder(int id, final Instant orderTimestamp) { this.id = id; this.balls = new HashMap<>(); this.mixins = new HashSet<>(); this.orderTimestamp = orderTimestamp; } IceCreamOrder addBall(final Flavor ballFlavor) { final Integer ballCount = balls.containsKey(ballFlavor) ? balls.get(ballFlavor) + 1 : 1; balls.put(ballFlavor, ballCount); return this; } IceCreamOrder addMixin(final Mixin mixin) { mixins.add(mixin); return this; } IceCreamOrder withOrderTimestamp(final Instant orderTimestamp) { this.orderTimestamp = orderTimestamp; return this; } public int getId() { return id; } public Map getBalls() { return balls; } public Set getMixins() { return mixins; } public Instant getOrderTimestamp() { return orderTimestamp; } @Override public String toString() { return "IceCreamOrder{" + " id=" + id + ", balls=" + balls + ", mixins=" + mixins + ", orderTimestamp=" + orderTimestamp + '}'; } } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/domain/Mixin.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase.domain; /** * Ice cream mix-ins. */ public enum Mixin { COOKIES, MNMS, CHOCOLATE_SIROP, STRAWBERRY_SIROP, NUTS, RAINBOW } ================================================ FILE: feign-reactor-core/src/test/java/reactivefeign/testcase/domain/OrderGenerator.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.testcase.domain; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * Generator of random ice cream orders. */ public class OrderGenerator { private static final int[] BALLS_NUMBER = {1, 3, 5, 7}; private static final int[] MIXIN_NUMBER = {1, 2, 3}; private static final Random random = new Random(); public IceCreamOrder generate(int id) { final IceCreamOrder order = new IceCreamOrder(id); final int nbBalls = peekBallsNumber(); final int nbMixins = peekMixinNumber(); IntStream.rangeClosed(1, nbBalls).mapToObj(i -> this.peekFlavor()) .forEach(order::addBall); IntStream.rangeClosed(1, nbMixins).mapToObj(i -> this.peekMixin()) .forEach(order::addMixin); return order; } public Collection generateRange(int n) { Instant now = Instant.now(); List orderTimestamps = IntStream.range(0, n) .mapToObj(minutes -> now.minus(minutes, ChronoUnit.MINUTES)) .collect(Collectors.toList()); return IntStream.range(0, n) .mapToObj( i -> this.generate(i).withOrderTimestamp(orderTimestamps.get(i))) .collect(Collectors.toList()); } private int peekBallsNumber() { return BALLS_NUMBER[random.nextInt(BALLS_NUMBER.length)]; } private int peekMixinNumber() { return MIXIN_NUMBER[random.nextInt(MIXIN_NUMBER.length)]; } private Flavor peekFlavor() { return Flavor.values()[random.nextInt(Flavor.values().length)]; } private Mixin peekMixin() { return Mixin.values()[random.nextInt(Mixin.values().length)]; } } ================================================ FILE: feign-reactor-jetty/pom.xml ================================================ 4.0.0 io.github.reactivefeign feign-reactor 1.0.0-SNAPSHOT feign-reactor-jetty io.github.reactivefeign feign-reactor-core org.eclipse.jetty jetty-reactive-httpclient io.kptfh.reactivejson json-reactor com.fasterxml.jackson.datatype jackson-datatype-jsr310 io.github.reactivefeign feign-reactor-core 1.0.0-SNAPSHOT test-jar test io.projectreactor reactor-test test org.hamcrest hamcrest-library test com.github.tomakehurst wiremock test org.apache.logging.log4j log4j-slf4j-impl test org.mockito mockito-all test org.assertj assertj-core test org.awaitility awaitility test org.springframework.boot spring-boot-starter-webflux spring-boot-starter-logging org.springframework.boot test org.springframework.boot spring-boot-starter-test test ================================================ FILE: feign-reactor-jetty/src/main/java/reactivefeign/jetty/JettyReactiveFeign.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import com.fasterxml.jackson.core.async_.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.eclipse.jetty.client.HttpClient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.jetty.client.JettyReactiveHttpClient; /** * Reactive Jetty client based implementation of reactive Feign * * @author Sergii Karpenko */ public class JettyReactiveFeign { public static Builder builder() { try { HttpClient httpClient = new HttpClient(); httpClient.start(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); return new Builder<>(httpClient, new JsonFactory(), objectMapper); } catch (Exception e) { throw new RuntimeException(e); } } public static Builder builder(HttpClient httpClient, JsonFactory jsonFactory, ObjectMapper objectMapper) { return new Builder<>(httpClient, jsonFactory, objectMapper); } public static class Builder extends ReactiveFeign.Builder { protected HttpClient httpClient; protected JsonFactory jsonFactory; private ObjectMapper objectMapper; protected ReactiveOptions options; protected Builder(HttpClient httpClient, JsonFactory jsonFactory, ObjectMapper objectMapper) { setHttpClient(httpClient, jsonFactory, objectMapper); this.jsonFactory = jsonFactory; this.objectMapper = objectMapper; } @Override public Builder options(ReactiveOptions options) { if (options.getConnectTimeoutMillis() != null) { httpClient.setConnectTimeout(options.getConnectTimeoutMillis()); } if (options.getReadTimeoutMillis() != null) { setHttpClient(httpClient, jsonFactory, objectMapper); } this.options = options; return this; } protected void setHttpClient(HttpClient httpClient, JsonFactory jsonFactory, ObjectMapper objectMapper){ this.httpClient = httpClient; clientFactory(methodMetadata -> { JettyReactiveHttpClient jettyClient = JettyReactiveHttpClient.jettyClient(methodMetadata, httpClient, jsonFactory, objectMapper); if (options != null && options.getReadTimeoutMillis() != null) { jettyClient.setRequestTimeout(options.getReadTimeoutMillis()); } return jettyClient; }); } } } ================================================ FILE: feign-reactor-jetty/src/main/java/reactivefeign/jetty/client/JettyReactiveHttpClient.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.jetty.client; import com.fasterxml.jackson.core.async_.JsonFactory; import com.fasterxml.jackson.core.util.ByteArrayBuilder; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; import feign.MethodMetadata; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.reactive.client.ContentChunk; import org.eclipse.jetty.reactive.client.ReactiveRequest; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.client.ReactiveHttpResponse; import reactivefeign.client.ReadTimeoutException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.UncheckedIOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.concurrent.TimeUnit; import static feign.Util.resolveLastTypeParameter; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.singletonList; import static org.eclipse.jetty.http.HttpHeader.ACCEPT; import static org.eclipse.jetty.http.HttpHeader.CONTENT_TYPE; import static reactivefeign.jetty.utils.ProxyPostProcessor.postProcess; import static reactivefeign.utils.FeignUtils.getBodyActualType; /** * Uses reactive Jetty client to execute http requests * @author Sergii Karpenko */ public class JettyReactiveHttpClient implements ReactiveHttpClient { public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; public static final String TEXT = "text/plain"; public static final String TEXT_UTF_8 = TEXT+";charset=utf-8"; public static final String APPLICATION_JSON = "application/json"; public static final String APPLICATION_JSON_UTF_8 = APPLICATION_JSON+";charset=utf-8"; public static final String APPLICATION_STREAM_JSON = "application/stream+json"; public static final String APPLICATION_STREAM_JSON_UTF_8 = APPLICATION_STREAM_JSON+";charset=utf-8"; private static final byte[] NEWLINE_SEPARATOR = {'\n'}; private final HttpClient httpClient; private final Class bodyActualClass; private final Class returnPublisherClass; private final Class returnActualClass; private final JsonFactory jsonFactory; private final ObjectWriter bodyWriter; private final ObjectReader responseReader; private long requestTimeout = -1; public static JettyReactiveHttpClient jettyClient( MethodMetadata methodMetadata, HttpClient httpClient, JsonFactory jsonFactory, ObjectMapper objectMapper) { final Type returnType = methodMetadata.returnType(); Class returnPublisherType = (Class)((ParameterizedType) returnType).getRawType(); Class returnActualType = getClass(resolveLastTypeParameter(returnType, returnPublisherType)); Class bodyActualType = getClass(getBodyActualType(methodMetadata.bodyType())); return new JettyReactiveHttpClient(httpClient, bodyActualType, returnPublisherType, returnActualType, jsonFactory, objectMapper.writerFor(bodyActualType), objectMapper.readerFor(returnActualType)); } public JettyReactiveHttpClient(HttpClient httpClient, Class bodyActualClass, Class returnPublisherClass, Class returnActualClass, JsonFactory jsonFactory, ObjectWriter bodyWriter, ObjectReader responseReader) { this.httpClient = httpClient; this.bodyActualClass = bodyActualClass; this.returnPublisherClass = returnPublisherClass; this.returnActualClass = returnActualClass; this.jsonFactory = jsonFactory; this.bodyWriter = bodyWriter; this.responseReader = responseReader; } public JettyReactiveHttpClient setRequestTimeout(long timeoutInMillis){ this.requestTimeout = timeoutInMillis; return this; } @Override public Mono executeRequest(ReactiveHttpRequest request) { Request jettyRequest = httpClient.newRequest(request.uri()).method(request.method()); setUpHeaders(request, jettyRequest.getHeaders()); if(requestTimeout > 0){ jettyRequest.timeout(requestTimeout, TimeUnit.MILLISECONDS); } ReactiveRequest.Builder requestBuilder = ReactiveRequest.newBuilder(jettyRequest); if(bodyActualClass != null){ ReactiveRequest.Content content = provideBody(request); requestBuilder.content(content); jettyRequest.getHeaders().put(CONTENT_TYPE.asString(), singletonList(content.getContentType())); } return Mono.from(requestBuilder.build().response((response, content) -> Mono.just( new JettyReactiveHttpResponse(response.getResponse(), postProcess(content, (contentChunk, throwable) -> { if(throwable != null){ contentChunk.callback.failed(throwable); } else { contentChunk.callback.succeeded(); } }), returnPublisherClass, returnActualClass, jsonFactory, responseReader)) )).onErrorMap(ex -> ex instanceof java.util.concurrent.TimeoutException, ReadTimeoutException::new); } protected void setUpHeaders(ReactiveHttpRequest request, HttpFields httpHeaders) { request.headers().forEach(httpHeaders::put); String acceptHeader; if(CharSequence.class.isAssignableFrom(returnActualClass) && returnPublisherClass == Mono.class){ acceptHeader = TEXT; } else if(returnActualClass == ByteBuffer.class || returnActualClass == byte[].class){ acceptHeader = APPLICATION_OCTET_STREAM; } else if(returnPublisherClass == Mono.class){ acceptHeader = APPLICATION_JSON; } else { acceptHeader = APPLICATION_STREAM_JSON; } httpHeaders.put(ACCEPT.asString(), singletonList(acceptHeader)); } protected ReactiveRequest.Content provideBody(ReactiveHttpRequest request) { Publisher bodyPublisher; String contentType; if(request.body() instanceof Mono){ if(bodyActualClass == ByteBuffer.class){ bodyPublisher = ((Mono)request.body()).map(this::toByteBufferChunk); contentType = APPLICATION_OCTET_STREAM; } else if(bodyActualClass == byte[].class){ bodyPublisher = Flux.from(request.body()).map(this::toByteArrayChunk); contentType = APPLICATION_OCTET_STREAM; } else if (CharSequence.class.isAssignableFrom(bodyActualClass)){ bodyPublisher = Flux.from(request.body()).map(this::toCharSequenceChunk); contentType = TEXT_UTF_8; } else { bodyPublisher = Flux.from(request.body()).map(data -> toJsonChunk(data, false)); contentType = APPLICATION_JSON_UTF_8; } } else { if(bodyActualClass == ByteBuffer.class){ bodyPublisher = Flux.from(request.body()).map(this::toByteBufferChunk); contentType = APPLICATION_OCTET_STREAM; } else if(bodyActualClass == byte[].class){ bodyPublisher = Flux.from(request.body()).map(this::toByteArrayChunk); contentType = APPLICATION_OCTET_STREAM; } else { bodyPublisher = Flux.from(request.body()).map(data -> toJsonChunk(data, true)); contentType = APPLICATION_STREAM_JSON_UTF_8; } } return ReactiveRequest.Content.fromPublisher(bodyPublisher, contentType); } protected ContentChunk toByteBufferChunk(Object data){ return new ContentChunk((ByteBuffer)data); } protected ContentChunk toByteArrayChunk(Object data){ return new ContentChunk(ByteBuffer.wrap((byte[])data)); } protected ContentChunk toCharSequenceChunk(Object data){ CharBuffer charBuffer = CharBuffer.wrap((CharSequence) data); ByteBuffer byteBuffer = UTF_8.encode(charBuffer); return new ContentChunk(byteBuffer); } protected ContentChunk toJsonChunk(Object data, boolean stream){ try { ByteArrayBuilder byteArrayBuilder = new ByteArrayBuilder(); bodyWriter.writeValue(byteArrayBuilder, data); if(stream) { byteArrayBuilder.write(NEWLINE_SEPARATOR); } ByteBuffer buffer = ByteBuffer.wrap(byteArrayBuilder.toByteArray()); return new ContentChunk(buffer); } catch (java.io.IOException e) { throw new UncheckedIOException(e); } } public static Class getClass(Type type){ return (Class)(type instanceof ParameterizedType ? ((ParameterizedType) type).getRawType() : type); } } ================================================ FILE: feign-reactor-jetty/src/main/java/reactivefeign/jetty/client/JettyReactiveHttpResponse.java ================================================ package reactivefeign.jetty.client; import com.fasterxml.jackson.core.async_.JsonFactory; import com.fasterxml.jackson.databind.ObjectReader; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.reactive.client.ContentChunk; import org.reactivestreams.Publisher; import reactivefeign.client.ReactiveHttpResponse; import reactivejson.ReactorObjectReader; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.Optional.ofNullable; import static org.eclipse.jetty.http.HttpHeader.CONTENT_TYPE; class JettyReactiveHttpResponse implements ReactiveHttpResponse{ public static final String CHARSET_DELIMITER = ";charset="; private final Response clientResponse; private final Publisher contentChunks; private final Class returnPublisherType; private Class returnActualClass; private final ObjectReader objectReader; private final JsonFactory jsonFactory; JettyReactiveHttpResponse(Response clientResponse, Publisher contentChunks, Class returnPublisherType, Class returnActualClass, JsonFactory jsonFactory, ObjectReader objectReader) { this.clientResponse = clientResponse; this.contentChunks = contentChunks; this.returnPublisherType = returnPublisherType; this.returnActualClass = returnActualClass; this.objectReader = objectReader; this.jsonFactory = jsonFactory; } @Override public int status() { return clientResponse.getStatus(); } @Override public Map> headers() { return clientResponse.getHeaders().stream() .collect(Collectors.toMap(HttpField::getName, field -> asList(field.getValues()))); } @Override public Publisher body() { ReactorObjectReader reactorObjectReader = new ReactorObjectReader(jsonFactory); Flux content = directContent(); if(returnActualClass == ByteBuffer.class){ return content; } else if(returnActualClass.isAssignableFrom(String.class) && returnPublisherType == Mono.class){ Charset charset = getCharset(); return content.map(byteBuffer -> charset.decode(byteBuffer).toString()); } else { if (returnPublisherType == Mono.class) { return reactorObjectReader.read(content, objectReader); } else if(returnPublisherType == Flux.class){ return reactorObjectReader.readElements(content, objectReader); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + returnPublisherType); } } } private Charset getCharset() { return ofNullable(clientResponse.getHeaders().get(CONTENT_TYPE.asString())) .map(header -> { int pos = header.indexOf(CHARSET_DELIMITER); if(pos >= 0){ return header.substring(pos + CHARSET_DELIMITER.length()); } else { return null; } }) .map(Charset::forName) .orElse(UTF_8); } private Flux directContent() { return Flux.from(contentChunks).map(contentChunk -> contentChunk.buffer.slice()); } @Override public Mono bodyData() { return joinChunks(); } private Mono joinChunks() { return directContent().reduce(new ByteArrayOutputStream(), (baos, byteBuffer) -> { for(int i = byteBuffer.position(), limit = byteBuffer.limit(); i < limit; i++){ baos.write(byteBuffer.get(i)); } return baos; }).map(ByteArrayOutputStream::toByteArray); } } ================================================ FILE: feign-reactor-jetty/src/main/java/reactivefeign/jetty/utils/ProxyPostProcessor.java ================================================ package reactivefeign.jetty.utils; import org.eclipse.jetty.reactive.client.internal.AbstractSingleProcessor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import java.util.function.BiConsumer; public class ProxyPostProcessor extends AbstractSingleProcessor{ private final Publisher publisher; private final BiConsumer postProcessor; private ProxyPostProcessor(Publisher publisher, BiConsumer postProcessor) { this.publisher = publisher; this.postProcessor = postProcessor; } @Override public void onNext(I i) { try { downStreamOnNext(i); postProcessor.accept(i, null); } catch (Throwable err) { postProcessor.accept(i, err); } } @Override public void subscribe(Subscriber s) { publisher.subscribe(this); super.subscribe(s); } public static Publisher postProcess(Publisher publisher, BiConsumer postProcessor){ return new ProxyPostProcessor<>(publisher, postProcessor); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/CompressionTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class CompressionTest extends reactivefeign.CompressionTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return JettyReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/ConnectionTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class ConnectionTimeoutTest extends reactivefeign.ConnectionTimeoutTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return JettyReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/ContractTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; /** * @author Sergii Karpenko */ public class ContractTest extends reactivefeign.ContractTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/DefaultMethodTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class DefaultMethodTest extends reactivefeign.DefaultMethodTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } @Override protected ReactiveFeign.Builder builder(Class apiClass) { return JettyReactiveFeign.builder(); } @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return JettyReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/LoggerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class LoggerTest extends reactivefeign.LoggerTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/NotFoundTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class NotFoundTest extends reactivefeign.NotFoundTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/ReactivityTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import com.fasterxml.jackson.core.JsonProcessingException; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; public class ReactivityTest extends reactivefeign.ReactivityTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } @Override public void shouldRunReactively() throws JsonProcessingException { super.shouldRunReactively(); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/ReadTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class ReadTimeoutTest extends reactivefeign.ReadTimeoutTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return JettyReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/RequestInterceptorTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class RequestInterceptorTest extends reactivefeign.RequestInterceptorTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } @Override protected Class notAuthorizedException() { return org.eclipse.jetty.client.HttpResponseException.class; } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/RetryingTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import org.junit.Ignore; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class RetryingTest extends reactivefeign.RetryingTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } @Test public void shouldFailAsNoMoreRetriesWithBackoff() { } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/SmokeTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class SmokeTest extends reactivefeign.SmokeTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/StatusHandlerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.jetty; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class StatusHandlerTest extends reactivefeign.StatusHandlerTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-jetty/src/test/java/reactivefeign/jetty/allfeatures/AllFeaturesTest.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.jetty.allfeatures; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import reactivefeign.ReactiveFeign; import reactivefeign.jetty.JettyReactiveFeign; /** * @author Sergii Karpenko * * Tests ReactiveFeign in conjunction with WebFlux rest controller. */ @EnableAutoConfiguration(exclude = {org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class}) public class AllFeaturesTest extends reactivefeign.allfeatures.AllFeaturesTest { @Override protected ReactiveFeign.Builder builder() { return JettyReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-jetty/src/test/resources/log4j2.xml ================================================ ================================================ FILE: feign-reactor-rx2/pom.xml ================================================ 4.0.0 io.github.reactivefeign feign-reactor 1.0.0-SNAPSHOT feign-reactor-rx2 io.projectreactor.addons reactor-adapter io.reactivex.rxjava2 rxjava io.github.reactivefeign feign-reactor-webclient org.springframework.boot spring-boot-starter-webflux spring-boot-starter-logging org.springframework.boot org.springframework.boot spring-boot-starter-test test io.projectreactor reactor-test test junit junit test org.assertj assertj-core test com.github.tomakehurst wiremock test org.awaitility awaitility test com.google.guava guava test org.apache.logging.log4j log4j-slf4j-impl test ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/Rx2Contract.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import feign.Contract; import feign.MethodMetadata; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashSet; import java.util.List; import java.util.Set; import static feign.Util.checkNotNull; import static java.util.Arrays.asList; /** * Contract allowing only {@link Mono} and {@link Flux} return type. * * @author Sergii Karpenko */ public class Rx2Contract implements Contract { public static final Set RX2_TYPES = new HashSet<>(asList( Flowable.class, Observable.class, Single.class, Maybe.class)); private final Contract delegate; public Rx2Contract(final Contract delegate) { this.delegate = checkNotNull(delegate, "delegate must not be null"); } @Override public List parseAndValidatateMetadata(final Class targetType) { final List methodsMetadata = this.delegate.parseAndValidatateMetadata(targetType); for (final MethodMetadata metadata : methodsMetadata) { final Type type = metadata.returnType(); if (!isRx2Type(type)) { throw new IllegalArgumentException(String.format( "Method %s of contract %s doesn't returns rx2 types", metadata.configKey(), targetType.getSimpleName())); } } return methodsMetadata; } private boolean isRx2Type(final Type type) { return (type instanceof ParameterizedType) && RX2_TYPES.contains(((ParameterizedType) type).getRawType()); } } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/Rx2ReactiveFeign.java ================================================ package reactivefeign.rx2; import feign.Contract; import feign.InvocationHandlerFactory; import feign.MethodMetadata; import io.reactivex.*; import org.springframework.core.ParameterizedTypeReference; import org.springframework.web.reactive.function.client.WebClient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.ReactiveRetryPolicy; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequestInterceptor; import reactivefeign.methodhandler.MethodHandlerFactory; import reactivefeign.publisher.FluxPublisherHttpClient; import reactivefeign.publisher.MonoPublisherHttpClient; import reactivefeign.publisher.PublisherHttpClient; import reactivefeign.rx2.client.statushandler.Rx2ReactiveStatusHandler; import reactivefeign.rx2.client.statushandler.Rx2StatusHandler; import reactivefeign.rx2.methodhandler.Rx2MethodHandlerFactory; import reactivefeign.utils.Pair; import reactivefeign.webclient.WebReactiveFeign; import reactivefeign.webclient.client.WebReactiveHttpClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import static feign.Util.resolveLastTypeParameter; import static java.util.Optional.ofNullable; import static reactivefeign.utils.FeignUtils.getBodyActualType; import static reactivefeign.utils.FeignUtils.returnPublisherType; /** * @author Sergii Karpenko */ public class Rx2ReactiveFeign extends ReactiveFeign { private Rx2ReactiveFeign(ReactiveFeign.ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { super(targetToHandlersByName, factory); } public static Builder builder() { return new Builder<>(); } public static Builder builder(WebClient webClient) { return new Builder<>(webClient); } public static class Builder extends WebReactiveFeign.Builder { private BackpressureStrategy backpressureStrategy; protected Builder() { super(); } protected Builder(WebClient webClient) { super(webClient); } /** * Used to convert {@link Observable} into {@link Flux} * @param backpressureStrategy */ public void setBackpressureStrategy(BackpressureStrategy backpressureStrategy) { this.backpressureStrategy = backpressureStrategy; } @Override protected MethodHandlerFactory buildReactiveMethodHandlerFactory() { return new Rx2MethodHandlerFactory(buildReactiveClientFactory(), backpressureStrategy); } @Override public Builder contract(final Contract contract) { this.contract = new Rx2Contract(contract); return this; } @Override public ReactiveFeign.Builder addHeaders(List> headers) { super.addHeaders(headers); return this; } @Override public Builder requestInterceptor(ReactiveHttpRequestInterceptor requestInterceptor) { super.requestInterceptor(requestInterceptor); return this; } @Override public Builder decode404() { super.decode404(); return this; } public Builder statusHandler(Rx2StatusHandler statusHandler) { super.statusHandler(new Rx2ReactiveStatusHandler(statusHandler)); return this; } @Override public Builder retryWhen(ReactiveRetryPolicy retryPolicy){ super.retryWhen(retryPolicy); return this; } @Override public Builder options(final ReactiveOptions options) { super.options(options); return this; } protected PublisherHttpClient toPublisher(ReactiveHttpClient reactiveHttpClient, MethodMetadata methodMetadata){ Type returnType = returnPublisherType(methodMetadata); if(returnType == Single.class || returnType == Maybe.class){ return new MonoPublisherHttpClient(reactiveHttpClient); } else if(returnType == Flowable.class || returnType == Observable.class){ return new FluxPublisherHttpClient(reactiveHttpClient); } else { throw new IllegalArgumentException("Unknown returnType: " + returnType); } } @Override protected void setWebClient(WebClient webClient){ this.webClient = webClient; clientFactory(methodMetadata -> webClient(methodMetadata, webClient)); } public static WebReactiveHttpClient webClient(MethodMetadata methodMetadata, WebClient webClient) { final Type returnType = methodMetadata.returnType(); Type returnPublisherType = ((ParameterizedType) returnType).getRawType(); ParameterizedTypeReference returnActualType = ParameterizedTypeReference.forType( resolveLastTypeParameter(returnType, (Class) returnPublisherType)); ParameterizedTypeReference bodyActualType = ofNullable( getBodyActualType(methodMetadata.bodyType())) .map(type -> ParameterizedTypeReference.forType(type)) .orElse(null); return new WebReactiveHttpClient(webClient, bodyActualType, rx2ToReactor(returnPublisherType), returnActualType); } private static Class rx2ToReactor(Type type){ if(type == Flowable.class){ return Flux.class; } else if(type == Observable.class){ return Flux.class; } else if(type == Single.class){ return Mono.class; } else if(type == Maybe.class){ return Mono.class; } else { throw new IllegalArgumentException("Unexpected type="+type); } } } } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/client/statushandler/Rx2ReactiveStatusHandler.java ================================================ package reactivefeign.rx2.client.statushandler; import reactivefeign.client.ReactiveHttpResponse; import reactivefeign.client.statushandler.ReactiveStatusHandler; import reactor.core.publisher.Mono; import static reactor.adapter.rxjava.RxJava2Adapter.singleToMono; public class Rx2ReactiveStatusHandler implements ReactiveStatusHandler { private final Rx2StatusHandler statusHandler; public Rx2ReactiveStatusHandler(Rx2StatusHandler statusHandler) { this.statusHandler = statusHandler; } @Override public boolean shouldHandle(int status) { return statusHandler.shouldHandle(status); } @Override public Mono decode(String methodKey, ReactiveHttpResponse response) { return singleToMono(statusHandler.decode(methodKey, response)); } } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/client/statushandler/Rx2StatusHandler.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.client.statushandler; import io.reactivex.Single; import reactivefeign.client.ReactiveHttpResponse; /** * @author Sergii Karpenko */ public interface Rx2StatusHandler { boolean shouldHandle(int status); Single decode(String methodKey, ReactiveHttpResponse response); } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/client/statushandler/Rx2StatusHandlers.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.client.statushandler; import io.reactivex.Single; import reactivefeign.client.ReactiveHttpResponse; import java.util.function.BiFunction; import java.util.function.Predicate; public class Rx2StatusHandlers { public static Rx2StatusHandler throwOnStatus( Predicate statusPredicate, BiFunction errorFunction) { return new Rx2StatusHandler() { @Override public boolean shouldHandle(int status) { return statusPredicate.test(status); } @Override public Single decode(String methodKey, ReactiveHttpResponse response) { return Single.just(errorFunction.apply(methodKey, response)); } }; } } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/methodhandler/Rx2MethodHandler.java ================================================ package reactivefeign.rx2.methodhandler; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; import org.reactivestreams.Publisher; import reactivefeign.methodhandler.MethodHandler; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.Type; import static reactor.adapter.rxjava.RxJava2Adapter.*; public class Rx2MethodHandler implements MethodHandler { private final MethodHandler methodHandler; private final Type returnPublisherType; public Rx2MethodHandler(MethodHandler methodHandler, Type returnPublisherType) { this.methodHandler = methodHandler; this.returnPublisherType = returnPublisherType; } @Override @SuppressWarnings("unchecked") public Object invoke(final Object[] argv) { try { Publisher publisher = (Publisher)methodHandler.invoke(argv); if(returnPublisherType == Flowable.class){ return fluxToFlowable((Flux) publisher); } else if(returnPublisherType == Observable.class){ return fluxToObservable((Flux) publisher); } else if(returnPublisherType == Single.class){ return monoToSingle((Mono) publisher); } else if(returnPublisherType == Maybe.class){ return monoToMaybe((Mono) publisher); } else { throw new IllegalArgumentException("Unexpected returnPublisherType="+returnPublisherType.getClass()); } } catch (Throwable throwable) { if(returnPublisherType == Flowable.class){ return Flowable.error(throwable); } else if(returnPublisherType == Observable.class){ return Observable.error(throwable); } else if(returnPublisherType == Single.class){ return Single.error(throwable); } else if(returnPublisherType == Maybe.class){ return Maybe.error(throwable); } else { throw new IllegalArgumentException("Unexpected returnPublisherType="+returnPublisherType.getClass()); } } } } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/methodhandler/Rx2MethodHandlerFactory.java ================================================ package reactivefeign.rx2.methodhandler; import feign.MethodMetadata; import feign.Target; import io.reactivex.BackpressureStrategy; import reactivefeign.methodhandler.DefaultMethodHandler; import reactivefeign.methodhandler.MethodHandler; import reactivefeign.methodhandler.MethodHandlerFactory; import reactivefeign.publisher.PublisherClientFactory; import java.lang.reflect.Method; import static reactivefeign.utils.FeignUtils.returnPublisherType; public class Rx2MethodHandlerFactory implements MethodHandlerFactory { private final PublisherClientFactory publisherClientFactory; private final BackpressureStrategy backpressureStrategy; public Rx2MethodHandlerFactory(PublisherClientFactory publisherClientFactory, BackpressureStrategy backpressureStrategy) { this.publisherClientFactory = publisherClientFactory; this.backpressureStrategy = backpressureStrategy; } @Override public MethodHandler create(final Target target, final MethodMetadata metadata) { MethodHandler methodHandler = new Rx2PublisherClientMethodHandler( target, metadata, publisherClientFactory.apply(metadata), backpressureStrategy); return new Rx2MethodHandler(methodHandler, returnPublisherType(metadata)); } @Override public MethodHandler createDefault(Method method) { return new DefaultMethodHandler(method); } } ================================================ FILE: feign-reactor-rx2/src/main/java/reactivefeign/rx2/methodhandler/Rx2PublisherClientMethodHandler.java ================================================ package reactivefeign.rx2.methodhandler; import feign.MethodMetadata; import feign.Target; import io.reactivex.*; import org.reactivestreams.Publisher; import reactivefeign.methodhandler.PublisherClientMethodHandler; import reactivefeign.publisher.PublisherHttpClient; import reactor.core.publisher.Mono; import static reactor.adapter.rxjava.RxJava2Adapter.*; public class Rx2PublisherClientMethodHandler extends PublisherClientMethodHandler { private final BackpressureStrategy backpressureStrategy; public Rx2PublisherClientMethodHandler( Target target, MethodMetadata methodMetadata, PublisherHttpClient publisherClient, BackpressureStrategy backpressureStrategy) { super(target, methodMetadata, publisherClient); this.backpressureStrategy = backpressureStrategy; } @Override protected Publisher body(Object body) { if (body instanceof Flowable) { return flowableToFlux((Flowable) body); } else if (body instanceof Observable) { return observableToFlux((Observable) body, backpressureStrategy); } else if (body instanceof Single) { return singleToMono((Single) body); } else if (body instanceof Maybe) { return maybeToMono((Maybe) body); } else { return Mono.just(body); } } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/ContractTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import reactivefeign.ReactiveFeign; import reactivefeign.rx2.testcase.IcecreamServiceApi; import reactivefeign.rx2.testcase.IcecreamServiceApiBroken; import static org.hamcrest.Matchers.containsString; /** * @author Sergii Karpenko */ public class ContractTest { @Rule public ExpectedException expectedException = ExpectedException.none(); protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } @Test public void shouldFailOnBrokenContract() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage(containsString("Broken Contract")); this.builder() .contract(targetType -> { throw new IllegalArgumentException("Broken Contract"); }) .target(IcecreamServiceApi.class, "http://localhost:8888"); } @Test public void shouldFailIfNotReactiveContract() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage(containsString("IcecreamServiceApiBroken#findOrder(int)")); this.builder() .target(IcecreamServiceApiBroken.class, "http://localhost:8888"); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/DefaultMethodTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.RequestLine; import io.reactivex.Single; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.rx2.testcase.IcecreamServiceApi; import reactivefeign.rx2.testcase.domain.IceCreamOrder; import reactivefeign.rx2.testcase.domain.OrderGenerator; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static reactivefeign.rx2.TestUtils.equalsComparingFieldByFieldRecursivelyRx; /** * @author Sergii Karpenko */ public class DefaultMethodTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); @Before public void resetServers() { wireMockRule.resetAll(); } protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } protected ReactiveFeign.Builder builder(Class apiClass){ return Rx2ReactiveFeign.builder(); } protected ReactiveFeign.Builder builder(ReactiveOptions options){ return Rx2ReactiveFeign.builder().options(options); } @Test public void shouldProcessDefaultMethodOnProxy() throws JsonProcessingException, InterruptedException { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(orderStr))); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.findFirstOrder().test() .await() .assertSubscribed() .assertValue(equalsComparingFieldByFieldRecursivelyRx(orderGenerated)) .assertNoErrors() .assertComplete(); } @Test(expected = RuntimeException.class) public void shouldNotWrapException() { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.throwsException().onErrorReturn( throwable -> orderGenerated).blockingGet(); } @Test public void shouldOverrideEquals() { IcecreamServiceApi client = builder( new ReactiveOptions.Builder() .setConnectTimeoutMillis(300) .setReadTimeoutMillis(100).build()) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); IcecreamServiceApi clientWithSameTarget = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client).isEqualTo(clientWithSameTarget); IcecreamServiceApi clientWithOtherPort = builder() .target(IcecreamServiceApi.class, "http://localhost:" + (wireMockRule.port() + 1)); Assertions.assertThat(client).isNotEqualTo(clientWithOtherPort); OtherApi clientWithOtherInterface = builder(OtherApi.class) .target(OtherApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client).isNotEqualTo(clientWithOtherInterface); } interface OtherApi { @RequestLine("GET /icecream/flavors") Single method(String arg); } @Test public void shouldOverrideHashcode() { IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); IcecreamServiceApi otherClientWithSameTarget = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client.hashCode()).isEqualTo(otherClientWithSameTarget.hashCode()); } @Test public void shouldOverrideToString() { IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Assertions.assertThat(client.toString()) .isEqualTo("HardCodedTarget(type=IcecreamServiceApi, " + "url=http://localhost:" + wireMockRule.port() + ")"); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/LoggerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import io.reactivex.Single; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; import org.assertj.core.api.Condition; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import reactivefeign.ReactiveFeign; import reactivefeign.client.LoggerReactiveHttpClient; import reactivefeign.rx2.testcase.IcecreamServiceApi; import reactivefeign.rx2.testcase.domain.Bill; import reactivefeign.rx2.testcase.domain.IceCreamOrder; import reactivefeign.rx2.testcase.domain.OrderGenerator; import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; /** * @author Sergii Karpenko */ public class LoggerTest { public static final String LOGGER_NAME = LoggerReactiveHttpClient.class.getName(); @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig() .asynchronousResponseEnabled(true) .dynamicPort()); protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } protected Appender appender; @Test public void shouldLog() throws Exception { setLogLevel(Level.TRACE); IceCreamOrder order = new OrderGenerator().generate(20); Bill billExpected = Bill.makeBill(order); wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(billExpected)))); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); Single billMono = client.makeOrder(order); // no logs before subscription ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(LogEvent.class); Mockito.verify(appender, never()).append(argumentCaptor.capture()); billMono.blockingGet(); Mockito.verify(appender, times(7)).append(argumentCaptor.capture()); List logEvents = argumentCaptor.getAllValues(); assertLogEvent(logEvents, 0, Level.DEBUG, "[IcecreamServiceApi#makeOrder]--->POST http://localhost"); assertLogEvent(logEvents, 1, Level.TRACE, "[IcecreamServiceApi#makeOrder] REQUEST HEADERS\n" + "Accept:[application/json]"); assertLogEvent(logEvents, 2, Level.TRACE, "[IcecreamServiceApi#makeOrder] REQUEST BODY\n" + "IceCreamOrder{ id=20, balls="); assertLogEvent(logEvents, 3, Level.TRACE, "[IcecreamServiceApi#makeOrder] RESPONSE HEADERS\n" + "Content-Type:application/json"); assertLogEvent(logEvents, 4, Level.DEBUG, "[IcecreamServiceApi#makeOrder]<--- headers takes"); assertLogEvent(logEvents, 5, Level.TRACE, "[IcecreamServiceApi#makeOrder] RESPONSE BODY\n" + "reactivefeign.rx2.testcase.domain.Bill"); assertLogEvent(logEvents, 6, Level.DEBUG, "[IcecreamServiceApi#makeOrder]<--- body takes"); } private void assertLogEvent(List events, int index, Level level, String message) { assertThat(events).element(index) .hasFieldOrPropertyWithValue("level", level) .extracting("message") .extractingResultOf("getFormattedMessage") .have(new Condition<>(o -> ((String) o).contains(message), "check message")); } @Before public void before() { appender = Mockito.mock(Appender.class); when(appender.getName()).thenReturn("TestAppender"); when(appender.isStarted()).thenReturn(true); getLoggerConfig().addAppender(appender, Level.ALL, null); } private static void setLogLevel(Level logLevel) { LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); Configuration configuration = loggerContext.getConfiguration(); configuration.getLoggerConfig(LOGGER_NAME).setLevel(logLevel); loggerContext.updateLoggers(); } private static LoggerConfig getLoggerConfig() { LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); Configuration configuration = loggerContext.getConfiguration(); configuration.addLogger(LOGGER_NAME, new LoggerConfig()); return configuration.getLoggerConfig(LOGGER_NAME); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/NotFoundTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.rx2.testcase.IcecreamServiceApi; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; /** * @author Sergii Karpenko */ public class NotFoundTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } @Test public void shouldReturnEmptyMono() throws InterruptedException { String orderUrl = "/icecream/orders/2"; wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_NOT_FOUND))); IcecreamServiceApi client = builder() .decode404() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.findOrder(2).test() .await() .assertSubscribed() .assertNoValues() .assertNoErrors() .assertComplete(); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/ReactivityTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.awaitility.Duration; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.rx2.testcase.IcecreamServiceApi; import reactivefeign.rx2.testcase.domain.IceCreamOrder; import reactivefeign.rx2.testcase.domain.OrderGenerator; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.awaitility.Awaitility.waitAtMost; /** * @author Sergii Karpenko */ public class ReactivityTest { public static final int DELAY_IN_MILLIS = 500; public static final int CALLS_NUMBER = 100; public static final int REACTIVE_GAIN_RATIO = 10; @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig() .asynchronousResponseEnabled(true) .dynamicPort()); protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } @Test public void shouldRunReactively() throws JsonProcessingException { IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(orderStr) .withFixedDelay(DELAY_IN_MILLIS))); IcecreamServiceApi client = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); AtomicInteger counter = new AtomicInteger(); new Thread(() -> { for (int i = 0; i < CALLS_NUMBER; i++) { client.findFirstOrder() .doOnSuccess(order -> counter.incrementAndGet()) .subscribe(); } }).start(); int timeToCompleteReactively = CALLS_NUMBER * DELAY_IN_MILLIS / REACTIVE_GAIN_RATIO; waitAtMost(new Duration(timeToCompleteReactively, TimeUnit.MILLISECONDS)) .until(() -> counter.get() == CALLS_NUMBER); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/ReadTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.client.ReadTimeoutException; import reactivefeign.rx2.testcase.IcecreamServiceApi; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; /** * @author Sergii Karpenko */ public class ReadTimeoutTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); protected ReactiveFeign.Builder builder(ReactiveOptions options){ return Rx2ReactiveFeign.builder().options(options); } @Test public void shouldFailOnReadTimeout() throws InterruptedException { String orderUrl = "/icecream/orders/1"; wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withFixedDelay(200))); IcecreamServiceApi client = builder( new ReactiveOptions.Builder() .setConnectTimeoutMillis(300) .setReadTimeoutMillis(100) .build()) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.findOrder(1).test() .await() .assertSubscribed() .assertError(ReadTimeoutException.class); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/RequestInterceptorTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.FeignException; import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.ReactiveFeign; import reactivefeign.rx2.testcase.IcecreamServiceApi; import reactivefeign.rx2.testcase.domain.IceCreamOrder; import reactivefeign.rx2.testcase.domain.OrderGenerator; import reactivefeign.utils.Pair; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.util.Collections.singletonList; import static reactivefeign.rx2.TestUtils.equalsComparingFieldByFieldRecursivelyRx; /** * @author Sergii Karpenko */ public class RequestInterceptorTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } @Test public void shouldInterceptRequestAndSetAuthHeader() throws JsonProcessingException, InterruptedException { String orderUrl = "/icecream/orders/1"; IceCreamOrder orderGenerated = new OrderGenerator().generate(1); String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_UNAUTHORIZED))) .setPriority(100); wireMockRule.stubFor(get(urlEqualTo(orderUrl)) .withHeader("Accept", equalTo("application/json")) .withHeader("Authorization", equalTo("Bearer mytoken123")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(orderStr))) .setPriority(1); IcecreamServiceApi clientWithoutAuth = builder() .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); clientWithoutAuth.findFirstOrder().test() .await() .assertSubscribed() .assertError(FeignException.class); IcecreamServiceApi clientWithAuth = builder() .addHeaders(singletonList(new Pair<>("Authorization", "Bearer mytoken123"))) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); clientWithAuth.findFirstOrder().test() .await() .assertSubscribed() .assertValue(equalsComparingFieldByFieldRecursivelyRx(orderGenerated)) .assertNoErrors() .assertComplete(); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/SmokeTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.fasterxml.jackson.core.JsonProcessingException; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import io.reactivex.Single; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import reactivefeign.ReactiveFeign; import reactivefeign.rx2.testcase.IcecreamServiceApi; import reactivefeign.rx2.testcase.domain.*; import java.util.Map; import java.util.stream.Collectors; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static reactivefeign.rx2.TestUtils.equalsComparingFieldByFieldRecursively; import static reactivefeign.rx2.TestUtils.equalsComparingFieldByFieldRecursivelyRx; /** * @author Sergii Karpenko */ public class SmokeTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); @Before public void resetServers() { wireMockRule.resetAll(); } protected ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } private IcecreamServiceApi client; private OrderGenerator generator = new OrderGenerator(); private Map orders = generator.generateRange(10).stream() .collect(Collectors.toMap(IceCreamOrder::getId, o -> o)); @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { String targetUrl = "http://localhost:" + wireMockRule.port(); client = builder() .decode404() .target(IcecreamServiceApi.class, targetUrl); } @Test public void testSimpleGet_success() throws JsonProcessingException, InterruptedException { wireMockRule.stubFor(get(urlEqualTo("/icecream/flavors")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(Flavor.values())))); wireMockRule.stubFor(get(urlEqualTo("/icecream/mixins")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(Mixin.values())))); client.getAvailableFlavors().test() .await() .assertResult(Flavor.values()); client.getAvailableMixins().test() .await() .assertResult(Mixin.values()); } @Test public void testFindOrder_success() throws JsonProcessingException, InterruptedException { IceCreamOrder orderExpected = orders.get(1); wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(orderExpected)))); client.findOrder(1).test() .await() .assertSubscribed() .assertValue(equalsComparingFieldByFieldRecursivelyRx(orderExpected)) .assertNoErrors() .assertComplete(); } @Test public void testFindOrder_empty() throws InterruptedException { client.findOrder(123).test() .await() .assertSubscribed() .assertNoValues() .assertNoErrors() .assertComplete(); } @Test public void testMakeOrder_success() throws JsonProcessingException { IceCreamOrder order = new OrderGenerator().generate(20); Bill billExpected = Bill.makeBill(order); wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(TestUtils.MAPPER.writeValueAsString(billExpected)))); Single bill = client.makeOrder(order); assertThat(bill.blockingGet()) .matches(equalsComparingFieldByFieldRecursively(billExpected)); } @Test public void testPayBill_success() throws JsonProcessingException { Bill bill = Bill.makeBill(new OrderGenerator().generate(30)); wireMockRule.stubFor(post(urlEqualTo("/icecream/bills/pay")) .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(bill))) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json") .withBody(Long.toString(321)))); Single result = client.payBill(bill); assertThat(result.blockingGet()).isEqualTo(321); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/StatusHandlerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import feign.RetryableException; import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import reactivefeign.rx2.testcase.IcecreamServiceApi; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static reactivefeign.rx2.client.statushandler.Rx2StatusHandlers.throwOnStatus; /** * @author Sergii Karpenko */ public class StatusHandlerTest { @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule( wireMockConfig().dynamicPort()); protected Rx2ReactiveFeign.Builder builder(){ return Rx2ReactiveFeign.builder(); } @Before public void resetServers() { wireMockRule.resetAll(); } @Test public void shouldThrowRetryException() throws InterruptedException { wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))); IcecreamServiceApi client = builder() .statusHandler(throwOnStatus( status -> status == HttpStatus.SC_SERVICE_UNAVAILABLE, (methodTag, response) -> new RetryableException("Should retry on next node", null))) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.findFirstOrder().test() .await() .assertError(RetryableException.class); } @Test public void shouldThrowOnStatusCode() throws InterruptedException { wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/2")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse().withStatus(HttpStatus.SC_UNAUTHORIZED))); IcecreamServiceApi client = builder() .statusHandler( throwOnStatus( status -> status == HttpStatus.SC_UNAUTHORIZED, (methodTag, response) -> new RuntimeException("Should login", null))) .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); client.findOrder(2).test() .await() .assertError(RuntimeException.class); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/TestUtils.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.util.function.Predicate; /** * Helper methods for tests. */ class TestUtils { static final ObjectMapper MAPPER; static { MAPPER = new ObjectMapper(); MAPPER.registerModule(new JavaTimeModule()); } public static Predicate equalsComparingFieldByFieldRecursively(T rhs) { return lhs -> { try { return MAPPER.writeValueAsString(lhs).equals(MAPPER.writeValueAsString(rhs)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }; } public static io.reactivex.functions.Predicate equalsComparingFieldByFieldRecursivelyRx(T rhs) { return lhs -> { try { return MAPPER.writeValueAsString(lhs).equals(MAPPER.writeValueAsString(rhs)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }; } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/IcecreamServiceApi.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase; import feign.Headers; import feign.Param; import feign.RequestLine; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; import reactivefeign.rx2.testcase.domain.Bill; import reactivefeign.rx2.testcase.domain.Flavor; import reactivefeign.rx2.testcase.domain.IceCreamOrder; import reactivefeign.rx2.testcase.domain.Mixin; /** * API of an iceream web service. * * @author Sergii Karpenko */ @Headers({"Accept: application/json"}) public interface IcecreamServiceApi { RuntimeException RUNTIME_EXCEPTION = new RuntimeException("tests exception"); @RequestLine("GET /icecream/flavors") Flowable getAvailableFlavors(); @RequestLine("GET /icecream/mixins") Observable getAvailableMixins(); @RequestLine("POST /icecream/orders") @Headers("Content-Type: application/json") Single makeOrder(IceCreamOrder order); @RequestLine("GET /icecream/orders/{orderId}") Maybe findOrder(@Param("orderId") int orderId); @RequestLine("POST /icecream/bills/pay") @Headers("Content-Type: application/json") Single payBill(Bill bill); default Maybe findFirstOrder() { return findOrder(1); } default Single throwsException() { throw RUNTIME_EXCEPTION; } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/IcecreamServiceApiBroken.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase; import feign.Headers; import feign.Param; import feign.RequestLine; import io.reactivex.Flowable; import io.reactivex.Single; import reactivefeign.ReactiveContract; import reactivefeign.rx2.testcase.domain.Bill; import reactivefeign.rx2.testcase.domain.Flavor; import reactivefeign.rx2.testcase.domain.IceCreamOrder; import reactivefeign.rx2.testcase.domain.Mixin; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; /** * API of an iceream web service with one method that doesn't returns {@link Mono} or {@link Flux} * and violates {@link ReactiveContract}s rules. * * @author Sergii Karpenko */ public interface IcecreamServiceApiBroken { @RequestLine("GET /icecream/flavors") Single> getAvailableFlavors(); @RequestLine("GET /icecream/mixins") Flowable getAvailableMixins(); /** * Method that doesn't respects contract. */ @RequestLine("GET /icecream/orders/{orderId}") IceCreamOrder findOrder(@Param("orderId") int orderId); } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/domain/Bill.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase.domain; import java.util.HashMap; import java.util.Map; /** * Bill for consumed ice cream. */ public class Bill { private static final Map PRICES = new HashMap<>(); static { PRICES.put(1, (float) 2.00); // two euros for one ball (expensive!) PRICES.put(3, (float) 2.85); // 2.85€ for 3 balls PRICES.put(5, (float) 4.30); // 4.30€ for 5 balls PRICES.put(7, (float) 5); // only five euros for seven balls! Wow } private static final float MIXIN_PRICE = (float) 0.6; // price per mixin private Float price; public Bill() {} public Bill(final Float price) { this.price = price; } public Float getPrice() { return price; } public void setPrice(final Float price) { this.price = price; } /** * Makes a bill from an order. * * @param order ice cream order * @return bill */ public static Bill makeBill(final IceCreamOrder order) { int nbBalls = order.getBalls().values().stream().mapToInt(Integer::intValue) .sum(); Float price = PRICES.get(nbBalls) + order.getMixins().size() * MIXIN_PRICE; return new Bill(price); } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/domain/Flavor.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase.domain; /** * Ice cream flavors. */ public enum Flavor { STRAWBERRY, CHOCOLATE, BANANA, PISTACHIO, MELON, VANILLA } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/domain/IceCreamOrder.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase.domain; import java.time.Instant; import java.util.*; /** * Give me some ice-cream! :p */ public class IceCreamOrder { private static Random random = new Random(); private int id; // order id private Map balls; // how much balls of flavor private Set mixins; // and some mixins ... private Instant orderTimestamp; // and give it to me right now ! IceCreamOrder() {} IceCreamOrder(int id) { this(id, Instant.now()); } IceCreamOrder(int id, final Instant orderTimestamp) { this.id = id; this.balls = new HashMap<>(); this.mixins = new HashSet<>(); this.orderTimestamp = orderTimestamp; } IceCreamOrder addBall(final Flavor ballFlavor) { final Integer ballCount = balls.containsKey(ballFlavor) ? balls.get(ballFlavor) + 1 : 1; balls.put(ballFlavor, ballCount); return this; } IceCreamOrder addMixin(final Mixin mixin) { mixins.add(mixin); return this; } IceCreamOrder withOrderTimestamp(final Instant orderTimestamp) { this.orderTimestamp = orderTimestamp; return this; } public int getId() { return id; } public Map getBalls() { return balls; } public Set getMixins() { return mixins; } public Instant getOrderTimestamp() { return orderTimestamp; } @Override public String toString() { return "IceCreamOrder{" + " id=" + id + ", balls=" + balls + ", mixins=" + mixins + ", orderTimestamp=" + orderTimestamp + '}'; } } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/domain/Mixin.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase.domain; /** * Ice cream mix-ins. */ public enum Mixin { COOKIES, MNMS, CHOCOLATE_SIROP, STRAWBERRY_SIROP, NUTS, RAINBOW } ================================================ FILE: feign-reactor-rx2/src/test/java/reactivefeign/rx2/testcase/domain/OrderGenerator.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.rx2.testcase.domain; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * Generator of random ice cream orders. */ public class OrderGenerator { private static final int[] BALLS_NUMBER = {1, 3, 5, 7}; private static final int[] MIXIN_NUMBER = {1, 2, 3}; private static final Random random = new Random(); public IceCreamOrder generate(int id) { final IceCreamOrder order = new IceCreamOrder(id); final int nbBalls = peekBallsNumber(); final int nbMixins = peekMixinNumber(); IntStream.rangeClosed(1, nbBalls).mapToObj(i -> this.peekFlavor()) .forEach(order::addBall); IntStream.rangeClosed(1, nbMixins).mapToObj(i -> this.peekMixin()) .forEach(order::addMixin); return order; } public Collection generateRange(int n) { Instant now = Instant.now(); List orderTimestamps = IntStream.range(0, n) .mapToObj(minutes -> now.minus(minutes, ChronoUnit.MINUTES)) .collect(Collectors.toList()); return IntStream.range(0, n) .mapToObj( i -> this.generate(i).withOrderTimestamp(orderTimestamps.get(i))) .collect(Collectors.toList()); } private int peekBallsNumber() { return BALLS_NUMBER[random.nextInt(BALLS_NUMBER.length)]; } private int peekMixinNumber() { return MIXIN_NUMBER[random.nextInt(MIXIN_NUMBER.length)]; } private Flavor peekFlavor() { return Flavor.values()[random.nextInt(Flavor.values().length)]; } private Mixin peekMixin() { return Mixin.values()[random.nextInt(Mixin.values().length)]; } } ================================================ FILE: feign-reactor-webclient/pom.xml ================================================ 4.0.0 io.github.reactivefeign feign-reactor 1.0.0-SNAPSHOT feign-reactor-webclient io.github.reactivefeign feign-reactor-core org.springframework spring-webflux org.springframework spring-web io.projectreactor.ipc reactor-netty io.netty netty-all io.github.reactivefeign feign-reactor-core 1.0.0-SNAPSHOT test-jar test org.springframework.boot spring-boot-starter-webflux spring-boot-starter-logging org.springframework.boot test org.springframework.boot spring-boot-starter-test test io.projectreactor reactor-test test com.github.tomakehurst wiremock test org.apache.logging.log4j log4j-slf4j-impl test org.awaitility awaitility test ================================================ FILE: feign-reactor-webclient/src/main/java/reactivefeign/webclient/WebReactiveFeign.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import io.netty.channel.ChannelOption; import io.netty.handler.timeout.ReadTimeoutHandler; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import java.util.concurrent.TimeUnit; import static reactivefeign.webclient.client.WebReactiveHttpClient.webClient; /** * {@link WebClient} based implementation of reactive Feign * * @author Sergii Karpenko */ public class WebReactiveFeign { public static final int DEFAULT_READ_TIMEOUT_MILLIS = 10000; public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 5000; public static Builder builder() { return new Builder<>(); } public static Builder builder(WebClient webClient) { return new Builder<>(webClient); } public static class Builder extends ReactiveFeign.Builder { protected WebClient webClient; protected Builder() { this(WebClient.create()); } protected Builder(WebClient webClient) { setWebClient(webClient); options(new ReactiveOptions.Builder() .setConnectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS) .setReadTimeoutMillis(DEFAULT_READ_TIMEOUT_MILLIS) .build()); } @Override public Builder options(ReactiveOptions options) { if (!options.isEmpty()) { ReactorClientHttpConnector connector = new ReactorClientHttpConnector( opts -> { if (options.getConnectTimeoutMillis() != null) { opts.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, options.getConnectTimeoutMillis().intValue()); } if (options.getReadTimeoutMillis() != null) { opts.afterNettyContextInit(ctx -> { ctx.addHandlerLast(new ReadTimeoutHandler( options.getReadTimeoutMillis(), TimeUnit.MILLISECONDS)); }); } if (options.isTryUseCompression() != null) { opts.compression(options.isTryUseCompression()); } }); setWebClient(webClient.mutate().clientConnector(connector).build()); } return this; } protected void setWebClient(WebClient webClient){ this.webClient = webClient; clientFactory(methodMetadata -> webClient(methodMetadata, webClient)); } } } ================================================ FILE: feign-reactor-webclient/src/main/java/reactivefeign/webclient/client/WebReactiveHttpClient.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.webclient.client; import feign.MethodMetadata; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import reactivefeign.client.ReactiveHttpClient; import reactivefeign.client.ReactiveHttpRequest; import reactivefeign.client.ReactiveHttpResponse; import reactivefeign.client.ReadTimeoutException; import reactor.core.publisher.Mono; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import static feign.Util.resolveLastTypeParameter; import static java.util.Optional.ofNullable; import static reactivefeign.utils.FeignUtils.getBodyActualType; /** * Uses {@link WebClient} to execute http requests * @author Sergii Karpenko */ public class WebReactiveHttpClient implements ReactiveHttpClient { private final WebClient webClient; private final ParameterizedTypeReference bodyActualType; private final Type returnPublisherType; private final ParameterizedTypeReference returnActualType; public static WebReactiveHttpClient webClient(MethodMetadata methodMetadata, WebClient webClient) { final Type returnType = methodMetadata.returnType(); Type returnPublisherType = ((ParameterizedType) returnType).getRawType(); ParameterizedTypeReference returnActualType = ParameterizedTypeReference.forType( resolveLastTypeParameter(returnType, (Class) returnPublisherType)); ParameterizedTypeReference bodyActualType = ofNullable( getBodyActualType(methodMetadata.bodyType())) .map(type -> ParameterizedTypeReference.forType(type)) .orElse(null); return new WebReactiveHttpClient(webClient, bodyActualType, returnPublisherType, returnActualType); } public WebReactiveHttpClient(WebClient webClient, ParameterizedTypeReference bodyActualType, Type returnPublisherType, ParameterizedTypeReference returnActualType) { this.webClient = webClient; this.bodyActualType = bodyActualType; this.returnPublisherType = returnPublisherType; this.returnActualType = returnActualType; } @Override public Mono executeRequest(ReactiveHttpRequest request) { return webClient.method(HttpMethod.valueOf(request.method())) .uri(request.uri()) .headers(httpHeaders -> setUpHeaders(request, httpHeaders)) .body(provideBody(request)) .exchange() .onErrorMap(ex -> ex instanceof io.netty.handler.timeout.ReadTimeoutException, ReadTimeoutException::new) .map(response -> new WebReactiveHttpResponse(response, returnPublisherType, returnActualType)); } protected BodyInserter provideBody(ReactiveHttpRequest request) { return bodyActualType != null ? BodyInserters.fromPublisher(request.body(), bodyActualType) : BodyInserters.empty(); } protected void setUpHeaders(ReactiveHttpRequest request, HttpHeaders httpHeaders) { request.headers().forEach(httpHeaders::put); } } ================================================ FILE: feign-reactor-webclient/src/main/java/reactivefeign/webclient/client/WebReactiveHttpResponse.java ================================================ package reactivefeign.webclient.client; import org.reactivestreams.Publisher; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ByteArrayResource; import org.springframework.web.reactive.function.client.ClientResponse; import reactivefeign.client.ReactiveHttpResponse; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.Type; import java.util.List; import java.util.Map; class WebReactiveHttpResponse implements ReactiveHttpResponse{ private final ClientResponse clientResponse; private final Type returnPublisherType; private final ParameterizedTypeReference returnActualType; WebReactiveHttpResponse(ClientResponse clientResponse, Type returnPublisherType, ParameterizedTypeReference returnActualType) { this.clientResponse = clientResponse; this.returnPublisherType = returnPublisherType; this.returnActualType = returnActualType; } @Override public int status() { return clientResponse.statusCode().value(); } @Override public Map> headers() { return clientResponse.headers().asHttpHeaders(); } @Override public Publisher body() { if (returnPublisherType == Mono.class) { return clientResponse.bodyToMono(returnActualType); } else if(returnPublisherType == Flux.class){ return clientResponse.bodyToFlux(returnActualType); } else { throw new IllegalArgumentException("Unknown returnPublisherType: " + returnPublisherType); } } @Override public Mono bodyData() { return clientResponse.bodyToMono(ByteArrayResource.class) .map(ByteArrayResource::getByteArray) .defaultIfEmpty(new byte[0]); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/CompressionTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class CompressionTest extends reactivefeign.CompressionTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return WebReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/ConnectionTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class ConnectionTimeoutTest extends reactivefeign.ConnectionTimeoutTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return WebReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/ContractTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; /** * @author Sergii Karpenko */ public class ContractTest extends reactivefeign.ContractTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/DefaultMethodTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class DefaultMethodTest extends reactivefeign.DefaultMethodTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } @Override protected ReactiveFeign.Builder builder(Class apiClass) { return WebReactiveFeign.builder(); } @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return WebReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/LoggerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class LoggerTest extends reactivefeign.LoggerTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/NotFoundTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class NotFoundTest extends reactivefeign.NotFoundTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/ReactivityTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import com.fasterxml.jackson.core.JsonProcessingException; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; public class ReactivityTest extends reactivefeign.ReactivityTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } @Override public void shouldRunReactively() throws JsonProcessingException { super.shouldRunReactively(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/ReadTimeoutTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.ReactiveOptions; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class ReadTimeoutTest extends reactivefeign.ReadTimeoutTest { @Override protected ReactiveFeign.Builder builder(ReactiveOptions options) { return WebReactiveFeign.builder().options(options); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/RequestInterceptorTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class RequestInterceptorTest extends reactivefeign.RequestInterceptorTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/RetryingTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class RetryingTest extends reactivefeign.RetryingTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/SmokeTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class SmokeTest extends reactivefeign.SmokeTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/StatusHandlerTest.java ================================================ /** * Copyright 2018 The Feign Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package reactivefeign.webclient; import reactivefeign.ReactiveFeign; import reactivefeign.testcase.IcecreamServiceApi; /** * @author Sergii Karpenko */ public class StatusHandlerTest extends reactivefeign.StatusHandlerTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/allfeatures/AllFeaturesTest.java ================================================ /* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package reactivefeign.webclient.allfeatures; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import reactivefeign.ReactiveFeign; import reactivefeign.allfeatures.AllFeaturesController; import reactivefeign.webclient.WebReactiveFeign; /** * @author Sergii Karpenko * * Tests ReactiveFeign in conjunction with WebFlux rest controller. */ @EnableAutoConfiguration(exclude = {ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class}) public class AllFeaturesTest extends reactivefeign.allfeatures.AllFeaturesTest { @Override protected ReactiveFeign.Builder builder() { return WebReactiveFeign.builder(); } //Netty's WebClient is not able to do this trick @Ignore @Test @Override public void shouldReturnFirstResultBeforeSecondSent() { } //WebClient is not able to do this @Ignore @Test @Override public void shouldMirrorStringStreamBody() { } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/allfeatures/WebClientFeaturesApi.java ================================================ package reactivefeign.webclient.allfeatures; import feign.Headers; import feign.RequestLine; import org.reactivestreams.Publisher; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import reactor.core.publisher.Flux; import java.nio.ByteBuffer; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; public interface WebClientFeaturesApi { @RequestLine("POST " + "/mirrorStreamingBinaryBodyReactive") @Headers({ "Content-Type: "+APPLICATION_OCTET_STREAM_VALUE }) Flux mirrorStreamingBinaryBodyReactive(Publisher body); @RequestLine("POST " + "/mirrorResourceReactiveWithZeroCopying") @Headers({ "Content-Type: "+APPLICATION_OCTET_STREAM_VALUE }) Flux mirrorResourceReactiveWithZeroCopying(Resource resource); } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/allfeatures/WebClientFeaturesController.java ================================================ package reactivefeign.webclient.allfeatures; import org.reactivestreams.Publisher; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class WebClientFeaturesController implements WebClientFeaturesApi{ @PostMapping(path = "/mirrorStreamingBinaryBodyReactive") @Override public Flux mirrorStreamingBinaryBodyReactive(@RequestBody Publisher body) { return Flux.from(body); } @PostMapping(path = "/mirrorResourceReactiveWithZeroCopying") @Override public Flux mirrorResourceReactiveWithZeroCopying(@RequestBody Resource resource) { return DataBufferUtils.read(resource, new DefaultDataBufferFactory(), 3); } } ================================================ FILE: feign-reactor-webclient/src/test/java/reactivefeign/webclient/allfeatures/WebClientFeaturesTest.java ================================================ package reactivefeign.webclient.allfeatures; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.test.context.junit4.SpringRunner; import reactivefeign.webclient.WebReactiveFeign; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import static java.nio.ByteBuffer.wrap; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest( properties = {"spring.main.web-application-type=reactive"}, classes = {WebClientFeaturesController.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @EnableAutoConfiguration(exclude = {org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class}) public class WebClientFeaturesTest { private WebClientFeaturesApi client; @LocalServerPort private int port; @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { client = WebReactiveFeign.builder() .decode404() .target(WebClientFeaturesApi.class, "http://localhost:" + port); } @Test public void shouldMirrorStreamingBinaryBodyReactive() { Flux returned = client .mirrorStreamingBinaryBodyReactive(Flux.just( fromByteArray(new byte[]{1,2,3}), fromByteArray(new byte[]{4,5,6}))); StepVerifier.create(returned) .expectNextMatches(dataBuffer -> dataBuffer.asByteBuffer().equals(wrap(new byte[]{1,2,3}))) .expectNextMatches(dataBuffer -> dataBuffer.asByteBuffer().equals(wrap(new byte[]{4,5,6}))) .verifyComplete(); } private static DataBuffer fromByteArray(byte[] data){ return new DefaultDataBufferFactory().wrap(data); } @Test public void shouldMirrorResourceReactiveWithZeroCopying(){ byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; ByteArrayResource resource = new ByteArrayResource(data); Flux returned = client.mirrorResourceReactiveWithZeroCopying(resource); assertThat(DataBufferUtils.join(returned).block().asByteBuffer()).isEqualTo(wrap(data)); } } ================================================ FILE: feign-reactor-webclient/src/test/resources/log4j2.xml ================================================ ================================================ FILE: pom.xml ================================================ 4.0.0 io.github.reactivefeign feign-reactor 1.0.0-SNAPSHOT pom feign-reactor-core feign-reactor-webclient feign-reactor-cloud feign-reactor-rx2 feign-reactor-jetty feign-reactive Use Feign client on WebClient https://github.com/kptfh/feign-reactive ReactiveFeign https://github.com/kptfh/feign-reactive Github https://github.com/kptfh/feign-reactive/issues The Apache Software License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt repo kptfh Sergii Karpenko sergey.karpenko@gmail.com 1.8 ${java.version} ${java.version} UTF-8 UTF-8 3.1.8.RELEASE 9.5.1 1.7.25 3.1 5.0.9.RELEASE 5.0.9.RELEASE 0.7.9.RELEASE 4.1.29.Final 2.9.7 2.1.1 1.4.26 0.6.6 1.3.8 1.2.1 3.1.6.RELEASE 2.2.2 1.0.1 0.0.1 2.0.5.RELEASE 2.0.5.RELEASE 4.12 3.9.0 2.19.0 9.4.12.v20180830 1.8.0-beta0 1.3 3.0.0 1.9.5 2.11.1 20.0 0.7.7.201606060606 4.1.0 3.5.1 2.19.1 2.3 io.github.reactivefeign feign-reactor-core ${project.version} io.github.reactivefeign feign-reactor-webclient ${project.version} io.github.reactivefeign feign-reactor-cloud ${project.version} io.github.openfeign feign-core ${feign.version} io.projectreactor reactor-core ${reactor.version} io.projectreactor reactor-test ${reactor.version} test commons-httpclient commons-httpclient ${commons-httpclient.version} org.slf4j slf4j-api ${slf4j.version} org.springframework spring-webflux ${spring-webflux.version} org.springframework spring-web ${spring-web.version} io.projectreactor.ipc reactor-netty ${reactor-netty.version} io.netty netty-all ${netty-all.version} com.fasterxml.jackson jackson-bom ${jackson.version} import pom com.netflix.hystrix hystrix-core ${hystrix.version} com.netflix.archaius archaius-core ${archaius.version} com.netflix.ribbon ribbon-core ${ribbon-version} io.reactivex rxjava ${rxjava.version} io.reactivex rxjava-reactive-streams ${rxjava-reactive-streams.version} com.netflix.ribbon ribbon-loadbalancer ${ribbon-version} io.projectreactor.addons reactor-adapter ${reactor-adapter.version} io.reactivex.rxjava2 rxjava ${rxjava2.version} org.eclipse.jetty jetty-reactive-httpclient ${jetty-reactive-httpclient.version} io.kptfh.reactivejson json-reactor ${json-reactor.version} junit junit ${junit.version} test org.springframework.boot spring-boot-starter-webflux spring-boot-starter-logging org.springframework.boot ${spring-boot-starter-webflux.version} test org.springframework.boot spring-boot-starter-test ${spring-boot-starter.version} test org.hamcrest hamcrest-library ${hamcrest.version} test org.awaitility awaitility ${awaitility.version} test org.mockito mockito-all ${mockito.version} test org.assertj assertj-core ${assertj.version} test com.github.tomakehurst wiremock ${wiremock.version} test org.eclipse.jetty jetty-bom ${jetty.version} import pom com.fasterxml.jackson.datatype jackson-datatype-jsr310 ${jackson.version} org.apache.logging.log4j log4j-slf4j-impl ${log4j.version} test org.apache.logging.log4j log4j-api ${log4j.version} test com.google.guava guava ${guava.version} test bintray-kptfh-feign-reactive bintray https://dl.bintray.com/kptfh/json-reactive org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} ${java.version} ${java.version} org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} org.jacoco jacoco-maven-plugin ${jacoco-plugin.version} org.apache.maven.plugins maven-source-plugin attach-sources jar org.eluder.coveralls coveralls-maven-plugin ${coveralls-plugin.version} org.apache.maven.plugins maven-javadoc-plugin attach-javadocs jar maven-release-plugin 2.4.1 false release true bintray-kptfh-feign-reactive kptfh-feign-reactive https://api.bintray.com/maven/kptfh/feign-reactive/client/;publish=1 org.codehaus.mojo versions-maven-plugin ${versions-maven-plugin.version} dependency-updates-report plugin-updates-report property-updates-report ================================================ FILE: settings.xml ================================================ bintray-kptfh-feign-reactive kptfh 4b4f2162048b2d8f2950ebd29fd604232bf5b2e4