├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── feign │ └── reactor │ ├── ReactorDelegatingContract.java │ ├── ReactorFeign.java │ └── ReactorInvocationHandler.java └── test └── java └── feign └── reactor ├── ReactorBuilderCompletableFutureTest.java ├── ReactorBuilderFluxTest.java ├── ReactorBuilderMonoTest.java └── ReactorBuilderPlainTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012 Netflix, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reactor core for Feign 2 | =================== 3 | WARNING this module only support Java 8 ! 4 | 5 | This module wraps Feign's http requests in [Reactor core](https://github.com/reactor/reactor-core). 6 | 7 | To use Reactor with Feign, add the Reactor module to your classpath. Then, configure Feign to use the `ReactorFeign`: 8 | 9 | ```java 10 | GitHub github = ReactorFeign.builder() 11 | .target(GitHub.class, "https://api.github.com"); 12 | ``` 13 | 14 | For asynchronous, return `CompletableFuture`. 15 | 16 | For Reactor compatibility or reactive use, return `Mono` or `Flux`, of package `reactor.core.publisher`. 17 | 18 | ```java 19 | interface YourApi { 20 | @RequestLine("GET /yourtype/{id}") 21 | CompletableFuture getYourType(@Param("id") String id); 22 | 23 | @RequestLine("GET /yourtype/{id}") 24 | Mono getYourTypeObservable(@Param("id") String id); 25 | 26 | @RequestLine("GET /yourtype/{id}") 27 | Flux getYourTypeSingle(@Param("id") String id); 28 | 29 | @RequestLine("GET /yourtype/{id}") 30 | YourType getYourTypeSynchronous(@Param("id") String id); 31 | } 32 | 33 | YourApi api = ReactorFeign.builder() 34 | .target(YourApi.class, "https://example.com"); 35 | ``` 36 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.github.ilaborie 6 | feign-reactor 7 | 0.0.2 8 | Feign Reactor 9 | 10 | 11 | 1.8 12 | 1.8 13 | UTF-8 14 | 15 | 16 | 9.3.1 17 | 3.0.3.RELEASE 18 | 19 | 20 | 4.12 21 | 3.5.2 22 | 3.4.2 23 | 24 | 25 | 26 | 27 | 28 | io.github.openfeign 29 | feign-core 30 | ${feign.version} 31 | 32 | 33 | 34 | io.projectreactor 35 | reactor-core 36 | ${reactor.core.version} 37 | 38 | 39 | 40 | 41 | junit 42 | junit 43 | ${junit.version} 44 | 45 | 46 | org.assertj 47 | assertj-core 48 | ${assertj.version} 49 | 50 | 51 | io.github.openfeign 52 | feign-core 53 | ${feign.version} 54 | test-jar 55 | test 56 | 57 | 58 | io.github.openfeign 59 | feign-gson 60 | ${feign.version} 61 | test 62 | 63 | 64 | com.squareup.okhttp3 65 | mockwebserver 66 | ${mockwebserver.version} 67 | test 68 | 69 | 70 | io.projectreactor.addons 71 | reactor-test 72 | ${reactor.core.version} 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/feign/reactor/ReactorDelegatingContract.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | import feign.Contract; 4 | import feign.MethodMetadata; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | import java.util.List; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.function.UnaryOperator; 13 | 14 | import static feign.Util.resolveLastTypeParameter; 15 | import static java.util.stream.Collectors.toList; 16 | 17 | /** 18 | * This special cases methods that return {@link Flux}, {@link Mono}, or {@link CompletableFuture} so that they 19 | * are decoded properly. 20 | *

21 | *

For example, {@literal Mono} and {@literal CompletableFuture} will decode {@code Foo}. 22 | */ 23 | final class ReactorDelegatingContract implements Contract { 24 | 25 | private final Contract delegate; 26 | 27 | ReactorDelegatingContract(Contract delegate) { 28 | this.delegate = delegate; 29 | } 30 | 31 | @Override 32 | public List parseAndValidatateMetadata(Class targetType) { 33 | return this.delegate.parseAndValidatateMetadata(targetType).stream() 34 | .map(handleParametrizedType(Flux.class)) 35 | .map(handleParametrizedType(Mono.class)) 36 | .map(handleParametrizedType(CompletableFuture.class)) 37 | .collect(toList()); 38 | } 39 | 40 | private UnaryOperator handleParametrizedType(Class clazz) { 41 | return metadata -> { 42 | Type type = metadata.returnType(); 43 | if (type instanceof ParameterizedType && 44 | ((ParameterizedType) type).getRawType().equals(clazz)) { 45 | Type actualType = resolveLastTypeParameter(type, clazz); 46 | metadata.returnType(actualType); 47 | } 48 | return metadata; 49 | }; 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/feign/reactor/ReactorFeign.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | 4 | import feign.*; 5 | import feign.codec.Decoder; 6 | import feign.codec.Encoder; 7 | import feign.codec.ErrorDecoder; 8 | 9 | /** 10 | * Allows Feign interfaces to return Mono, Flux Plublisher or CompletableFuture. 11 | */ 12 | public final class ReactorFeign { 13 | 14 | public static Builder builder() { 15 | return new Builder(); 16 | } 17 | 18 | public static final class Builder extends Feign.Builder { 19 | 20 | private Contract contract = new Contract.Default(); 21 | 22 | @Override 23 | public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { 24 | throw new UnsupportedOperationException("A custom InvocationHandler is already used internaly"); 25 | } 26 | 27 | @Override 28 | public Builder contract(Contract contract) { 29 | this.contract = contract; 30 | return this; 31 | } 32 | 33 | @Override 34 | public Feign build() { 35 | super.invocationHandlerFactory(ReactorInvocationHandler::new); 36 | super.contract(new ReactorDelegatingContract(contract)); 37 | return super.build(); 38 | } 39 | 40 | // Covariant overrides to support chaining method. 41 | @Override 42 | public Builder logLevel(Logger.Level logLevel) { 43 | return (Builder) super.logLevel(logLevel); 44 | } 45 | 46 | @Override 47 | public Builder client(Client client) { 48 | return (Builder) super.client(client); 49 | } 50 | 51 | @Override 52 | public Builder retryer(Retryer retryer) { 53 | return (Builder) super.retryer(retryer); 54 | } 55 | 56 | @Override 57 | public Builder logger(Logger logger) { 58 | return (Builder) super.logger(logger); 59 | } 60 | 61 | @Override 62 | public Builder encoder(Encoder encoder) { 63 | return (Builder) super.encoder(encoder); 64 | } 65 | 66 | @Override 67 | public Builder decoder(Decoder decoder) { 68 | return (Builder) super.decoder(decoder); 69 | } 70 | 71 | @Override 72 | public Builder decode404() { 73 | return (Builder) super.decode404(); 74 | } 75 | 76 | @Override 77 | public Builder errorDecoder(ErrorDecoder errorDecoder) { 78 | return (Builder) super.errorDecoder(errorDecoder); 79 | } 80 | 81 | @Override 82 | public Builder options(Request.Options options) { 83 | return (Builder) super.options(options); 84 | } 85 | 86 | @Override 87 | public Builder requestInterceptor(RequestInterceptor requestInterceptor) { 88 | return (Builder) super.requestInterceptor(requestInterceptor); 89 | } 90 | 91 | @Override 92 | public Builder requestInterceptors(Iterable requestInterceptors) { 93 | return (Builder) super.requestInterceptors(requestInterceptors); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/feign/reactor/ReactorInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | 4 | import feign.InvocationHandlerFactory.MethodHandler; 5 | import feign.Target; 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | 9 | import java.lang.reflect.InvocationHandler; 10 | import java.lang.reflect.Method; 11 | import java.lang.reflect.Proxy; 12 | import java.util.Map; 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | import static java.util.Objects.requireNonNull; 16 | 17 | final class ReactorInvocationHandler implements InvocationHandler { 18 | 19 | private final Target target; 20 | private final Map dispatch; 21 | 22 | ReactorInvocationHandler(Target target, Map dispatch) { 23 | this.target = requireNonNull(target, "target"); 24 | this.dispatch = requireNonNull(dispatch, "dispatch"); 25 | } 26 | 27 | 28 | @Override 29 | public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { 30 | // handling standard object methods 31 | if ("equals".equals(method.getName())) { 32 | try { 33 | boolean hasArgs = args.length > 0 && args[0] != null; 34 | Object otherHandler = hasArgs ? Proxy.getInvocationHandler(args[0]) : null; 35 | return equals(otherHandler); 36 | } catch (IllegalArgumentException e) { 37 | return false; 38 | } 39 | } else if ("hashCode".equals(method.getName())) { 40 | return hashCode(); 41 | } else if ("toString".equals(method.getName())) { 42 | return toString(); 43 | } 44 | 45 | // dealing with Reactor types 46 | Class returnType = method.getReturnType(); 47 | if (Mono.class.isAssignableFrom(returnType)) { 48 | return createMono(method, args); 49 | } else if (Flux.class.isAssignableFrom(returnType)) { 50 | return Flux.from(createMono(method, args)); 51 | } else if (CompletableFuture.class.isAssignableFrom(returnType)) { 52 | return createMono(method, args).toFuture(); 53 | } 54 | // Default case 55 | return this.dispatch.get(method).invoke(args); 56 | } 57 | 58 | private Mono createMono(Method method, Object[] args) { 59 | return Mono.defer(() -> { 60 | try { 61 | Object value = this.dispatch.get(method).invoke(args); 62 | return Mono.just(value); 63 | } catch (Throwable throwable) { 64 | return Mono.error(throwable); 65 | } 66 | }); 67 | } 68 | 69 | @Override 70 | public boolean equals(Object obj) { 71 | if (obj instanceof ReactorInvocationHandler) { 72 | ReactorInvocationHandler other = (ReactorInvocationHandler) obj; 73 | return target.equals(other.target); 74 | } 75 | return false; 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | return target.hashCode(); 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return target.toString(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/feign/reactor/ReactorBuilderCompletableFutureTest.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import feign.gson.GsonDecoder; 6 | import okhttp3.mockwebserver.MockResponse; 7 | import okhttp3.mockwebserver.MockWebServer; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.ExpectedException; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.ExecutionException; 15 | 16 | import static feign.assertj.MockWebServerAssertions.assertThat; 17 | 18 | public class ReactorBuilderCompletableFutureTest { 19 | 20 | @Rule 21 | public final ExpectedException thrown = ExpectedException.none(); 22 | @Rule 23 | public final MockWebServer server = new MockWebServer(); 24 | 25 | @Test 26 | public void testCompletableFuture() { 27 | server.enqueue(new MockResponse().setBody("\"foo\"")); 28 | TestInterface api = target(); 29 | 30 | CompletableFuture future = api.completableFuture(); 31 | 32 | assertThat(future).isNotNull(); 33 | assertThat(future.join()).isEqualTo("foo"); 34 | } 35 | 36 | @Test 37 | public void testCompletableFutureList() throws ExecutionException, InterruptedException { 38 | server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]")); 39 | TestInterface api = target(); 40 | 41 | CompletableFuture> future = api.listCompletableFuture(); 42 | 43 | assertThat(future.get()).isNotNull().containsExactly("foo", "bar"); 44 | } 45 | 46 | @Test 47 | public void testCompletableFutureInt() { 48 | server.enqueue(new MockResponse().setBody("42")); 49 | TestInterface api = target(); 50 | 51 | CompletableFuture future = api.intCompletableFuture(); 52 | 53 | assertThat(future).isNotNull(); 54 | assertThat(future.join()).isEqualTo(42); 55 | } 56 | 57 | 58 | private TestInterface target() { 59 | return ReactorFeign.builder() 60 | .decoder(new GsonDecoder()) 61 | .target(TestInterface.class, "http://localhost:" + server.getPort()); 62 | } 63 | 64 | 65 | interface TestInterface { 66 | @RequestLine("GET /") 67 | @Headers("Accept: application/json") 68 | CompletableFuture> listCompletableFuture(); 69 | 70 | @RequestLine("GET /") 71 | @Headers("Accept: application/json") 72 | CompletableFuture completableFuture(); 73 | 74 | @RequestLine("GET /") 75 | @Headers("Accept: application/json") 76 | CompletableFuture intCompletableFuture(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/feign/reactor/ReactorBuilderFluxTest.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import feign.gson.GsonDecoder; 6 | import okhttp3.mockwebserver.MockResponse; 7 | import okhttp3.mockwebserver.MockWebServer; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.ExpectedException; 11 | import reactor.core.publisher.Flux; 12 | import reactor.test.StepVerifier; 13 | 14 | import java.util.List; 15 | 16 | import static feign.assertj.MockWebServerAssertions.assertThat; 17 | 18 | public class ReactorBuilderFluxTest { 19 | 20 | @Rule 21 | public final ExpectedException thrown = ExpectedException.none(); 22 | @Rule 23 | public final MockWebServer server = new MockWebServer(); 24 | 25 | @Test 26 | public void testFlux() { 27 | server.enqueue(new MockResponse().setBody("\"foo\"")); 28 | TestInterface api = target(); 29 | 30 | Flux flux = api.flux(); 31 | 32 | assertThat(flux).isNotNull(); 33 | StepVerifier.create(flux) 34 | .expectNext("foo") 35 | .expectComplete() 36 | .verify(); 37 | } 38 | 39 | @Test 40 | public void testFluxList() { 41 | server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]")); 42 | TestInterface api = target(); 43 | 44 | Flux> flux = api.listFlux(); 45 | 46 | assertThat(flux).isNotNull(); 47 | StepVerifier.create(flux) 48 | .expectNextMatches(list -> { 49 | assertThat(list).isNotNull().containsExactly("foo", "bar"); 50 | return true; 51 | }) 52 | .expectComplete() 53 | .verify(); 54 | } 55 | 56 | @Test 57 | public void testFluxInt() { 58 | server.enqueue(new MockResponse().setBody("42")); 59 | TestInterface api = target(); 60 | 61 | Flux flux = api.intFlux(); 62 | 63 | assertThat(flux).isNotNull(); 64 | StepVerifier.create(flux) 65 | .expectNext(42) 66 | .expectComplete() 67 | .verify(); 68 | } 69 | 70 | 71 | private TestInterface target() { 72 | return ReactorFeign.builder() 73 | .decoder(new GsonDecoder()) 74 | .target(TestInterface.class, "http://localhost:" + server.getPort()); 75 | } 76 | 77 | 78 | interface TestInterface { 79 | @RequestLine("GET /") 80 | @Headers("Accept: application/json") 81 | Flux> listFlux(); 82 | 83 | @RequestLine("GET /") 84 | @Headers("Accept: application/json") 85 | Flux flux(); 86 | 87 | @RequestLine("GET /") 88 | @Headers("Accept: application/json") 89 | Flux intFlux(); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/feign/reactor/ReactorBuilderMonoTest.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import feign.gson.GsonDecoder; 6 | import okhttp3.mockwebserver.MockResponse; 7 | import okhttp3.mockwebserver.MockWebServer; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.ExpectedException; 11 | import reactor.core.publisher.Mono; 12 | import reactor.test.StepVerifier; 13 | 14 | import java.util.List; 15 | 16 | import static feign.assertj.MockWebServerAssertions.assertThat; 17 | 18 | public class ReactorBuilderMonoTest { 19 | 20 | @Rule 21 | public final ExpectedException thrown = ExpectedException.none(); 22 | @Rule 23 | public final MockWebServer server = new MockWebServer(); 24 | 25 | @Test 26 | public void testMono() { 27 | server.enqueue(new MockResponse().setBody("\"foo\"")); 28 | TestInterface api = target(); 29 | 30 | Mono mono = api.mono(); 31 | 32 | assertThat(mono).isNotNull(); 33 | StepVerifier.create(mono) 34 | .expectNext("foo") 35 | .expectComplete() 36 | .verify(); 37 | } 38 | 39 | @Test 40 | public void testMonoList() { 41 | server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]")); 42 | TestInterface api = target(); 43 | 44 | Mono> mono = api.listMono(); 45 | 46 | assertThat(mono).isNotNull(); 47 | 48 | 49 | StepVerifier.create(mono) 50 | .expectNextMatches(list -> { 51 | assertThat(list).isNotNull().containsExactly("foo", "bar"); 52 | return true; 53 | }) 54 | .expectComplete() 55 | .verify(); 56 | } 57 | 58 | @Test 59 | public void testMonoInt() { 60 | server.enqueue(new MockResponse().setBody("42")); 61 | TestInterface api = target(); 62 | 63 | Mono mono = api.intMono(); 64 | 65 | assertThat(mono).isNotNull(); 66 | StepVerifier.create(mono) 67 | .expectNext(42) 68 | .expectComplete() 69 | .verify(); 70 | } 71 | 72 | 73 | private TestInterface target() { 74 | return ReactorFeign.builder() 75 | .decoder(new GsonDecoder()) 76 | .target(TestInterface.class, "http://localhost:" + server.getPort()); 77 | } 78 | 79 | 80 | interface TestInterface { 81 | @RequestLine("GET /") 82 | @Headers("Accept: application/json") 83 | Mono> listMono(); 84 | 85 | @RequestLine("GET /") 86 | @Headers("Accept: application/json") 87 | Mono mono(); 88 | 89 | @RequestLine("GET /") 90 | @Headers("Accept: application/json") 91 | Mono intMono(); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/feign/reactor/ReactorBuilderPlainTest.java: -------------------------------------------------------------------------------- 1 | package feign.reactor; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import feign.Target; 6 | import feign.Target.HardCodedTarget; 7 | import feign.gson.GsonDecoder; 8 | import okhttp3.mockwebserver.MockResponse; 9 | import okhttp3.mockwebserver.MockWebServer; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.rules.ExpectedException; 13 | 14 | import java.util.List; 15 | 16 | import static feign.assertj.MockWebServerAssertions.assertThat; 17 | 18 | public class ReactorBuilderPlainTest { 19 | 20 | @Rule 21 | public final ExpectedException thrown = ExpectedException.none(); 22 | @Rule 23 | public final MockWebServer server = new MockWebServer(); 24 | 25 | @Test 26 | public void plainString() { 27 | server.enqueue(new MockResponse().setBody("\"foo\"")); 28 | TestInterface api = target(); 29 | 30 | String string = api.get(); 31 | assertThat(string).isEqualTo("foo"); 32 | } 33 | 34 | @Test 35 | public void plainList() { 36 | server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]")); 37 | TestInterface api = target(); 38 | 39 | List list = api.getList(); 40 | 41 | assertThat(list).isNotNull().containsExactly("foo", "bar"); 42 | } 43 | 44 | @Test 45 | public void plainInt() { 46 | server.enqueue(new MockResponse().setBody("42")); 47 | TestInterface api = target(); 48 | 49 | Integer value = api.getInt(); 50 | 51 | assertThat(value).isEqualTo(42); 52 | } 53 | 54 | 55 | @Test 56 | public void equalsHashCodeAndToStringWork() { 57 | Target t1 = 58 | new HardCodedTarget<>(TestInterface.class, "http://localhost:8080"); 59 | Target t2 = 60 | new HardCodedTarget<>(TestInterface.class, "http://localhost:8888"); 61 | Target t3 = 62 | new HardCodedTarget<>(OtherTestInterface.class, "http://localhost:8080"); 63 | TestInterface i1 = ReactorFeign.builder().target(t1); 64 | TestInterface i2 = ReactorFeign.builder().target(t1); 65 | TestInterface i3 = ReactorFeign.builder().target(t2); 66 | OtherTestInterface i4 = ReactorFeign.builder().target(t3); 67 | 68 | assertThat(i1) 69 | .isEqualTo(i2) 70 | .isNotEqualTo(i3) 71 | .isNotEqualTo(i4); 72 | 73 | assertThat(i1.hashCode()) 74 | .isEqualTo(i2.hashCode()) 75 | .isNotEqualTo(i3.hashCode()) 76 | .isNotEqualTo(i4.hashCode()); 77 | 78 | assertThat(i1.toString()) 79 | .isEqualTo(i2.toString()) 80 | .isNotEqualTo(i3.toString()) 81 | .isNotEqualTo(i4.toString()); 82 | 83 | assertThat(t1) 84 | .isNotEqualTo(i1); 85 | 86 | assertThat(t1.hashCode()) 87 | .isEqualTo(i1.hashCode()); 88 | 89 | assertThat(t1.toString()) 90 | .isEqualTo(i1.toString()); 91 | } 92 | 93 | private TestInterface target() { 94 | return ReactorFeign.builder() 95 | .decoder(new GsonDecoder()) 96 | .target(TestInterface.class, "http://localhost:" + server.getPort()); 97 | } 98 | 99 | interface OtherTestInterface { 100 | 101 | @RequestLine("GET /") 102 | @Headers("Accept: application/json") 103 | List list(); 104 | } 105 | 106 | interface TestInterface { 107 | 108 | @RequestLine("GET /") 109 | @Headers("Accept: application/json") 110 | String get(); 111 | 112 | @RequestLine("GET /") 113 | @Headers("Accept: application/json") 114 | List getList(); 115 | 116 | @RequestLine("GET /") 117 | Integer getInt(); 118 | } 119 | 120 | } 121 | --------------------------------------------------------------------------------