├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── run-tests.zsh ├── settings.xml └── src ├── main └── java │ └── feign │ ├── VertxBuildTemplateByResolvingArgs.java │ ├── VertxFeign.java │ ├── VertxInvocationHandler.java │ ├── VertxMethodHandler.java │ ├── package-info.java │ └── vertx │ ├── VertxDelegatingContract.java │ └── VertxHttpClient.java └── test ├── java └── feign │ └── vertx │ ├── AbstractClientReconnectTest.java │ ├── AbstractFeignVertxTest.java │ ├── ConnectionsLeakTests.java │ ├── Http11ClientReconnectTest.java │ ├── Http2ClientReconnectTest.java │ ├── QueryMapEncoderTest.java │ ├── RawContractTest.java │ ├── RequestPreProcessorTest.java │ ├── RetryingTest.java │ ├── TestUtils.java │ ├── TimeoutHandlingTest.java │ ├── VertxHttpClientTest.java │ ├── VertxHttpOptionsTest.java │ └── testcase │ ├── HelloServiceAPI.java │ ├── IcecreamServiceApi.java │ ├── IcecreamServiceApiBroken.java │ ├── RawServiceAPI.java │ └── domain │ ├── Bill.java │ ├── Flavor.java │ ├── IceCreamOrder.java │ ├── Mixin.java │ └── OrderGenerator.java └── resources └── log4j.properties /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | ij_continuation_indent_size = 4 11 | ij_visual_guides = 120 12 | 13 | [*.java] 14 | ij_java_align_multiline_parameters = false 15 | ij_java_generate_final_locals = true 16 | ij_java_generate_final_parameters = true 17 | ij_java_keep_blank_lines_before_right_brace = 0 18 | ij_java_keep_blank_lines_between_package_declaration_and_header = 1 19 | ij_java_keep_blank_lines_in_code = 1 20 | ij_java_keep_blank_lines_in_declarations = 1 21 | ij_java_space_after_comma = true 22 | ij_java_class_count_to_use_import_on_demand = 999 23 | ij_java_names_count_to_use_import_on_demand = 999 24 | 25 | # Doc: https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0 26 | # "all_others" "javax" "java" "static" 27 | ij_java_imports_layout = *,|,javax.**,java.**,|,$* 28 | ij_java_layout_static_imports_separately = true 29 | 30 | [pom.xml] 31 | indent_size = 4 32 | 33 | [*.md] 34 | trim_trailing_whitespace = false 35 | 36 | # Properties are expected to be encoded in ISO-8859-1 37 | # See https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html#load-java.io.InputStream- 38 | [*.properties] 39 | charset = latin1 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | env: 9 | jdk_version: 8 10 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 11 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 12 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 13 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup JDK ${{ env.jdk_version }} 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: ${{ env.jdk_version }} 22 | 23 | - name: Configure GPG Key 24 | run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import 25 | 26 | - name: Build 27 | run: mvn clean install 28 | 29 | - name: Cross-version tests 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install zsh 33 | ./run-tests.zsh 34 | 35 | - name: Publish release 36 | if: startsWith(github.ref, 'refs/tags/') 37 | run: mvn clean install source:jar javadoc:jar deploy -DskipTests=true --settings settings.xml 38 | 39 | - name: Report coverage 40 | if: github.ref == 'refs/heads/master' 41 | run: mvn clean compile jacoco:prepare-agent test jacoco:report coveralls:report -DrepoToken=${{ env.COVERALLS_REPO_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | feign-vertx.iml 3 | target 4 | 5 | # NonDex generated files 6 | .nondex -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feign-vertx 2 | 3 | [![CI](https://github.com/OpenFeign/feign-vertx/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/OpenFeign/feign-vertx/actions/workflows/ci.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/hosuaby/vertx-feign/badge.svg?branch=master)](https://coveralls.io/github/hosuaby/vertx-feign?branch=master) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.openfeign/feign-vertx/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.openfeign/feign-vertx) 6 | [![javadoc](https://javadoc.io/badge2/io.github.openfeign/feign-vertx/javadoc.svg)](https://javadoc.io/doc/io.github.openfeign/feign-vertx) 7 | 8 | Implementation of Feign on Vertx. Brings you the best of two worlds together : 9 | concise syntax of Feign to write client side API on fast, asynchronous and 10 | non-blocking HTTP client of Vertx. 11 | 12 | ## Installation 13 | 14 | ### With Maven 15 | 16 | ```xml 17 | 18 | ... 19 | 20 | io.github.openfeign 21 | feign-vertx 22 | 6.0.1 23 | 24 | ... 25 | 26 | ``` 27 | 28 | ### With Gradle 29 | 30 | ```groovy 31 | compile group: 'io.github.openfeign', name: 'feign-vertx', version: '6.0.1' 32 | ``` 33 | 34 | ## Compatibility 35 | 36 | Feign | feign-vertx | Vertx 37 | ---------------------- |-------------| ---------------------- 38 | 8.x | 1.x+ | 3.5.x - 3.9.x (except 3.5.2) 39 | 9.x | 2.x+ | 3.5.x - 3.9.x (except 3.5.2) 40 | 10.x (except 10.5.0) | 3.x+ | 3.5.x - 3.9.x (except 3.5.2) 41 | 11.x | 4.x+ | 3.5.x - 3.9.x (except 3.5.2) 42 | 11.x | 5.x+ | 4.x 43 | 12.x | unsupported | 44 | 13.x | 6.x+ | 4.x 45 | 46 | ## Usage 47 | 48 | Write Feign API as usual, but every method of interface must return 49 | `io.vertx.core.Future`. 50 | 51 | ```java 52 | @Headers({ "Accept: application/json" }) 53 | interface IcecreamServiceApi { 54 | 55 | @RequestLine("GET /icecream/flavors") 56 | Future> getAvailableFlavors(); 57 | 58 | @RequestLine("GET /icecream/mixins") 59 | Future> getAvailableMixins(); 60 | 61 | @RequestLine("POST /icecream/orders") 62 | @Headers("Content-Type: application/json") 63 | Future makeOrder(IceCreamOrder order); 64 | 65 | @RequestLine("GET /icecream/orders/{orderId}") 66 | Future findOrder(@Param("orderId") int orderId); 67 | 68 | @RequestLine("POST /icecream/bills/pay") 69 | @Headers("Content-Type: application/json") 70 | Future payBill(Bill bill); 71 | } 72 | ``` 73 | Build the client : 74 | 75 | ```java 76 | Vertx vertx = Vertx.vertx(); // get Vertx instance 77 | 78 | /* Create instance of your API */ 79 | IcecreamServiceApi icecreamApi = VertxFeign 80 | .builder() 81 | .vertx(vertx) // provide vertx instance 82 | .encoder(new JacksonEncoder()) 83 | .decoder(new JacksonDecoder()) 84 | .target(IcecreamServiceApi.class, "http://www.icecreame.com"); 85 | 86 | /* Execute requests asynchronously */ 87 | Future> flavorsFuture = icecreamApi.getAvailableFlavors(); 88 | Future> mixinsFuture = icecreamApi.getAvailableMixins(); 89 | ``` 90 | 91 | ## License 92 | 93 | Library distributed under Apache License Version 2.0. 94 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 4.0.0 7 | 8 | io.github.openfeign 9 | feign-vertx 10 | 6.0.1 11 | jar 12 | 13 | feign-vertx 14 | Use Feign client on Vert.x 15 | https://github.com/OpenFeign/feign-vertx 16 | 17 | 18 | OpenFeign 19 | https://github.com/openfeign 20 | 21 | 22 | 23 | Github 24 | https://github.com/OpenFeign/feign-vertx/issues 25 | 26 | 27 | 28 | https://github.com/OpenFeign/feign-vertx 29 | scm:git:https://github.com/OpenFeign/feign-vertx.git 30 | scm:git:https://github.com/OpenFeign/feign-vertx.git 31 | HEAD 32 | 33 | 34 | 35 | 36 | The Apache Software License, Version 2.0 37 | http://www.apache.org/licenses/LICENSE-2.0.txt 38 | repo 39 | 40 | 41 | 42 | 43 | 44 | hosuaby 45 | Alexei KLENIN 46 | alexei.klenin@gmail.com 47 | 48 | 49 | 50 | 51 | 1.8 52 | ${java.version} 53 | ${java.version} 54 | UTF-8 55 | UTF-8 56 | 57 | 4.5.10 58 | 13.5 59 | 60 | 61 | 5.7.0 62 | 3.18.1 63 | 2.35.1 64 | 2.12.0 65 | 1.8.0-beta0 66 | 67 | 68 | 0.8.6 69 | 4.3.0 70 | 71 | 3.8.1 72 | 3.0.0-M5 73 | 3.2.0 74 | 2.8.1 75 | 1.6 76 | 77 | 78 | 79 | 80 | 81 | com.fasterxml.jackson 82 | jackson-bom 83 | ${jackson.version} 84 | import 85 | pom 86 | 87 | 88 | org.junit 89 | junit-bom 90 | ${junit.version} 91 | pom 92 | import 93 | 94 | 95 | 96 | 97 | 98 | 99 | io.vertx 100 | vertx-core 101 | ${vertx.version} 102 | provided 103 | 104 | 105 | io.vertx 106 | vertx-web-client 107 | ${vertx.version} 108 | 109 | 110 | 111 | io.github.openfeign 112 | feign-core 113 | ${feign.version} 114 | provided 115 | 116 | 117 | 118 | 119 | org.junit.jupiter 120 | junit-jupiter-api 121 | test 122 | 123 | 124 | 125 | io.vertx 126 | vertx-junit5 127 | ${vertx.version} 128 | test 129 | 130 | 131 | 132 | org.assertj 133 | assertj-core 134 | ${assertj.version} 135 | test 136 | 137 | 138 | 139 | com.github.tomakehurst 140 | wiremock-jre8 141 | ${wiremock.version} 142 | test 143 | 144 | 145 | org.junit 146 | junit-bom 147 | 148 | 149 | 150 | 151 | 152 | com.fasterxml.jackson.core 153 | jackson-annotations 154 | test 155 | 156 | 157 | 158 | com.fasterxml.jackson.datatype 159 | jackson-datatype-jsr310 160 | test 161 | 162 | 163 | 164 | io.github.openfeign 165 | feign-jackson 166 | ${feign.version} 167 | test 168 | 169 | 170 | 171 | io.github.openfeign 172 | feign-slf4j 173 | ${feign.version} 174 | test 175 | 176 | 177 | 178 | org.slf4j 179 | slf4j-log4j12 180 | ${slf4j-log4j12.version} 181 | test 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | org.apache.maven.plugins 190 | maven-compiler-plugin 191 | ${maven-compiler-plugin.version} 192 | 193 | ${java.version} 194 | ${java.version} 195 | 196 | 197 | 198 | 199 | org.apache.maven.plugins 200 | maven-surefire-plugin 201 | ${maven-surefire-plugin.version} 202 | 203 | 204 | 205 | org.apache.maven.plugins 206 | maven-gpg-plugin 207 | ${maven-gpg-plugin.version} 208 | 209 | 210 | 211 | 212 | org.jacoco 213 | jacoco-maven-plugin 214 | ${jacoco-plugin.version} 215 | 216 | 217 | 218 | 219 | org.apache.maven.plugins 220 | maven-source-plugin 221 | 222 | 223 | attach-sources 224 | 225 | jar 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | org.eluder.coveralls 234 | coveralls-maven-plugin 235 | ${coveralls-plugin.version} 236 | 237 | 238 | 239 | 240 | org.apache.maven.plugins 241 | maven-javadoc-plugin 242 | 243 | 244 | attach-javadocs 245 | 246 | jar 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | org.apache.maven.plugins 257 | maven-surefire-plugin 258 | 259 | 260 | org.junit.jupiter 261 | junit-jupiter-engine 262 | ${junit.version} 263 | 264 | 265 | 266 | 267 | 268 | org.apache.maven.plugins 269 | maven-javadoc-plugin 270 | ${maven-javadoc-plugin.version} 271 | 272 | 8 273 | 274 | 275 | 276 | 277 | org.apache.maven.plugins 278 | maven-gpg-plugin 279 | 280 | 281 | sign-artifacts 282 | verify 283 | 284 | sign 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | org.jacoco 293 | jacoco-maven-plugin 294 | 295 | 296 | prepare-agent 297 | 298 | prepare-agent 299 | 300 | 301 | 302 | report 303 | prepare-package 304 | 305 | report 306 | 307 | 308 | 309 | post-unit-test 310 | test 311 | 312 | report 313 | 314 | 315 | 316 | 317 | target/jacoco.exec 318 | 319 | target/jacoco-ut 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | ossrh 331 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 332 | 333 | 334 | 335 | 336 | 337 | 338 | org.codehaus.mojo 339 | versions-maven-plugin 340 | ${versions-maven-plugin.version} 341 | 342 | 343 | 344 | dependency-updates-report 345 | plugin-updates-report 346 | property-updates-report 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | -------------------------------------------------------------------------------- /run-tests.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | RED='\033[0;31m' 4 | GREEN='\033[0;32m' 5 | NC='\033[0m' 6 | 7 | CHECK_CHAR='\U2713' 8 | CROSS_CHAR='\U2717' 9 | 10 | function print_result() { 11 | version=$1 12 | result=$2 13 | 14 | if [[ $result == 0 ]]; 15 | then 16 | mark=$CHECK_CHAR 17 | color=$GREEN 18 | else 19 | mark=$CROSS_CHAR 20 | color=$RED 21 | fi 22 | 23 | echo "\t${color}${version} ${mark}${NC}" 24 | } 25 | 26 | feign_versions=( "12.5" "13.5" ) 27 | 28 | for feign_version in $feign_versions; do 29 | echo "Tests with Feign ${version}:" 30 | 31 | printf "\tRun tests with Feign %s...\n" "${feign_version}" 32 | mvn clean compile test -Dfeign.version="$feign_version" &> /dev/null 33 | print_result "$feign_version" $? 34 | done 35 | 36 | declare -A vertx_versions 37 | vertx_versions=( [v40x]="4.0.x", [v41x]="4.1.x", [v42x]="4.2.x", [v43x]="4.3.x", [v44x]="4.4.x", [v45x]="4.5.x" ) 38 | v40x=( "4.0.2" ) 39 | v41x=( "4.1.8" ) 40 | v42x=( "4.2.7" ) 41 | v43x=( "4.3.2" ) 42 | v44x=( "4.4.9" ) 43 | v45x=( "4.5.10" ) 44 | 45 | for version in ${(k)vertx_versions}; do 46 | echo "Tests with Vertx ${vertx_versions[${version}]}:" 47 | 48 | for vertx_version in ${(P)version}; do 49 | printf "\tRun tests with Vertx %s...\n" "${vertx_version}" 50 | mvn clean compile test -Dvertx.version="$vertx_version" &> /dev/null 51 | print_result "$vertx_version" $? 52 | done 53 | done 54 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | ossrh 8 | ${env.SONATYPE_USER} 9 | ${env.SONATYPE_PASSWORD} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/feign/VertxBuildTemplateByResolvingArgs.java: -------------------------------------------------------------------------------- 1 | package feign; 2 | 3 | import static feign.Util.checkNotNull; 4 | 5 | import feign.codec.EncodeException; 6 | import feign.codec.Encoder; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | /** 15 | * Copy of private {@code ReflectiveFeign.BuildTemplateByResolvingArgs}. 16 | */ 17 | class VertxBuildTemplateByResolvingArgs implements RequestTemplate.Factory { 18 | private final QueryMapEncoder queryMapEncoder; 19 | final MethodMetadata metadata; 20 | final Target target; 21 | private final Map indexToExpander = new HashMap<>(); 22 | 23 | VertxBuildTemplateByResolvingArgs( 24 | final MethodMetadata metadata, 25 | final QueryMapEncoder queryMapEncoder, 26 | final Target target) { 27 | this.metadata = metadata; 28 | this.target = target; 29 | this.queryMapEncoder = queryMapEncoder; 30 | 31 | if (metadata.indexToExpander() != null) { 32 | indexToExpander.putAll(metadata.indexToExpander()); 33 | return; 34 | } 35 | 36 | if (metadata.indexToExpanderClass().isEmpty()) { 37 | return; 38 | } 39 | 40 | for (final Map.Entry> indexToExpanderClass : metadata 41 | .indexToExpanderClass().entrySet()) { 42 | try { 43 | indexToExpander.put( 44 | indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance()); 45 | } catch (final InstantiationException | IllegalAccessException exception) { 46 | throw new IllegalStateException(exception); 47 | } 48 | } 49 | } 50 | 51 | @Override 52 | public RequestTemplate create(final Object[] argv) { 53 | final RequestTemplate mutable = new RequestTemplate(metadata.template()); 54 | 55 | if (metadata.urlIndex() != null) { 56 | final int urlIndex = metadata.urlIndex(); 57 | checkNotNull(argv[urlIndex], "URI parameter %s was null", urlIndex); 58 | mutable.insert(0, String.valueOf(argv[urlIndex])); 59 | } 60 | 61 | final Map varBuilder = new HashMap<>(); 62 | 63 | for (final Map.Entry> entry : metadata.indexToName().entrySet()) { 64 | final int i = entry.getKey(); 65 | Object value = argv[entry.getKey()]; 66 | 67 | if (value != null) { // Null values are skipped. 68 | if (indexToExpander.containsKey(i)) { 69 | value = expandElements(indexToExpander.get(i), value); 70 | } 71 | 72 | for (final String name : entry.getValue()) { 73 | varBuilder.put(name, value); 74 | } 75 | } 76 | } 77 | 78 | RequestTemplate template = resolve(argv, mutable, varBuilder); 79 | 80 | if (metadata.queryMapIndex() != null) { 81 | // add query map parameters after initial resolve so that they take 82 | // precedence over any predefined values 83 | Object value = argv[metadata.queryMapIndex()]; 84 | Map queryMap = toQueryMap(value); 85 | template = addQueryMapQueryParameters(queryMap, template); 86 | } 87 | 88 | if (metadata.headerMapIndex() != null) { 89 | // add header map parameters for a resolution of the user pojo object 90 | Object value = argv[metadata.headerMapIndex()]; 91 | Map headerMap = toQueryMap(value); 92 | template = addHeaderMapHeaders(headerMap, template); 93 | } 94 | 95 | return template; 96 | } 97 | 98 | private Map toQueryMap(Object value) { 99 | if (value instanceof Map) { 100 | return (Map) value; 101 | } 102 | try { 103 | return queryMapEncoder.encode(value); 104 | } catch (EncodeException e) { 105 | throw new IllegalStateException(e); 106 | } 107 | } 108 | 109 | private Object expandElements(Param.Expander expander, Object value) { 110 | if (value instanceof Iterable) { 111 | return expandIterable(expander, (Iterable) value); 112 | } 113 | return expander.expand(value); 114 | } 115 | 116 | private List expandIterable(Param.Expander expander, Iterable value) { 117 | List values = new ArrayList<>(); 118 | for (Object element : value) { 119 | if (element != null) { 120 | values.add(expander.expand(element)); 121 | } 122 | } 123 | return values; 124 | } 125 | 126 | @SuppressWarnings("unchecked") 127 | private RequestTemplate addHeaderMapHeaders( 128 | final Map headerMap, 129 | final RequestTemplate mutableRequestTemplate) { 130 | for (final Map.Entry currEntry : headerMap.entrySet()) { 131 | final Object currValue = currEntry.getValue(); 132 | final Collection values = new ArrayList<>(); 133 | 134 | if (currValue instanceof Iterable) { 135 | for (final Object valueObject : (Iterable) currValue) { 136 | values.add(valueObject == null ? null : valueObject.toString()); 137 | } 138 | } else { 139 | values.add(currValue == null ? null : currValue.toString()); 140 | } 141 | 142 | mutableRequestTemplate.header(currEntry.getKey(), values); 143 | } 144 | 145 | return mutableRequestTemplate; 146 | } 147 | 148 | @SuppressWarnings("unchecked") 149 | private RequestTemplate addQueryMapQueryParameters( 150 | final Map queryMap, 151 | final RequestTemplate mutableRequestTemplate) { 152 | for (final Map.Entry currEntry : queryMap.entrySet()) { 153 | final Object currValue = currEntry.getValue(); 154 | final Collection values = new ArrayList<>(); 155 | 156 | if (currValue instanceof Iterable) { 157 | for (final Object valueObject : (Iterable) currValue) { 158 | values.add(valueObject == null ? null : valueObject.toString()); 159 | } 160 | } else { 161 | values.add(currValue == null ? null : currValue.toString()); 162 | } 163 | 164 | mutableRequestTemplate.query(currEntry.getKey(), values); 165 | } 166 | 167 | return mutableRequestTemplate; 168 | } 169 | 170 | protected RequestTemplate resolve( 171 | final Object[] argv, 172 | final RequestTemplate mutable, 173 | final Map variables) { 174 | return mutable.resolve(variables); 175 | } 176 | 177 | /** 178 | * Public copy of {@code ReflectiveFeign.BuildFormEncodedTemplateFromArgs}. 179 | */ 180 | static final class BuildFormEncodedTemplateFromArgs extends VertxBuildTemplateByResolvingArgs { 181 | private final Encoder encoder; 182 | 183 | BuildFormEncodedTemplateFromArgs( 184 | final MethodMetadata metadata, 185 | final QueryMapEncoder queryMapEncoder, 186 | final Target target, 187 | final Encoder encoder) { 188 | super(metadata, queryMapEncoder, target); 189 | this.encoder = encoder; 190 | } 191 | 192 | @Override 193 | protected RequestTemplate resolve( 194 | final Object[] argv, 195 | final RequestTemplate mutable, 196 | final Map variables) { 197 | final Map formVariables = new HashMap<>(); 198 | 199 | for (final Map.Entry entry : variables.entrySet()) { 200 | if (metadata.formParams().contains(entry.getKey())) { 201 | formVariables.put(entry.getKey(), entry.getValue()); 202 | } 203 | } 204 | 205 | try { 206 | encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable); 207 | } catch (final EncodeException encodeException) { 208 | throw encodeException; 209 | } catch (final RuntimeException unexpectedException) { 210 | throw new EncodeException(unexpectedException.getMessage(), unexpectedException); 211 | } 212 | 213 | return super.resolve(argv, mutable, variables); 214 | } 215 | } 216 | 217 | /** 218 | * Public copy of {@code ReflectiveFeign.BuildEncodedTemplateFromArgs}. 219 | */ 220 | static final class BuildEncodedTemplateFromArgs extends VertxBuildTemplateByResolvingArgs { 221 | private final Encoder encoder; 222 | 223 | BuildEncodedTemplateFromArgs( 224 | final MethodMetadata metadata, 225 | final QueryMapEncoder queryMapEncoder, 226 | final Target target, 227 | final Encoder encoder) { 228 | super(metadata, queryMapEncoder, target); 229 | this.encoder = encoder; 230 | } 231 | 232 | @Override 233 | protected RequestTemplate resolve( 234 | final Object[] argv, 235 | final RequestTemplate mutable, 236 | final Map variables) { 237 | final Object body = argv[metadata.bodyIndex()]; 238 | checkNotNull(body, "Body parameter %s was null", metadata.bodyIndex()); 239 | 240 | try { 241 | encoder.encode(body, metadata.bodyType(), mutable); 242 | } catch (final EncodeException encodeException) { 243 | throw encodeException; 244 | } catch (final RuntimeException unexpectedException) { 245 | throw new EncodeException(unexpectedException.getMessage(), unexpectedException); 246 | } 247 | 248 | return super.resolve(argv, mutable, variables); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/feign/VertxFeign.java: -------------------------------------------------------------------------------- 1 | package feign; 2 | 3 | import static feign.Util.checkNotNull; 4 | import static feign.Util.isDefault; 5 | 6 | import feign.InvocationHandlerFactory.MethodHandler; 7 | import feign.codec.Decoder; 8 | import feign.codec.Encoder; 9 | import feign.codec.ErrorDecoder; 10 | import feign.querymap.FieldQueryMapEncoder; 11 | import feign.vertx.VertxDelegatingContract; 12 | import feign.vertx.VertxHttpClient; 13 | import io.vertx.core.Vertx; 14 | import io.vertx.core.buffer.Buffer; 15 | import io.vertx.core.http.HttpClient; 16 | import io.vertx.core.http.HttpClientOptions; 17 | import io.vertx.ext.web.client.HttpRequest; 18 | 19 | import java.lang.reflect.InvocationHandler; 20 | import java.lang.reflect.Method; 21 | import java.lang.reflect.Proxy; 22 | import java.util.ArrayList; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.function.UnaryOperator; 27 | 28 | /** 29 | * Allows Feign interfaces to return Vert.x {@link io.vertx.core.Future Future}s. 30 | * 31 | * @author Alexei KLENIN 32 | * @author Gordon McKinney 33 | */ 34 | public final class VertxFeign extends Feign { 35 | private final ParseHandlersByName targetToHandlersByName; 36 | private final InvocationHandlerFactory factory; 37 | 38 | private VertxFeign( 39 | final ParseHandlersByName targetToHandlersByName, 40 | final InvocationHandlerFactory factory) { 41 | this.targetToHandlersByName = targetToHandlersByName; 42 | this.factory = factory; 43 | } 44 | 45 | public static Builder builder() { 46 | return new Builder(); 47 | } 48 | 49 | @Override 50 | @SuppressWarnings("unchecked") 51 | public T newInstance(final Target target) { 52 | checkNotNull(target, "Argument target must be not null"); 53 | 54 | final Map nameToHandler = targetToHandlersByName.apply(target); 55 | final Map methodToHandler = new HashMap<>(); 56 | final List defaultMethodHandlers = new ArrayList<>(); 57 | 58 | for (final Method method : target.type().getMethods()) { 59 | if (isDefault(method)) { 60 | final DefaultMethodHandler handler = new DefaultMethodHandler(method); 61 | defaultMethodHandlers.add(handler); 62 | methodToHandler.put(method, handler); 63 | } else { 64 | methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); 65 | } 66 | } 67 | 68 | final InvocationHandler handler = factory.create(target, methodToHandler); 69 | final T proxy = (T) Proxy.newProxyInstance( 70 | target.type().getClassLoader(), 71 | new Class[] { target.type() }, 72 | handler); 73 | 74 | for (final DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { 75 | defaultMethodHandler.bindTo(proxy); 76 | } 77 | 78 | return proxy; 79 | } 80 | 81 | /** 82 | * VertxFeign builder. 83 | */ 84 | public static final class Builder extends Feign.Builder { 85 | private Vertx vertx; 86 | private final List requestInterceptors = new ArrayList<>(); 87 | private Logger.Level logLevel = Logger.Level.NONE; 88 | private Contract contract = new VertxDelegatingContract(new Contract.Default()); 89 | private Retryer retryer = new Retryer.Default(); 90 | private Logger logger = new Logger.NoOpLogger(); 91 | private Encoder encoder = new Encoder.Default(); 92 | private Decoder decoder = new Decoder.Default(); 93 | private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder(); 94 | private List capabilities = new ArrayList<>(); 95 | private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); 96 | private HttpClientOptions options = new HttpClientOptions(); 97 | private long timeout = -1; 98 | private boolean decode404; 99 | private UnaryOperator> requestPreProcessor = UnaryOperator.identity(); 100 | 101 | /** Unsupported operation. */ 102 | @Override 103 | public Builder client(final Client client) { 104 | throw new UnsupportedOperationException(); 105 | } 106 | 107 | /** Unsupported operation. */ 108 | @Override 109 | public Builder invocationHandlerFactory( 110 | final InvocationHandlerFactory invocationHandlerFactory) { 111 | throw new UnsupportedOperationException(); 112 | } 113 | 114 | /** 115 | * Sets a vertx instance to use to make the client. 116 | * 117 | * @param vertx vertx instance 118 | * 119 | * @return this builder 120 | */ 121 | public Builder vertx(final Vertx vertx) { 122 | this.vertx = checkNotNull(vertx, "Argument vertx must be not null"); 123 | return this; 124 | } 125 | 126 | /** 127 | * Sets log level. 128 | * 129 | * @param logLevel log level 130 | * 131 | * @return this builder 132 | */ 133 | @Override 134 | public Builder logLevel(final Logger.Level logLevel) { 135 | this.logLevel = checkNotNull(logLevel, "Argument logLevel must be not null"); 136 | return this; 137 | } 138 | 139 | /** 140 | * Sets contract. Provided contract will be wrapped in {@link VertxDelegatingContract}. 141 | * 142 | * @param contract contract 143 | * 144 | * @return this builder 145 | */ 146 | @Override 147 | public Builder contract(final Contract contract) { 148 | checkNotNull(contract, "Argument contract must be not null"); 149 | this.contract = new VertxDelegatingContract(contract); 150 | return this; 151 | } 152 | 153 | /** 154 | * Sets retryer. 155 | * 156 | * @param retryer retryer 157 | * 158 | * @return this builder 159 | */ 160 | @Override 161 | public Builder retryer(final Retryer retryer) { 162 | this.retryer = checkNotNull(retryer, "Argument retryer must be not null"); 163 | return this; 164 | } 165 | 166 | /** 167 | * Sets logger. 168 | * 169 | * @param logger logger 170 | * 171 | * @return this builder 172 | */ 173 | @Override 174 | public Builder logger(final Logger logger) { 175 | this.logger = checkNotNull(logger, "Argument logger must be not null"); 176 | return this; 177 | } 178 | 179 | /** 180 | * Sets encoder. 181 | * 182 | * @param encoder encoder 183 | * 184 | * @return this builder 185 | */ 186 | @Override 187 | public Builder encoder(final Encoder encoder) { 188 | this.encoder = checkNotNull(encoder, "Argument encoder must be not null"); 189 | return this; 190 | } 191 | 192 | /** 193 | * Sets decoder. 194 | * 195 | * @param decoder decoder 196 | * 197 | * @return this builder 198 | */ 199 | @Override 200 | public Builder decoder(final Decoder decoder) { 201 | this.decoder = checkNotNull(decoder, "Argument decoder must be not null"); 202 | return this; 203 | } 204 | 205 | /** 206 | * Sets query map encoder. 207 | * 208 | * @param queryMapEncoder query map encoder 209 | * 210 | * @return this builder 211 | */ 212 | @Override 213 | public Builder queryMapEncoder(final QueryMapEncoder queryMapEncoder) { 214 | this.queryMapEncoder = checkNotNull(queryMapEncoder, "Argument queryMapEncoder must be not null"); 215 | return this; 216 | } 217 | 218 | /** 219 | * Adds a single capability to the builder. 220 | * 221 | * @param capability capability 222 | * 223 | * @return this builder 224 | */ 225 | @Override 226 | public Builder addCapability(Capability capability) { 227 | checkNotNull(capability, "Argument capability must be not null"); 228 | this.capabilities.add(capability); 229 | return this; 230 | } 231 | 232 | /** 233 | * This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with 234 | * 404 status, specifically returning null or empty instead of throwing {@link FeignException}. 235 | * 236 | *

All first-party (ex gson) decoders return well-known empty values defined by 237 | * {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder) 238 | * decoder} or make your own. 239 | * 240 | *

This flag only works with 404, as opposed to all or arbitrary status codes. This was an 241 | * explicit decision: 404 - empty is safe, common and doesn't complicate redirection, retry or 242 | * fallback policy. 243 | * 244 | * @return this builder 245 | */ 246 | @Override 247 | public Builder decode404() { 248 | this.decode404 = true; 249 | return this; 250 | } 251 | 252 | /** 253 | * Sets error decoder. 254 | * 255 | * @param errorDecoder error deoceder 256 | * 257 | * @return this builder 258 | */ 259 | @Override 260 | public Builder errorDecoder(final ErrorDecoder errorDecoder) { 261 | this.errorDecoder = checkNotNull(errorDecoder, "Argument errorDecoder must be not null"); 262 | return this; 263 | } 264 | 265 | /** 266 | * Sets request options using Vert.x {@link HttpClientOptions}. 267 | * 268 | * @param options {@code HttpClientOptions} for full customization of the underlying Vert.x 269 | * {@link HttpClient} 270 | * 271 | * @return this builder 272 | */ 273 | public Builder options(final HttpClientOptions options) { 274 | this.options = checkNotNull(options, "Argument options must be not null"); 275 | return this; 276 | } 277 | 278 | /** 279 | * Sets request options using Feign {@link Request.Options}. 280 | * 281 | * @param options Feign {@code Request.Options} object 282 | * 283 | * @return this builder 284 | */ 285 | @Override 286 | public Builder options(final Request.Options options) { 287 | checkNotNull(options, "Argument options must be not null"); 288 | this.options = new HttpClientOptions() 289 | .setConnectTimeout(options.connectTimeoutMillis()) 290 | .setIdleTimeout(options.readTimeoutMillis()); 291 | return this; 292 | } 293 | 294 | /** 295 | * Configures the amount of time in milliseconds after which if the request does not return any data within the 296 | * timeout period an {@link java.util.concurrent.TimeoutException} fails the request. 297 | *

298 | * Setting zero or a negative {@code value} disables the timeout. 299 | * 300 | * @param timeout The quantity of time in milliseconds. 301 | * @return this builder 302 | */ 303 | public Builder timeout(long timeout) { 304 | this.timeout = timeout; 305 | return this; 306 | } 307 | 308 | /** 309 | * Defines operation to execute on each {@link HttpRequest} before it sent. Used to make setup on request level. 310 | *

Example: 311 | * 312 | *

313 |      * var client = VertxFeign
314 |      *     .builder()
315 |      *     .vertx(vertx)
316 |      *     .requestPreProcessor(req -> req.ssl(true));
317 |      * 
318 | * 319 | * @param requestPreProcessor operation to execute on each request 320 | * @return updated request 321 | */ 322 | public Builder requestPreProcessor(UnaryOperator> requestPreProcessor) { 323 | this.requestPreProcessor = checkNotNull(requestPreProcessor, "Argument requestPreProcessor must be not null"); 324 | return this; 325 | } 326 | 327 | /** 328 | * Adds a single request interceptor to the builder. 329 | * 330 | * @param requestInterceptor request interceptor to add 331 | * 332 | * @return this builder 333 | */ 334 | @Override 335 | public Builder requestInterceptor(final RequestInterceptor requestInterceptor) { 336 | checkNotNull(requestInterceptor, "Argument requestInterceptor must be not null"); 337 | this.requestInterceptors.add(requestInterceptor); 338 | return this; 339 | } 340 | 341 | /** 342 | * Sets the full set of request interceptors for the builder, overwriting any previous 343 | * interceptors. 344 | * 345 | * @param requestInterceptors set of request interceptors 346 | * 347 | * @return this builder 348 | */ 349 | @Override 350 | public Builder requestInterceptors(final Iterable requestInterceptors) { 351 | checkNotNull(requestInterceptors, "Argument requestInterceptors must be not null"); 352 | 353 | this.requestInterceptors.clear(); 354 | 355 | for (final RequestInterceptor requestInterceptor : requestInterceptors) { 356 | this.requestInterceptors.add(requestInterceptor); 357 | } 358 | 359 | return this; 360 | } 361 | 362 | /** 363 | * Defines target and builds client. 364 | * 365 | * @param apiType API interface 366 | * @param url base URL 367 | * @param class of API interface 368 | * 369 | * @return built client 370 | */ 371 | @Override 372 | public T target(final Class apiType, final String url) { 373 | checkNotNull(apiType, "Argument apiType must be not null"); 374 | checkNotNull(url, "Argument url must be not null"); 375 | 376 | return target(new Target.HardCodedTarget<>(apiType, url)); 377 | } 378 | 379 | /** 380 | * Defines target and builds client. 381 | * 382 | * @param target target instance 383 | * @param class of API interface 384 | * 385 | * @return built client 386 | */ 387 | @Override 388 | public T target(final Target target) { 389 | return build().newInstance(target); 390 | } 391 | 392 | @Override 393 | public VertxFeign internalBuild() { 394 | checkNotNull(this.vertx, "Vertx instance wasn't provided in VertxFeign builder"); 395 | 396 | final VertxHttpClient client = new VertxHttpClient(vertx, options, timeout, requestPreProcessor); 397 | final VertxMethodHandler.Factory methodHandlerFactory = 398 | new VertxMethodHandler.Factory(client, retryer, requestInterceptors, logger, 399 | logLevel, decode404); 400 | final ParseHandlersByName handlersByName = new ParseHandlersByName( 401 | contract, options, encoder, decoder, queryMapEncoder, capabilities, errorDecoder, methodHandlerFactory); 402 | final InvocationHandlerFactory invocationHandlerFactory = 403 | new VertxInvocationHandler.Factory(); 404 | 405 | return new VertxFeign(handlersByName, invocationHandlerFactory); 406 | } 407 | } 408 | 409 | private static final class ParseHandlersByName { 410 | private final Contract contract; 411 | private final HttpClientOptions options; 412 | private final Encoder encoder; 413 | private final Decoder decoder; 414 | private final QueryMapEncoder queryMapEncoder; 415 | private final List capabilities; 416 | private final ErrorDecoder errorDecoder; 417 | private final VertxMethodHandler.Factory factory; 418 | 419 | private ParseHandlersByName( 420 | final Contract contract, 421 | final HttpClientOptions options, 422 | final Encoder encoder, 423 | final Decoder decoder, 424 | final QueryMapEncoder queryMapEncoder, 425 | final List capabilities, 426 | final ErrorDecoder errorDecoder, 427 | final VertxMethodHandler.Factory factory) { 428 | this.contract = contract; 429 | this.options = options; 430 | this.factory = factory; 431 | this.encoder = encoder; 432 | this.decoder = decoder; 433 | this.queryMapEncoder = queryMapEncoder; 434 | this.capabilities = capabilities; 435 | this.errorDecoder = errorDecoder; 436 | } 437 | 438 | private Map apply(final Target target) { 439 | final List metadatas = contract.parseAndValidateMetadata(target.type()); 440 | final Map result = new HashMap<>(); 441 | 442 | for (final MethodMetadata metadata : metadatas) { 443 | VertxBuildTemplateByResolvingArgs buildTemplate; 444 | 445 | if (!metadata.formParams().isEmpty() 446 | && metadata.template().bodyTemplate() == null) { 447 | buildTemplate = new VertxBuildTemplateByResolvingArgs 448 | .BuildFormEncodedTemplateFromArgs(metadata, queryMapEncoder, target, encoder); 449 | } else if (metadata.bodyIndex() != null) { 450 | buildTemplate = new VertxBuildTemplateByResolvingArgs 451 | .BuildEncodedTemplateFromArgs(metadata, queryMapEncoder, target, encoder); 452 | } else { 453 | buildTemplate = new VertxBuildTemplateByResolvingArgs(metadata, queryMapEncoder, target); 454 | } 455 | 456 | result.put(metadata.configKey(), factory.create( 457 | target, metadata, buildTemplate, decoder, errorDecoder)); 458 | } 459 | 460 | return result; 461 | } 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /src/main/java/feign/VertxInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package feign; 2 | 3 | import feign.InvocationHandlerFactory.MethodHandler; 4 | import feign.vertx.VertxHttpClient; 5 | import io.vertx.core.Future; 6 | 7 | import java.lang.reflect.InvocationHandler; 8 | import java.lang.reflect.Method; 9 | import java.lang.reflect.Proxy; 10 | import java.util.Map; 11 | 12 | /** 13 | * {@link InvocationHandler} implementation that transforms calls to methods of feign contract into 14 | * asynchronous HTTP requests via vertx. 15 | * 16 | * @author Alexei KLENIN 17 | */ 18 | final class VertxInvocationHandler implements InvocationHandler { 19 | private final Target target; 20 | private final Map dispatch; 21 | 22 | private VertxInvocationHandler( 23 | final Target target, 24 | final Map dispatch) { 25 | this.target = target; 26 | this.dispatch = dispatch; 27 | } 28 | 29 | @Override 30 | public Object invoke(final Object proxy, final Method method, final Object[] args) { 31 | switch (method.getName()) { 32 | case "equals" : 33 | final Object otherHandler = args.length > 0 && args[0] != null 34 | ? Proxy.getInvocationHandler(args[0]) 35 | : null; 36 | return equals(otherHandler); 37 | case "hashCode" : 38 | return hashCode(); 39 | case "toString": 40 | return toString(); 41 | default : 42 | if (isReturnsFuture(method)) { 43 | return invokeRequestMethod(method, args); 44 | } else { 45 | final String message = String.format( 46 | "Method %s of contract %s doesn't return io.vertx.core.Future", 47 | method.getName(), 48 | method.getDeclaringClass().getSimpleName()); 49 | throw new FeignException(-1, message); 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Transforms method invocation into request that executed by {@link VertxHttpClient}. 56 | * 57 | * @param method invoked method 58 | * @param args provided arguments to method 59 | * 60 | * @return future with decoded result or occurred exception 61 | */ 62 | private Future invokeRequestMethod(final Method method, final Object[] args) { 63 | try { 64 | return (Future) dispatch.get(method).invoke(args); 65 | } catch (Throwable throwable) { 66 | return Future.failedFuture(throwable); 67 | } 68 | } 69 | 70 | /** 71 | * Checks if method must return vertx {@code Future}. 72 | * 73 | * @param method invoked method 74 | * 75 | * @return true if method must return Future, false if not 76 | */ 77 | private boolean isReturnsFuture(final Method method) { 78 | return Future.class.isAssignableFrom(method.getReturnType()); 79 | } 80 | 81 | @Override 82 | public boolean equals(final Object other) { 83 | if (other instanceof VertxInvocationHandler) { 84 | final VertxInvocationHandler otherHandler = (VertxInvocationHandler) other; 85 | return this.target.equals(otherHandler.target); 86 | } 87 | 88 | return false; 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | return target.hashCode(); 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return target.toString(); 99 | } 100 | 101 | /** 102 | * Factory for VertxInvocationHandler. 103 | */ 104 | static final class Factory implements InvocationHandlerFactory { 105 | 106 | @Override 107 | public InvocationHandler create( 108 | final Target target, 109 | final Map dispatch) { 110 | return new VertxInvocationHandler(target, dispatch); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/feign/VertxMethodHandler.java: -------------------------------------------------------------------------------- 1 | package feign; 2 | 3 | import feign.InvocationHandlerFactory.MethodHandler; 4 | import feign.codec.DecodeException; 5 | import feign.codec.Decoder; 6 | import feign.codec.ErrorDecoder; 7 | import feign.vertx.VertxHttpClient; 8 | import io.vertx.core.Future; 9 | import io.vertx.core.VertxException; 10 | 11 | import java.io.IOException; 12 | import java.time.Duration; 13 | import java.time.Instant; 14 | import java.util.List; 15 | import java.util.concurrent.TimeoutException; 16 | import java.util.function.Function; 17 | 18 | import static feign.FeignException.errorExecuting; 19 | import static feign.FeignException.errorReading; 20 | import static feign.Util.ensureClosed; 21 | 22 | /** 23 | * Method handler for asynchronous HTTP requests via {@link VertxHttpClient}. 24 | * Inspired by {@link SynchronousMethodHandler}. 25 | * 26 | * @author Alexei KLENIN 27 | * @author Gordon McKinney 28 | */ 29 | final class VertxMethodHandler implements MethodHandler { 30 | private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L; 31 | 32 | private final MethodMetadata metadata; 33 | private final Target target; 34 | private final VertxHttpClient client; 35 | private final Retryer retryer; 36 | private final List requestInterceptors; 37 | private final Logger logger; 38 | private final Logger.Level logLevel; 39 | private final RequestTemplate.Factory buildTemplateFromArgs; 40 | private final Decoder decoder; 41 | private final ErrorDecoder errorDecoder; 42 | private final boolean decode404; 43 | 44 | private VertxMethodHandler( 45 | final Target target, 46 | final VertxHttpClient client, 47 | final Retryer retryer, 48 | final List requestInterceptors, 49 | final Logger logger, 50 | final Logger.Level logLevel, 51 | final MethodMetadata metadata, 52 | final RequestTemplate.Factory buildTemplateFromArgs, 53 | final Decoder decoder, 54 | final ErrorDecoder errorDecoder, 55 | final boolean decode404) { 56 | this.target = target; 57 | this.client = client; 58 | this.retryer = retryer; 59 | this.requestInterceptors = requestInterceptors; 60 | this.logger = logger; 61 | this.logLevel = logLevel; 62 | this.metadata = metadata; 63 | this.buildTemplateFromArgs = buildTemplateFromArgs; 64 | this.errorDecoder = errorDecoder; 65 | this.decoder = decoder; 66 | this.decode404 = decode404; 67 | } 68 | 69 | @Override 70 | @SuppressWarnings("unchecked") 71 | public Future invoke(final Object[] argv) { 72 | final RequestTemplate template = buildTemplateFromArgs.create(argv); 73 | final Retryer retryer = this.retryer.clone(); 74 | 75 | final RetryRecoverer recoverer = new RetryRecoverer<>(template, retryer); 76 | return executeAndDecode(template).recover(recoverer); 77 | } 78 | 79 | /** 80 | * Executes request from {@code template} with {@code this.client} and decodes the response. 81 | * Result or occurred error wrapped in returned Future. 82 | * 83 | * @param template request template 84 | * 85 | * @return future with decoded result or occurred error 86 | */ 87 | private Future executeAndDecode(final RequestTemplate template) { 88 | final Request request = targetRequest(template); 89 | 90 | logRequest(request); 91 | 92 | final Instant start = Instant.now(); 93 | 94 | return client 95 | .execute(request) 96 | .compose( 97 | response -> { 98 | final long elapsedTime = Duration.between(start, Instant.now()).toMillis(); 99 | boolean shouldClose = true; 100 | 101 | try { 102 | // TODO: check why this buffering is needed 103 | if (logLevel != Logger.Level.NONE) { 104 | response = logger.logAndRebufferResponse( 105 | metadata.configKey(), 106 | logLevel, 107 | response, 108 | elapsedTime); 109 | } 110 | 111 | if (Response.class == metadata.returnType()) { 112 | if (response.body() == null) { 113 | return Future.succeededFuture(response); 114 | } else if (response.body().length() == null 115 | || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { 116 | shouldClose = false; 117 | return Future.succeededFuture(response); 118 | } else { 119 | return Future.succeededFuture(Response.builder() 120 | .status(response.status()) 121 | .reason(response.reason()) 122 | .headers(response.headers()) 123 | .request(response.request()) 124 | .body(response.body()) 125 | .build()); 126 | } 127 | } else if (response.status() >= 200 && response.status() < 300) { 128 | if (Void.class == metadata.returnType()) { 129 | return Future.succeededFuture(); 130 | } else { 131 | return Future.succeededFuture(decode(response, request)); 132 | } 133 | } else if (decode404 && response.status() == 404) { 134 | return Future.succeededFuture(decoder.decode(response, metadata.returnType())); 135 | } else { 136 | return Future.failedFuture(errorDecoder.decode(metadata.configKey(), response)); 137 | } 138 | } catch (final IOException ioException) { 139 | logIoException(ioException, elapsedTime); 140 | return Future.failedFuture(errorReading(request, response, ioException)); 141 | } catch (FeignException exception) { 142 | return Future.failedFuture(exception); 143 | } finally { 144 | if (shouldClose) { 145 | ensureClosed(response.body()); 146 | } 147 | } 148 | }, 149 | failure -> { 150 | if (failure instanceof VertxException || failure instanceof TimeoutException) { 151 | return Future.failedFuture(failure); 152 | } else if (failure.getCause() instanceof IOException) { 153 | final long elapsedTime = Duration.between(start, Instant.now()).toMillis(); 154 | logIoException((IOException) failure.getCause(), elapsedTime); 155 | return Future.failedFuture(errorExecuting(request, (IOException) failure.getCause())); 156 | } else { 157 | return Future.failedFuture(failure.getCause()); 158 | } 159 | }); 160 | } 161 | 162 | /** 163 | * Associates request to defined target. 164 | * 165 | * @param template request template 166 | * 167 | * @return fully formed request 168 | */ 169 | private Request targetRequest(final RequestTemplate template) { 170 | for (final RequestInterceptor interceptor : requestInterceptors) { 171 | interceptor.apply(template); 172 | } 173 | 174 | return target.apply(template); 175 | } 176 | 177 | /** 178 | * Transforms HTTP response body into object using decoder. 179 | * 180 | * @param response HTTP response 181 | * @param request HTTP request 182 | * 183 | * @return decoded result 184 | * 185 | * @throws IOException IO exception during the reading of InputStream of response 186 | * @throws DecodeException when decoding failed due to a checked or unchecked exception besides 187 | * IOException 188 | * @throws FeignException when decoding succeeds, but conveys the operation failed 189 | */ 190 | private Object decode(final Response response, final Request request) throws IOException, FeignException { 191 | try { 192 | return decoder.decode(response, metadata.returnType()); 193 | } catch (final FeignException feignException) { 194 | /* All feign exception including decode exceptions */ 195 | throw feignException; 196 | } catch (final RuntimeException unexpectedException) { 197 | /* Any unexpected exception */ 198 | throw new DecodeException(-1, unexpectedException.getMessage(), request, unexpectedException); 199 | } 200 | } 201 | 202 | /** 203 | * Logs request. 204 | * 205 | * @param request HTTP request 206 | */ 207 | private void logRequest(final Request request) { 208 | if (logLevel != Logger.Level.NONE) { 209 | logger.logRequest(metadata.configKey(), logLevel, request); 210 | } 211 | } 212 | 213 | /** 214 | * Logs IO exception. 215 | * 216 | * @param exception IO exception 217 | * @param elapsedTime time spent to execute request 218 | */ 219 | private void logIoException(final IOException exception, final long elapsedTime) { 220 | if (logLevel != Logger.Level.NONE) { 221 | logger.logIOException(metadata.configKey(), logLevel, exception, elapsedTime); 222 | } 223 | } 224 | 225 | /** 226 | * Logs retry. 227 | */ 228 | private void logRetry() { 229 | if (logLevel != Logger.Level.NONE) { 230 | logger.logRetry(metadata.configKey(), logLevel); 231 | } 232 | } 233 | 234 | static final class Factory { 235 | private final VertxHttpClient client; 236 | private final Retryer retryer; 237 | private final List requestInterceptors; 238 | private final Logger logger; 239 | private final Logger.Level logLevel; 240 | private final boolean decode404; 241 | 242 | Factory( 243 | final VertxHttpClient client, 244 | final Retryer retryer, 245 | final List requestInterceptors, 246 | final Logger logger, 247 | final Logger.Level logLevel, 248 | final boolean decode404) { 249 | this.client = client; 250 | this.retryer = retryer; 251 | this.requestInterceptors = requestInterceptors; 252 | this.logger = logger; 253 | this.logLevel = logLevel; 254 | this.decode404 = decode404; 255 | } 256 | 257 | MethodHandler create( 258 | final Target target, 259 | final MethodMetadata metadata, 260 | final RequestTemplate.Factory buildTemplateFromArgs, 261 | final Decoder decoder, 262 | final ErrorDecoder errorDecoder) { 263 | return new VertxMethodHandler( 264 | target, 265 | client, 266 | retryer, 267 | requestInterceptors, 268 | logger, 269 | logLevel, 270 | metadata, 271 | buildTemplateFromArgs, 272 | decoder, 273 | errorDecoder, 274 | decode404); 275 | } 276 | } 277 | 278 | /** 279 | * Handler for failures able to retry execution of request. In this case handler passed to new request. 280 | * 281 | * @param type of response 282 | */ 283 | private final class RetryRecoverer implements Function> { 284 | private final RequestTemplate template; 285 | private final Retryer retryer; 286 | 287 | private RetryRecoverer(final RequestTemplate template, final Retryer retryer) { 288 | this.template = template; 289 | this.retryer = retryer; 290 | } 291 | 292 | @Override 293 | @SuppressWarnings("unchecked") 294 | public Future apply(final Throwable throwable) { 295 | if (throwable instanceof RetryableException) { 296 | this.retryer.continueOrPropagate((RetryableException) throwable); 297 | logRetry(); 298 | return ((Future) executeAndDecode(this.template)).recover(this); 299 | } else { 300 | return Future.failedFuture(throwable); 301 | } 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/main/java/feign/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Package for extensions that must be able use package-private classes of {@code feign}. 3 | * 4 | * @author Alexei KLENIN 5 | */ 6 | package feign; -------------------------------------------------------------------------------- /src/main/java/feign/vertx/VertxDelegatingContract.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import static feign.Util.checkNotNull; 4 | import static feign.Util.resolveLastTypeParameter; 5 | 6 | import feign.Contract; 7 | import feign.MethodMetadata; 8 | import io.vertx.core.Future; 9 | 10 | import java.lang.reflect.ParameterizedType; 11 | import java.lang.reflect.Type; 12 | import java.util.List; 13 | 14 | /** 15 | * Contract allowing only {@link Future} return type. 16 | * 17 | * @author Alexei KLENIN 18 | */ 19 | public final class VertxDelegatingContract implements Contract { 20 | private final Contract delegate; 21 | 22 | public VertxDelegatingContract(final Contract delegate) { 23 | this.delegate = checkNotNull(delegate, "delegate must not be null"); 24 | } 25 | 26 | @Override 27 | public List parseAndValidateMetadata(final Class targetType) { 28 | checkNotNull(targetType, "Argument targetType must be not null"); 29 | 30 | final List metadatas = delegate.parseAndValidateMetadata(targetType); 31 | 32 | for (final MethodMetadata metadata : metadatas) { 33 | final Type type = metadata.returnType(); 34 | 35 | if (type instanceof ParameterizedType 36 | && ((ParameterizedType) type).getRawType().equals(Future.class)) { 37 | final Type actualType = resolveLastTypeParameter(type, Future.class); 38 | metadata.returnType(actualType); 39 | } else { 40 | throw new IllegalStateException(String.format( 41 | "Method %s of contract %s doesn't returns io.vertx.core.Future", 42 | metadata.configKey(), targetType.getSimpleName())); 43 | } 44 | } 45 | 46 | return metadatas; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/feign/vertx/VertxHttpClient.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import static feign.Util.checkNotNull; 4 | 5 | import feign.Request; 6 | import feign.Response; 7 | import io.vertx.core.Future; 8 | import io.vertx.core.Vertx; 9 | import io.vertx.core.buffer.Buffer; 10 | import io.vertx.core.http.HttpClient; 11 | import io.vertx.core.http.HttpClientOptions; 12 | import io.vertx.core.http.HttpMethod; 13 | import io.vertx.ext.web.client.HttpRequest; 14 | import io.vertx.ext.web.client.HttpResponse; 15 | import io.vertx.ext.web.client.WebClient; 16 | 17 | import java.net.MalformedURLException; 18 | import java.net.URL; 19 | import java.util.ArrayList; 20 | import java.util.Collection; 21 | import java.util.Map; 22 | import java.util.function.UnaryOperator; 23 | import java.util.stream.Collectors; 24 | import java.util.stream.StreamSupport; 25 | 26 | /** 27 | * Like {@link feign.Client} but method {@link #execute} returns {@link Future} with 28 | * {@link Response}. HTTP request is executed asynchronously with Vert.x 29 | * 30 | * @author Alexei KLENIN 31 | * @author Gordon McKinney 32 | */ 33 | @SuppressWarnings("unused") 34 | public final class VertxHttpClient { 35 | private final HttpClient httpClient; 36 | private final long timeout; 37 | private final UnaryOperator> requestPreProcessor; 38 | 39 | /** 40 | * Constructor from {@link Vertx} instance, HTTP client options and request timeout. 41 | * @param vertx vertx instance 42 | * @param options HTTP options 43 | * @param timeout request timeout 44 | * @param requestPreProcessor request pre-processor 45 | */ 46 | public VertxHttpClient( 47 | final Vertx vertx, 48 | final HttpClientOptions options, 49 | final long timeout, 50 | final UnaryOperator> requestPreProcessor) { 51 | checkNotNull(vertx, "Argument vertx must not be null"); 52 | checkNotNull(options, "Argument options must be not null"); 53 | checkNotNull(requestPreProcessor, "Argument requestPreProcessor must be not null"); 54 | 55 | this.httpClient = vertx.createHttpClient(options); 56 | this.timeout = timeout; 57 | this.requestPreProcessor = requestPreProcessor; 58 | } 59 | 60 | /** 61 | * Executes HTTP request and returns {@link Future} with response. 62 | * 63 | * @param request request 64 | * @return future of HTTP response 65 | */ 66 | public Future execute(final Request request) { 67 | checkNotNull(request, "Argument request must be not null"); 68 | 69 | final HttpRequest httpClientRequest; 70 | 71 | try { 72 | httpClientRequest = makeHttpClientRequest(request); 73 | } catch (final MalformedURLException unexpectedException) { 74 | return Future.failedFuture(unexpectedException); 75 | } 76 | 77 | final Future> responseFuture = request.body() != null 78 | ? httpClientRequest.sendBuffer(Buffer.buffer(request.body())) 79 | : httpClientRequest.send(); 80 | 81 | return responseFuture.compose(response -> { 82 | final Map> responseHeaders = StreamSupport 83 | .stream(response.headers().spliterator(), false) 84 | .collect(Collectors.groupingBy( 85 | Map.Entry::getKey, 86 | Collectors.mapping( 87 | Map.Entry::getValue, 88 | Collectors.toCollection(ArrayList::new)))); 89 | 90 | byte[] body = response.body() != null ? response.body().getBytes() : null; 91 | 92 | return Future.succeededFuture(Response 93 | .builder() 94 | .status(response.statusCode()) 95 | .reason(response.statusMessage()) 96 | .headers(responseHeaders) 97 | .body(body) 98 | .request(request) 99 | .build()); 100 | }); 101 | } 102 | 103 | private HttpRequest makeHttpClientRequest(final Request request) 104 | throws MalformedURLException { 105 | final URL url = new URL(request.url()); 106 | final String host = url.getHost(); 107 | final String requestUri = url.getFile(); 108 | 109 | int port; 110 | if (url.getPort() > -1) { 111 | port = url.getPort(); 112 | } else if (url.getProtocol().equalsIgnoreCase("https")) { 113 | port = 443; 114 | } else { 115 | port = HttpClientOptions.DEFAULT_DEFAULT_PORT; 116 | } 117 | 118 | HttpRequest httpClientRequest = WebClient 119 | .wrap(this.httpClient) 120 | .request(HttpMethod.valueOf(request.httpMethod().name()), 121 | port, 122 | host, 123 | requestUri) 124 | .timeout(timeout); 125 | 126 | /* Add headers to request */ 127 | for (final Map.Entry> header : request.headers().entrySet()) { 128 | httpClientRequest = httpClientRequest.putHeader(header.getKey(), header.getValue()); 129 | } 130 | 131 | return requestPreProcessor.apply(httpClientRequest); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/AbstractClientReconnectTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import feign.vertx.testcase.HelloServiceAPI; 5 | import io.vertx.core.AsyncResult; 6 | import io.vertx.core.CompositeFuture; 7 | import io.vertx.core.Future; 8 | import io.vertx.core.Vertx; 9 | import io.vertx.junit5.VertxTestContext; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Nested; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.TestInstance; 17 | 18 | import java.util.List; 19 | import java.util.stream.Collectors; 20 | import java.util.stream.IntStream; 21 | 22 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 25 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 29 | abstract class AbstractClientReconnectTest extends AbstractFeignVertxTest { 30 | static String baseUrl; 31 | static int serverPort; 32 | 33 | HelloServiceAPI client = null; 34 | 35 | @BeforeAll 36 | static void setupMockServer() { 37 | serverPort = wireMock.port(); 38 | baseUrl = wireMock.baseUrl(); 39 | wireMock.stubFor(get(anyUrl()) 40 | .willReturn(aResponse() 41 | .withStatus(200))); 42 | } 43 | 44 | @BeforeAll 45 | protected abstract void createClient(Vertx vertx); 46 | 47 | @Test 48 | @DisplayName("All requests should be answered") 49 | void testAllRequestsShouldBeAnswered(VertxTestContext testContext) { 50 | sendRequests(10) 51 | .compose(responses -> assertAllRequestsAnswered(responses, testContext)); 52 | } 53 | 54 | @Nested 55 | @DisplayName("After server has became unavailable") 56 | class AfterServerBecameUnavailable { 57 | 58 | @BeforeEach 59 | void shutDownServer() { 60 | wireMock.stop(); 61 | } 62 | 63 | @Test 64 | @DisplayName("All requests should fail") 65 | void testAllRequestsShouldFail(VertxTestContext testContext) { 66 | sendRequests(10) 67 | .onComplete(responses -> testContext.verify(() -> { 68 | if (responses.succeeded()) { 69 | testContext.failNow(new IllegalStateException("Client should not get responses from unavailable server")); 70 | } 71 | 72 | try { 73 | assertThat(responses.cause().getMessage()) 74 | .startsWith("Connection "); 75 | testContext.completeNow(); 76 | } catch(Throwable assertionException) { 77 | testContext.failNow(assertionException); 78 | } 79 | })); 80 | } 81 | 82 | @Nested 83 | @DisplayName("After server is available again") 84 | class AfterServerIsBack { 85 | WireMockServer restartedServer = new WireMockServer(options().port(serverPort)); 86 | 87 | @BeforeEach 88 | void restartServer() { 89 | restartedServer.start(); 90 | restartedServer.stubFor(get(anyUrl()) 91 | .willReturn(aResponse() 92 | .withStatus(200))); 93 | } 94 | 95 | @AfterEach 96 | void shutDownServer() { 97 | restartedServer.stop(); 98 | } 99 | 100 | @Test 101 | @DisplayName("All requests should be answered") 102 | void testAllRequestsShouldBeAnswered(VertxTestContext testContext) { 103 | sendRequests(10) 104 | .compose(responses -> assertAllRequestsAnswered(responses, testContext)); 105 | } 106 | } 107 | } 108 | 109 | CompositeFuture sendRequests(int requests) { 110 | List requestList = IntStream 111 | .range(0, requests) 112 | .mapToObj(ignored -> client.hello()) 113 | .collect(Collectors.toList()); 114 | return CompositeFuture.all(requestList); 115 | } 116 | 117 | Future assertAllRequestsAnswered(AsyncResult responses, VertxTestContext testContext) { 118 | if (responses.succeeded()) { 119 | testContext.completeNow(); 120 | return Future.succeededFuture(); 121 | } else { 122 | testContext.failNow(responses.cause()); 123 | return Future.failedFuture(responses.cause()); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/AbstractFeignVertxTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; 4 | 5 | import com.github.tomakehurst.wiremock.WireMockServer; 6 | import feign.vertx.testcase.domain.OrderGenerator; 7 | import io.vertx.junit5.VertxExtension; 8 | import org.junit.jupiter.api.AfterAll; 9 | import org.junit.jupiter.api.BeforeAll; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | 13 | @ExtendWith(VertxExtension.class) 14 | public abstract class AbstractFeignVertxTest { 15 | protected static WireMockServer wireMock = new WireMockServer(options().dynamicPort()); 16 | protected static final OrderGenerator generator = new OrderGenerator(); 17 | 18 | @BeforeAll 19 | @DisplayName("Setup WireMock server") 20 | static void setupWireMockServer() { 21 | wireMock.start(); 22 | } 23 | 24 | @AfterAll 25 | @DisplayName("Shutdown WireMock server") 26 | static void shutdownWireMockServer() { 27 | wireMock.stop(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/ConnectionsLeakTests.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.VertxFeign; 4 | import feign.jackson.JacksonDecoder; 5 | import feign.jackson.JacksonEncoder; 6 | import feign.vertx.testcase.HelloServiceAPI; 7 | import io.vertx.core.CompositeFuture; 8 | import io.vertx.core.Future; 9 | import io.vertx.core.Vertx; 10 | import io.vertx.core.http.HttpClientOptions; 11 | import io.vertx.core.http.HttpConnection; 12 | import io.vertx.core.http.HttpServer; 13 | import io.vertx.core.http.HttpServerOptions; 14 | import io.vertx.core.http.HttpVersion; 15 | import io.vertx.core.impl.ConcurrentHashSet; 16 | import io.vertx.junit5.VertxExtension; 17 | import io.vertx.junit5.VertxTestContext; 18 | import org.junit.jupiter.api.AfterEach; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.DisplayName; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | 24 | import java.util.List; 25 | import java.util.Set; 26 | import java.util.stream.IntStream; 27 | 28 | import static java.util.stream.Collectors.toList; 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | 31 | @ExtendWith(VertxExtension.class) 32 | @DisplayName("Test that connections does not leak") 33 | public class ConnectionsLeakTests { 34 | private static final HttpServerOptions serverOptions = new HttpServerOptions() 35 | .setLogActivity(true) 36 | .setPort(8091) 37 | .setSsl(false); 38 | 39 | HttpServer httpServer; 40 | 41 | private final Set connections = new ConcurrentHashSet<>(); 42 | 43 | @BeforeEach 44 | public void initServer(Vertx vertx) { 45 | httpServer = vertx.createHttpServer(serverOptions); 46 | httpServer.requestHandler(request -> { 47 | if (request.connection() != null) { 48 | this.connections.add(request.connection()); 49 | } 50 | request.response().end("Hello world"); 51 | }); 52 | httpServer.listen(); 53 | } 54 | 55 | @AfterEach 56 | public void shutdownServer() { 57 | httpServer.close(); 58 | connections.clear(); 59 | } 60 | 61 | @Test 62 | @DisplayName("when use HTTP 1.1") 63 | public void testHttp11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { 64 | int pollSize = 3; 65 | int nbRequests = 100; 66 | 67 | HttpClientOptions options = new HttpClientOptions() 68 | .setMaxPoolSize(pollSize); 69 | 70 | HelloServiceAPI client = VertxFeign 71 | .builder() 72 | .vertx(vertx) 73 | .options(options) 74 | .encoder(new JacksonEncoder()) 75 | .decoder(new JacksonDecoder()) 76 | .target(HelloServiceAPI.class, "http://localhost:8091"); 77 | 78 | assertNotLeaks(client, testContext, nbRequests, pollSize); 79 | } 80 | 81 | @Test 82 | @DisplayName("when use HTTP 2") 83 | public void testHttp2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { 84 | int pollSize = 1; 85 | int nbRequests = 100; 86 | 87 | HttpClientOptions options = new HttpClientOptions() 88 | .setProtocolVersion(HttpVersion.HTTP_2) 89 | .setHttp2MaxPoolSize(1); 90 | 91 | HelloServiceAPI client = VertxFeign 92 | .builder() 93 | .vertx(vertx) 94 | .options(options) 95 | .encoder(new JacksonEncoder()) 96 | .decoder(new JacksonDecoder()) 97 | .target(HelloServiceAPI.class, "http://localhost:8091"); 98 | 99 | assertNotLeaks(client, testContext, nbRequests, pollSize); 100 | } 101 | 102 | void assertNotLeaks(HelloServiceAPI client, VertxTestContext testContext, int nbRequests, int pollSize) { 103 | List futures = IntStream 104 | .range(0, nbRequests) 105 | .mapToObj(ignored -> client.hello()) 106 | .collect(toList()); 107 | 108 | CompositeFuture 109 | .all(futures) 110 | .onComplete(ignored -> testContext.verify(() -> { 111 | try { 112 | assertThat(this.connections.size()) 113 | .isEqualTo(pollSize); 114 | testContext.completeNow(); 115 | } catch (Throwable assertionFailure) { 116 | testContext.failNow(assertionFailure); 117 | } 118 | })); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/Http11ClientReconnectTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.VertxFeign; 4 | import feign.jackson.JacksonDecoder; 5 | import feign.jackson.JacksonEncoder; 6 | import feign.vertx.testcase.HelloServiceAPI; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.http.HttpClientOptions; 9 | import org.junit.jupiter.api.BeforeAll; 10 | import org.junit.jupiter.api.DisplayName; 11 | 12 | @DisplayName("Tests of reconnection with HTTP 1.1") 13 | public class Http11ClientReconnectTest extends AbstractClientReconnectTest { 14 | 15 | @BeforeAll 16 | @Override 17 | protected void createClient(final Vertx vertx) { 18 | HttpClientOptions options = new HttpClientOptions() 19 | .setMaxPoolSize(3); 20 | 21 | client = VertxFeign 22 | .builder() 23 | .vertx(vertx) 24 | .options(options) 25 | .encoder(new JacksonEncoder()) 26 | .decoder(new JacksonDecoder()) 27 | .target(HelloServiceAPI.class, baseUrl); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/Http2ClientReconnectTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.VertxFeign; 4 | import feign.jackson.JacksonDecoder; 5 | import feign.jackson.JacksonEncoder; 6 | import feign.vertx.testcase.HelloServiceAPI; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.http.HttpClientOptions; 9 | import io.vertx.core.http.HttpVersion; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.DisplayName; 12 | 13 | @DisplayName("Tests of reconnection with HTTP 2") 14 | public class Http2ClientReconnectTest extends AbstractClientReconnectTest { 15 | 16 | @BeforeAll 17 | @Override 18 | protected void createClient(Vertx vertx) { 19 | HttpClientOptions options = new HttpClientOptions() 20 | .setProtocolVersion(HttpVersion.HTTP_2) 21 | .setHttp2MaxPoolSize(1); 22 | 23 | client = VertxFeign 24 | .builder() 25 | .vertx(vertx) 26 | .options(options) 27 | .encoder(new JacksonEncoder()) 28 | .decoder(new JacksonDecoder()) 29 | .target(HelloServiceAPI.class, baseUrl); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/QueryMapEncoderTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.Logger; 4 | import feign.QueryMap; 5 | import feign.QueryMapEncoder; 6 | import feign.RequestLine; 7 | import feign.VertxFeign; 8 | import feign.jackson.JacksonDecoder; 9 | import feign.slf4j.Slf4jLogger; 10 | import feign.vertx.testcase.domain.Bill; 11 | import feign.vertx.testcase.domain.Flavor; 12 | import feign.vertx.testcase.domain.IceCreamOrder; 13 | import io.vertx.core.Future; 14 | import io.vertx.core.Vertx; 15 | import io.vertx.core.http.HttpClientOptions; 16 | import io.vertx.junit5.VertxTestContext; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.DisplayName; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.util.Comparator; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.stream.Collectors; 25 | 26 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 27 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 28 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | 31 | @DisplayName("Tests of QueryMapEncoder") 32 | public class QueryMapEncoderTest extends AbstractFeignVertxTest { 33 | interface Api { 34 | 35 | @RequestLine("POST /icecream/orders") 36 | Future makeOrder(@QueryMap IceCreamOrder order); 37 | } 38 | 39 | Api client; 40 | 41 | @BeforeEach 42 | void createClient(Vertx vertx) { 43 | client = VertxFeign 44 | .builder() 45 | .vertx(vertx) 46 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 47 | .options(new HttpClientOptions().setLogActivity(true)) 48 | .queryMapEncoder(new CustomQueryMapEncoder()) 49 | .logger(new Slf4jLogger()) 50 | .logLevel(Logger.Level.FULL) 51 | .target(Api.class, wireMock.baseUrl()); 52 | } 53 | 54 | @Test 55 | @DisplayName("QueryMapEncoder will be used") 56 | void testWillMakeOrder(VertxTestContext testContext) { 57 | 58 | /* Given */ 59 | IceCreamOrder order = new IceCreamOrder(); 60 | order.addBall(Flavor.PISTACHIO); 61 | order.addBall(Flavor.PISTACHIO); 62 | order.addBall(Flavor.STRAWBERRY); 63 | order.addBall(Flavor.BANANA); 64 | order.addBall(Flavor.VANILLA); 65 | 66 | Bill bill = Bill.makeBill(order); 67 | String billStr = TestUtils.encodeAsJsonString(bill); 68 | 69 | wireMock.stubFor(post(urlEqualTo("/icecream/orders?balls=BANANA:1,PISTACHIO:2,STRAWBERRY:1,VANILLA:1")) 70 | .willReturn(aResponse() 71 | .withStatus(200) 72 | .withHeader("Content-Type", "application/json") 73 | .withBody(billStr))); 74 | 75 | /* When */ 76 | Future billFuture = client.makeOrder(order); 77 | 78 | /* Then */ 79 | billFuture.onComplete(res -> testContext.verify(() -> { 80 | if (res.succeeded()) { 81 | assertThat(res.result()) 82 | .isEqualTo(bill); 83 | testContext.completeNow(); 84 | } else { 85 | testContext.failNow(res.cause()); 86 | } 87 | })); 88 | } 89 | 90 | class CustomQueryMapEncoder implements QueryMapEncoder { 91 | @Override 92 | public Map encode(final Object o) { 93 | IceCreamOrder order = (IceCreamOrder) o; 94 | 95 | String balls = order 96 | .getBalls() 97 | .entrySet() 98 | .stream() 99 | .sorted(Comparator.comparing(en -> en.getKey().toString())) 100 | .map(entry -> entry.getKey().toString() + ':' + entry.getValue()) 101 | .collect(Collectors.joining(",")); 102 | 103 | Map encoded = new HashMap<>(); 104 | encoded.put("balls", balls); 105 | return encoded; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/RawContractTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.Response; 4 | import feign.VertxFeign; 5 | import feign.jackson.JacksonDecoder; 6 | import feign.jackson.JacksonEncoder; 7 | import feign.vertx.testcase.RawServiceAPI; 8 | import feign.vertx.testcase.domain.Bill; 9 | import io.vertx.core.Future; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.junit5.VertxTestContext; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.IOException; 18 | import java.io.InputStreamReader; 19 | import java.util.stream.Collectors; 20 | 21 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 25 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 26 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 27 | import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | @DisplayName("When creating client from 'raw' contract") 31 | public class RawContractTest extends AbstractFeignVertxTest { 32 | static RawServiceAPI client; 33 | 34 | @BeforeAll 35 | static void createClient(Vertx vertx) { 36 | client = VertxFeign 37 | .builder() 38 | .vertx(vertx) 39 | .encoder(new JacksonEncoder(TestUtils.MAPPER)) 40 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 41 | .target(RawServiceAPI.class, wireMock.baseUrl()); 42 | } 43 | 44 | @Test 45 | @DisplayName("should get available flavors") 46 | public void testGetAvailableFlavors(VertxTestContext testContext) { 47 | 48 | /* Given */ 49 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 50 | .withHeader("Accept", equalTo("application/json")) 51 | .willReturn(aResponse() 52 | .withStatus(200) 53 | .withHeader("Content-Type", "application/json") 54 | .withBody(FLAVORS_JSON))); 55 | 56 | /* When */ 57 | Future flavorsFuture = client.getAvailableFlavors(); 58 | 59 | /* Then */ 60 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 61 | if (res.succeeded()) { 62 | Response response = res.result(); 63 | try { 64 | String content = new BufferedReader(new InputStreamReader( 65 | response.body().asInputStream())) 66 | .lines() 67 | .collect(Collectors.joining("\n")); 68 | 69 | assertThat(response.status()) 70 | .isEqualTo(200); 71 | assertThat(content) 72 | .isEqualTo(FLAVORS_JSON); 73 | testContext.completeNow(); 74 | } catch (IOException ioException) { 75 | testContext.failNow(ioException); 76 | } 77 | } else { 78 | testContext.failNow(res.cause()); 79 | } 80 | })); 81 | } 82 | 83 | @Test 84 | @DisplayName("should pay bill") 85 | public void testPayBill(VertxTestContext testContext) { 86 | 87 | /* Given */ 88 | Bill bill = Bill.makeBill(generator.generate()); 89 | String billStr = TestUtils.encodeAsJsonString(bill); 90 | 91 | wireMock.stubFor(post(urlEqualTo("/icecream/bills/pay")) 92 | .withHeader("Content-Type", equalTo("application/json")) 93 | .withRequestBody(equalToJson(billStr)) 94 | .willReturn(aResponse() 95 | .withStatus(200))); 96 | 97 | /* When */ 98 | Future payedFuture = client.payBill(bill); 99 | 100 | /* Then */ 101 | payedFuture.onComplete(res -> testContext.verify(() -> { 102 | if (res.succeeded()) { 103 | testContext.completeNow(); 104 | } else { 105 | testContext.failNow(res.cause()); 106 | } 107 | })); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/RequestPreProcessorTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.Logger; 4 | import feign.VertxFeign; 5 | import feign.jackson.JacksonDecoder; 6 | import feign.slf4j.Slf4jLogger; 7 | import feign.vertx.testcase.IcecreamServiceApi; 8 | import feign.vertx.testcase.domain.Flavor; 9 | import io.vertx.core.Future; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.core.http.HttpClientOptions; 12 | import io.vertx.junit5.VertxTestContext; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.DisplayName; 15 | import org.junit.jupiter.api.Test; 16 | 17 | import java.util.Arrays; 18 | import java.util.Collection; 19 | 20 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 21 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 24 | import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | @DisplayName("Test request pre processor") 28 | public class RequestPreProcessorTest extends AbstractFeignVertxTest { 29 | IcecreamServiceApi client; 30 | 31 | @BeforeEach 32 | void createClient(Vertx vertx) { 33 | client = VertxFeign 34 | .builder() 35 | .vertx(vertx) 36 | .options(new HttpClientOptions().setLogActivity(true)) 37 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 38 | .requestPreProcessor(req -> req.addQueryParam("version", "v1")) 39 | .logger(new Slf4jLogger()) 40 | .logLevel(Logger.Level.FULL) 41 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 42 | } 43 | 44 | @Test 45 | @DisplayName("request pre processor must be applied") 46 | void testRequestPreProcessorMustApply(VertxTestContext testContext) { 47 | 48 | /* Given */ 49 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors?version=v1")) 50 | .withHeader("Accept", equalTo("application/json")) 51 | .willReturn(aResponse() 52 | .withStatus(200) 53 | .withFixedDelay(100) 54 | .withHeader("Content-Type", "application/json") 55 | .withBody(FLAVORS_JSON))); 56 | 57 | Future> flavorsFuture = client.getAvailableFlavors(); 58 | 59 | /* Then */ 60 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 61 | if (res.succeeded()) { 62 | Collection flavors = res.result(); 63 | assertThat(flavors) 64 | .hasSize(Flavor.values().length) 65 | .containsAll(Arrays.asList(Flavor.values())); 66 | testContext.completeNow(); 67 | } else { 68 | testContext.failNow(res.cause()); 69 | } 70 | })); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/RetryingTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.Logger; 4 | import feign.RetryableException; 5 | import feign.Retryer; 6 | import feign.VertxFeign; 7 | import feign.jackson.JacksonDecoder; 8 | import feign.slf4j.Slf4jLogger; 9 | import feign.vertx.testcase.IcecreamServiceApi; 10 | import feign.vertx.testcase.domain.Flavor; 11 | import io.vertx.core.Future; 12 | import io.vertx.core.Vertx; 13 | import io.vertx.junit5.VertxTestContext; 14 | import org.junit.jupiter.api.BeforeAll; 15 | import org.junit.jupiter.api.DisplayName; 16 | import org.junit.jupiter.api.Test; 17 | 18 | import java.util.Arrays; 19 | import java.util.Collection; 20 | 21 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 25 | import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; 26 | import static feign.vertx.TestUtils.MAPPER; 27 | import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; 28 | import static java.util.concurrent.TimeUnit.SECONDS; 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | 31 | @DisplayName("When server ask client to retry") 32 | public class RetryingTest extends AbstractFeignVertxTest { 33 | static IcecreamServiceApi client; 34 | 35 | @BeforeAll 36 | static void createClient(Vertx vertx) { 37 | client = VertxFeign 38 | .builder() 39 | .vertx(vertx) 40 | .decoder(new JacksonDecoder(MAPPER)) 41 | .retryer(new Retryer.Default(100, SECONDS.toMillis(1), 5)) 42 | .logger(new Slf4jLogger()) 43 | .logLevel(Logger.Level.FULL) 44 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 45 | } 46 | 47 | @Test 48 | @DisplayName("should succeed when client retries less than max attempts") 49 | public void testRetrying_success(VertxTestContext testContext) { 50 | 51 | /* Given */ 52 | String scenario = "testRetrying_success"; 53 | 54 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 55 | .withHeader("Accept", equalTo("application/json")) 56 | .inScenario(scenario) 57 | .whenScenarioStateIs(STARTED) 58 | .willReturn(aResponse() 59 | .withStatus(503) 60 | .withHeader("Retry-After", "1")) 61 | .willSetStateTo("attempt1")); 62 | 63 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 64 | .withHeader("Accept", equalTo("application/json")) 65 | .inScenario(scenario) 66 | .whenScenarioStateIs("attempt1") 67 | .willReturn(aResponse() 68 | .withStatus(503) 69 | .withHeader("Retry-After", "1")) 70 | .willSetStateTo("attempt2")); 71 | 72 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 73 | .withHeader("Accept", equalTo("application/json")) 74 | .inScenario(scenario) 75 | .whenScenarioStateIs("attempt2") 76 | .willReturn(aResponse() 77 | .withStatus(200) 78 | .withHeader("Content-Type", "application/json") 79 | .withBody(FLAVORS_JSON))); 80 | 81 | /* When */ 82 | Future> flavorsFuture = client.getAvailableFlavors(); 83 | 84 | /* Then */ 85 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 86 | if (res.succeeded()) { 87 | assertThat(res.result()) 88 | .hasSize(Flavor.values().length) 89 | .containsAll(Arrays.asList(Flavor.values())); 90 | testContext.completeNow(); 91 | } else { 92 | testContext.failNow(res.cause()); 93 | } 94 | })); 95 | } 96 | 97 | @Test 98 | @DisplayName("should fail when after max number of attempts") 99 | public void testRetrying_noMoreAttempts(VertxTestContext testContext) { 100 | 101 | /* Given */ 102 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 103 | .withHeader("Accept", equalTo("application/json")) 104 | .willReturn(aResponse() 105 | .withStatus(503) 106 | .withHeader("Retry-After", "1"))); 107 | 108 | /* When */ 109 | Future> flavorsFuture = client.getAvailableFlavors(); 110 | 111 | /* Then */ 112 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 113 | if (res.failed()) { 114 | assertThat(res.cause()) 115 | .isInstanceOf(RetryableException.class) 116 | .hasMessageContaining("503 Service Unavailable"); 117 | testContext.completeNow(); 118 | } else { 119 | testContext.failNow(new IllegalStateException("RetryableException excepted but not occurred")); 120 | } 121 | })); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/TestUtils.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 | 7 | class TestUtils { 8 | static final ObjectMapper MAPPER = new ObjectMapper(); 9 | static { 10 | MAPPER.registerModule(new JavaTimeModule()); 11 | } 12 | 13 | static String encodeAsJsonString(final Object object) { 14 | try { 15 | return MAPPER.writeValueAsString(object); 16 | } catch (JsonProcessingException unexpectedException) { 17 | return ""; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/TimeoutHandlingTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.Logger; 4 | import feign.VertxFeign; 5 | import feign.jackson.JacksonDecoder; 6 | import feign.slf4j.Slf4jLogger; 7 | import feign.vertx.testcase.IcecreamServiceApi; 8 | import feign.vertx.testcase.domain.Flavor; 9 | import io.vertx.core.Future; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.core.http.HttpClientOptions; 12 | import io.vertx.junit5.VertxTestContext; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.DisplayName; 15 | import org.junit.jupiter.api.Test; 16 | 17 | import java.util.Arrays; 18 | import java.util.Collection; 19 | import java.util.concurrent.TimeoutException; 20 | 21 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 25 | import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | @DisplayName("Tests of handling of timeouts") 29 | public class TimeoutHandlingTest extends AbstractFeignVertxTest { 30 | IcecreamServiceApi client; 31 | 32 | @BeforeEach 33 | void createClient(Vertx vertx) { 34 | client = VertxFeign 35 | .builder() 36 | .vertx(vertx) 37 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 38 | .timeout(1000) 39 | .options(new HttpClientOptions().setLogActivity(true)) 40 | .logger(new Slf4jLogger()) 41 | .logLevel(Logger.Level.FULL) 42 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 43 | } 44 | 45 | @Test 46 | @DisplayName("when timeout is reached") 47 | void testWhenTimeoutIsReached(VertxTestContext testContext) { 48 | 49 | /* Given */ 50 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 51 | .withHeader("Accept", equalTo("application/json")) 52 | .willReturn(aResponse() 53 | .withStatus(200) 54 | .withFixedDelay(1500) 55 | .withHeader("Content-Type", "application/json") 56 | .withBody(FLAVORS_JSON))); 57 | 58 | Future> flavorsFuture = client.getAvailableFlavors(); 59 | 60 | /* Then */ 61 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 62 | if (res.succeeded()) { 63 | testContext.failNow("should timeout!"); 64 | } else { 65 | assertThat(res.cause()) 66 | .isInstanceOf(TimeoutException.class) 67 | .hasMessageContaining("timeout"); 68 | testContext.completeNow(); 69 | } 70 | })); 71 | } 72 | 73 | @Test 74 | @DisplayName("when timeout is not reached") 75 | void testWhenTimeoutIsNotReached(VertxTestContext testContext) { 76 | 77 | /* Given */ 78 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 79 | .withHeader("Accept", equalTo("application/json")) 80 | .willReturn(aResponse() 81 | .withStatus(200) 82 | .withFixedDelay(100) 83 | .withHeader("Content-Type", "application/json") 84 | .withBody(FLAVORS_JSON))); 85 | 86 | Future> flavorsFuture = client.getAvailableFlavors(); 87 | 88 | /* Then */ 89 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 90 | if (res.succeeded()) { 91 | Collection flavors = res.result(); 92 | assertThat(flavors) 93 | .hasSize(Flavor.values().length) 94 | .containsAll(Arrays.asList(Flavor.values())); 95 | testContext.completeNow(); 96 | } else { 97 | testContext.failNow(res.cause()); 98 | } 99 | })); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/VertxHttpClientTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.FeignException; 4 | import feign.Logger; 5 | import feign.Request; 6 | import feign.VertxFeign; 7 | import feign.jackson.JacksonDecoder; 8 | import feign.jackson.JacksonEncoder; 9 | import feign.slf4j.Slf4jLogger; 10 | import feign.vertx.testcase.IcecreamServiceApi; 11 | import feign.vertx.testcase.IcecreamServiceApiBroken; 12 | import feign.vertx.testcase.domain.Bill; 13 | import feign.vertx.testcase.domain.Flavor; 14 | import feign.vertx.testcase.domain.IceCreamOrder; 15 | import feign.vertx.testcase.domain.Mixin; 16 | import io.vertx.core.Future; 17 | import io.vertx.core.Vertx; 18 | import io.vertx.junit5.VertxTestContext; 19 | import org.assertj.core.api.ThrowableAssert; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.DisplayName; 22 | import org.junit.jupiter.api.Nested; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import java.util.Arrays; 26 | import java.util.Collection; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 30 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 31 | import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; 32 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 33 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 34 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 35 | import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; 36 | import static feign.vertx.testcase.domain.Mixin.MIXINS_JSON; 37 | import static org.assertj.core.api.Assertions.assertThat; 38 | import static org.assertj.core.api.Assertions.assertThatCode; 39 | 40 | @DisplayName("FeignVertx client") 41 | public class VertxHttpClientTest extends AbstractFeignVertxTest { 42 | 43 | @Nested 44 | @DisplayName("When make a GET request") 45 | class WhenMakeGetRequest { 46 | IcecreamServiceApi client; 47 | 48 | @BeforeEach 49 | void createClient(Vertx vertx) { 50 | client = VertxFeign 51 | .builder() 52 | .vertx(vertx) 53 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 54 | .options(new Request.Options(5L, TimeUnit.SECONDS, 5L, TimeUnit.SECONDS, true)) 55 | .logger(new Slf4jLogger()) 56 | .logLevel(Logger.Level.FULL) 57 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 58 | } 59 | 60 | @Test 61 | @DisplayName("will get flavors") 62 | void testWillGetFlavors(VertxTestContext testContext) { 63 | 64 | /* Given */ 65 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 66 | .withHeader("Accept", equalTo("application/json")) 67 | .willReturn(aResponse() 68 | .withStatus(200) 69 | .withHeader("Content-Type", "application/json") 70 | .withBody(FLAVORS_JSON))); 71 | 72 | /* When */ 73 | Future> flavorsFuture = client.getAvailableFlavors(); 74 | 75 | /* Then */ 76 | flavorsFuture.onComplete(res -> testContext.verify(() -> { 77 | if (res.succeeded()) { 78 | Collection flavors = res.result(); 79 | 80 | assertThat(flavors) 81 | .hasSize(Flavor.values().length) 82 | .containsAll(Arrays.asList(Flavor.values())); 83 | testContext.completeNow(); 84 | } else { 85 | testContext.failNow(res.cause()); 86 | } 87 | })); 88 | } 89 | 90 | @Test 91 | @DisplayName("will get mixins") 92 | void testWillGetMixins(VertxTestContext testContext) { 93 | 94 | /* Given */ 95 | wireMock.stubFor(get(urlEqualTo("/icecream/mixins")) 96 | .withHeader("Accept", equalTo("application/json")) 97 | .willReturn(aResponse() 98 | .withStatus(200) 99 | .withHeader("Content-Type", "application/json") 100 | .withBody(MIXINS_JSON))); 101 | 102 | /* When */ 103 | Future> mixinsFuture = client.getAvailableMixins(); 104 | 105 | /* Then */ 106 | mixinsFuture.onComplete(res -> testContext.verify(() -> { 107 | if (res.succeeded()) { 108 | Collection mixins = res.result(); 109 | 110 | assertThat(mixins) 111 | .hasSize(Mixin.values().length) 112 | .containsAll(Arrays.asList(Mixin.values())); 113 | testContext.completeNow(); 114 | } else { 115 | testContext.failNow(res.cause()); 116 | } 117 | })); 118 | } 119 | 120 | @Test 121 | @DisplayName("will get order by id") 122 | void testWillGetOrderById(VertxTestContext testContext) { 123 | 124 | /* Given */ 125 | IceCreamOrder order = generator.generate(); 126 | int orderId = order.getId(); 127 | String orderStr = TestUtils.encodeAsJsonString(order); 128 | 129 | wireMock.stubFor(get(urlEqualTo("/icecream/orders/" + orderId)) 130 | .withHeader("Accept", equalTo("application/json")) 131 | .willReturn(aResponse() 132 | .withStatus(200) 133 | .withHeader("Content-Type", "application/json") 134 | .withBody(orderStr))); 135 | 136 | /* When */ 137 | Future orderFuture = client.findOrder(orderId); 138 | 139 | /* Then */ 140 | orderFuture.onComplete(res -> testContext.verify(() -> { 141 | if (res.succeeded()) { 142 | assertThat(res.result()) 143 | .isEqualTo(order); 144 | testContext.completeNow(); 145 | } else { 146 | testContext.failNow(res.cause()); 147 | } 148 | })); 149 | } 150 | 151 | @Test 152 | @DisplayName("will return 404 when try to get non-existing order by id") 153 | void testWillReturn404WhenTryToGetNonExistingOrderById(VertxTestContext testContext) { 154 | 155 | /* Given */ 156 | wireMock.stubFor(get(urlEqualTo("/icecream/orders/123")) 157 | .withHeader("Accept", equalTo("application/json")) 158 | .willReturn(aResponse().withStatus(404))); 159 | 160 | /* When */ 161 | Future orderFuture = client.findOrder(123); 162 | 163 | /* Then */ 164 | orderFuture.onComplete(res -> testContext.verify(() -> { 165 | if (res.failed()) { 166 | assertThat(res.cause()) 167 | .isInstanceOf(FeignException.class) 168 | .hasMessageContaining("404 Not Found"); 169 | testContext.completeNow(); 170 | } else { 171 | testContext.failNow(new IllegalStateException("FeignException excepted but not occurred")); 172 | } 173 | })); 174 | } 175 | } 176 | 177 | @Nested 178 | @DisplayName("When make a POST request") 179 | class WhenMakePostRequest { 180 | IcecreamServiceApi client; 181 | 182 | @BeforeEach 183 | void createClient(Vertx vertx) { 184 | client = VertxFeign 185 | .builder() 186 | .vertx(vertx) 187 | .encoder(new JacksonEncoder(TestUtils.MAPPER)) 188 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 189 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 190 | } 191 | 192 | @Test 193 | @DisplayName("will make an order") 194 | void testWillMakeOrder(VertxTestContext testContext) { 195 | 196 | /* Given */ 197 | IceCreamOrder order = generator.generate(); 198 | Bill bill = Bill.makeBill(order); 199 | String orderStr = TestUtils.encodeAsJsonString(order); 200 | String billStr = TestUtils.encodeAsJsonString(bill); 201 | 202 | wireMock.stubFor(post(urlEqualTo("/icecream/orders")) 203 | .withHeader("Content-Type", equalTo("application/json")) 204 | .withHeader("Accept", equalTo("application/json")) 205 | .withRequestBody(equalToJson(orderStr)) 206 | .willReturn(aResponse() 207 | .withStatus(200) 208 | .withHeader("Content-Type", "application/json") 209 | .withBody(billStr))); 210 | 211 | /* When */ 212 | Future billFuture = client.makeOrder(order); 213 | 214 | /* Then */ 215 | billFuture.onComplete(res -> testContext.verify(() -> { 216 | if (res.succeeded()) { 217 | assertThat(res.result()) 218 | .isEqualTo(bill); 219 | testContext.completeNow(); 220 | } else { 221 | testContext.failNow(res.cause()); 222 | } 223 | })); 224 | } 225 | 226 | @Test 227 | @DisplayName("will pay bill") 228 | void testWillPayBill(VertxTestContext testContext) { 229 | 230 | /* Given */ 231 | Bill bill = Bill.makeBill(generator.generate()); 232 | String billStr = TestUtils.encodeAsJsonString(bill); 233 | 234 | wireMock.stubFor(post(urlEqualTo("/icecream/bills/pay")) 235 | .withHeader("Content-Type", equalTo("application/json")) 236 | .withRequestBody(equalToJson(billStr)) 237 | .willReturn(aResponse() 238 | .withStatus(200))); 239 | 240 | /* When */ 241 | Future payedFuture = client.payBill(bill); 242 | 243 | /* Then */ 244 | payedFuture.onComplete(res -> testContext.verify(() -> { 245 | if (res.succeeded()) { 246 | testContext.completeNow(); 247 | } else { 248 | testContext.failNow(res.cause()); 249 | } 250 | })); 251 | } 252 | } 253 | 254 | @Nested 255 | @DisplayName("Should fail client instantiation") 256 | class ShouldFailedClientInstantiation { 257 | 258 | @Test 259 | @DisplayName("when Vertx is not provided") 260 | void testWhenVertxMissing() { 261 | 262 | /* Given */ 263 | ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = () -> VertxFeign 264 | .builder() 265 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 266 | 267 | /* Then */ 268 | assertThatCode(instantiateContractForgottenVertx) 269 | .isInstanceOf(NullPointerException.class) 270 | .hasMessage("Vertx instance wasn't provided in VertxFeign builder"); 271 | } 272 | 273 | @Test 274 | @DisplayName("when try to instantiate contract that have method that not return future") 275 | void testWhenTryToInstantiateBrokenContract(Vertx vertx) { 276 | 277 | /* Given */ 278 | ThrowableAssert.ThrowingCallable instantiateBrokenContract = () -> VertxFeign 279 | .builder() 280 | .vertx(vertx) 281 | .target(IcecreamServiceApiBroken.class, wireMock.baseUrl()); 282 | 283 | /* Then */ 284 | assertThatCode(instantiateBrokenContract) 285 | .isInstanceOf(IllegalStateException.class) 286 | .hasMessageContaining("IcecreamServiceApiBroken#findOrder(int)"); 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/VertxHttpOptionsTest.java: -------------------------------------------------------------------------------- 1 | package feign.vertx; 2 | 3 | import feign.Logger; 4 | import feign.Request; 5 | import feign.VertxFeign; 6 | import feign.jackson.JacksonDecoder; 7 | import feign.slf4j.Slf4jLogger; 8 | import feign.vertx.testcase.IcecreamServiceApi; 9 | import feign.vertx.testcase.domain.Flavor; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.core.http.HttpClientOptions; 12 | import io.vertx.core.http.HttpVersion; 13 | import io.vertx.junit5.VertxTestContext; 14 | import org.junit.jupiter.api.BeforeAll; 15 | import org.junit.jupiter.api.DisplayName; 16 | import org.junit.jupiter.api.Test; 17 | 18 | import java.util.Arrays; 19 | import java.util.Collection; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 25 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 26 | import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | @DisplayName("FeignVertx client should be created from") 30 | public class VertxHttpOptionsTest extends AbstractFeignVertxTest { 31 | 32 | @BeforeAll 33 | static void setupStub() { 34 | wireMock.stubFor(get(urlEqualTo("/icecream/flavors")) 35 | .withHeader("Accept", equalTo("application/json")) 36 | .willReturn(aResponse() 37 | .withStatus(200) 38 | .withHeader("Content-Type", "application/json") 39 | .withBody(FLAVORS_JSON))); 40 | } 41 | 42 | @Test 43 | @DisplayName("HttpClientOptions from Vertx") 44 | public void testHttpClientOptions(Vertx vertx, VertxTestContext testContext) { 45 | HttpClientOptions options = new HttpClientOptions() 46 | .setProtocolVersion(HttpVersion.HTTP_2) 47 | .setHttp2MaxPoolSize(1) 48 | .setConnectTimeout(5000) 49 | .setIdleTimeout(5000); 50 | 51 | IcecreamServiceApi client = VertxFeign 52 | .builder() 53 | .vertx(vertx) 54 | .options(options) 55 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 56 | .logger(new Slf4jLogger()) 57 | .logLevel(Logger.Level.FULL) 58 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 59 | 60 | testClient(client, testContext); 61 | } 62 | 63 | @Test 64 | @DisplayName("Request Options from Feign") 65 | public void testRequestOptions(Vertx vertx, VertxTestContext testContext) { 66 | IcecreamServiceApi client = VertxFeign 67 | .builder() 68 | .vertx(vertx) 69 | .options(new Request.Options(5L, TimeUnit.SECONDS, 5L, TimeUnit.SECONDS, true)) 70 | .decoder(new JacksonDecoder(TestUtils.MAPPER)) 71 | .logger(new Slf4jLogger()) 72 | .logLevel(Logger.Level.FULL) 73 | .target(IcecreamServiceApi.class, wireMock.baseUrl()); 74 | 75 | testClient(client, testContext); 76 | } 77 | 78 | private void testClient(IcecreamServiceApi client, VertxTestContext testContext) { 79 | client.getAvailableFlavors().onComplete(res -> { 80 | if (res.succeeded()) { 81 | Collection flavors = res.result(); 82 | 83 | assertThat(flavors) 84 | .hasSize(Flavor.values().length) 85 | .containsAll(Arrays.asList(Flavor.values())); 86 | testContext.completeNow(); 87 | } else { 88 | testContext.failNow(res.cause()); 89 | } 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/HelloServiceAPI.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import feign.Response; 6 | import io.vertx.core.Future; 7 | 8 | /** 9 | * Example of an API to to test number of Http2 connections of Feign. 10 | * 11 | * @author James Xu 12 | */ 13 | @Headers({ "Accept: application/json" }) 14 | public interface HelloServiceAPI { 15 | 16 | @RequestLine("GET /hello") 17 | Future hello(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/IcecreamServiceApi.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase; 2 | 3 | import feign.Headers; 4 | import feign.Param; 5 | import feign.RequestLine; 6 | import feign.vertx.testcase.domain.Bill; 7 | import feign.vertx.testcase.domain.Flavor; 8 | import feign.vertx.testcase.domain.IceCreamOrder; 9 | import feign.vertx.testcase.domain.Mixin; 10 | import io.vertx.core.Future; 11 | 12 | import java.util.Collection; 13 | 14 | /** 15 | * API of an iceream web service. 16 | * 17 | * @author Alexei KLENIN 18 | */ 19 | @Headers({ "Accept: application/json" }) 20 | public interface IcecreamServiceApi { 21 | 22 | @RequestLine("GET /icecream/flavors") 23 | Future> getAvailableFlavors(); 24 | 25 | @RequestLine("GET /icecream/mixins") 26 | Future> getAvailableMixins(); 27 | 28 | @RequestLine("POST /icecream/orders") 29 | @Headers("Content-Type: application/json") 30 | Future makeOrder(IceCreamOrder order); 31 | 32 | @RequestLine("GET /icecream/orders/{orderId}") 33 | Future findOrder(@Param("orderId") int orderId); 34 | 35 | @RequestLine("POST /icecream/bills/pay") 36 | @Headers("Content-Type: application/json") 37 | Future payBill(Bill bill); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/IcecreamServiceApiBroken.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase; 2 | 3 | import feign.Headers; 4 | import feign.Param; 5 | import feign.RequestLine; 6 | import feign.vertx.VertxDelegatingContract; 7 | import feign.vertx.testcase.domain.Bill; 8 | import feign.vertx.testcase.domain.Flavor; 9 | import feign.vertx.testcase.domain.IceCreamOrder; 10 | import feign.vertx.testcase.domain.Mixin; 11 | import io.vertx.core.Future; 12 | 13 | import java.util.Collection; 14 | 15 | /** 16 | * API of an iceream web service with one method that doesn't returns 17 | * {@link Future} and violates {@link VertxDelegatingContract}s rules. 18 | * 19 | * @author Alexei KLENIN 20 | */ 21 | public interface IcecreamServiceApiBroken { 22 | 23 | @RequestLine("GET /icecream/flavors") 24 | Future> getAvailableFlavors(); 25 | 26 | @RequestLine("GET /icecream/mixins") 27 | Future> getAvailableMixins(); 28 | 29 | @RequestLine("POST /icecream/orders") 30 | @Headers("Content-Type: application/json") 31 | Future makeOrder(IceCreamOrder order); 32 | 33 | /** Method that doesn't respects contract. */ 34 | @RequestLine("GET /icecream/orders/{orderId}") 35 | IceCreamOrder findOrder(@Param("orderId") int orderId); 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/RawServiceAPI.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import feign.Response; 6 | import feign.vertx.testcase.domain.Bill; 7 | import io.vertx.core.Future; 8 | 9 | /** 10 | * Example of an API to to test rarely used features of Feign. 11 | * 12 | * @author Alexei KLENIN 13 | */ 14 | @Headers({ "Accept: application/json" }) 15 | public interface RawServiceAPI { 16 | 17 | @RequestLine("GET /icecream/flavors") 18 | Future getAvailableFlavors(); 19 | 20 | @RequestLine("POST /icecream/bills/pay") 21 | @Headers("Content-Type: application/json") 22 | Future payBill(Bill bill); 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/domain/Bill.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase.domain; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | /** 8 | * Bill for consumed ice cream. 9 | * 10 | * @author Alexei KLENIN 11 | */ 12 | public class Bill { 13 | private static final Map PRICES = new HashMap<>(); 14 | static { 15 | PRICES.put(1, (float) 2.00); // two euros for one ball (expensive!) 16 | PRICES.put(3, (float) 2.85); // 2.85€ for 3 balls 17 | PRICES.put(5, (float) 4.30); // 4.30€ for 5 balls 18 | PRICES.put(7, (float) 5); // only five euros for seven balls! Wow 19 | } 20 | 21 | private static final float MIXIN_PRICE = (float) 0.6; // price per mixin 22 | 23 | private Float price; 24 | 25 | public Bill() { 26 | } 27 | 28 | public Bill(final Float price) { 29 | this.price = price; 30 | } 31 | 32 | public Float getPrice() { 33 | return price; 34 | } 35 | 36 | public void setPrice(final Float price) { 37 | this.price = price; 38 | } 39 | 40 | /** 41 | * Makes a bill from an order. 42 | * 43 | * @param order ice cream order 44 | * 45 | * @return bill 46 | */ 47 | public static Bill makeBill(final IceCreamOrder order) { 48 | int nbBalls = order 49 | .getBalls() 50 | .values() 51 | .stream() 52 | .mapToInt(Integer::intValue) 53 | .sum(); 54 | Float price = PRICES.get(nbBalls) + order.getMixins().size() * MIXIN_PRICE; 55 | return new Bill(price); 56 | } 57 | 58 | @Override 59 | public boolean equals(final Object other) { 60 | if (this == other) { 61 | return true; 62 | } 63 | 64 | if (!(other instanceof Bill)) { 65 | return false; 66 | } 67 | 68 | final Bill another = (Bill) other; 69 | return Objects.equals(price, another.price); 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | return Objects.hash(price); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/domain/Flavor.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase.domain; 2 | 3 | import java.util.stream.Collectors; 4 | import java.util.stream.Stream; 5 | 6 | /** 7 | * Ice cream flavors. 8 | * 9 | * @author Alexei KLENIN 10 | */ 11 | public enum Flavor { 12 | STRAWBERRY, CHOCOLATE, BANANA, PISTACHIO, MELON, VANILLA; 13 | 14 | static public final String FLAVORS_JSON = Stream 15 | .of(Flavor.values()) 16 | .map(flavor -> "\"" + flavor + "\"") 17 | .collect(Collectors.joining(", ", "[ ", " ]")); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/domain/IceCreamOrder.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase.domain; 2 | 3 | import java.time.Instant; 4 | import java.util.HashMap; 5 | import java.util.LinkedHashSet; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.Set; 9 | import java.util.concurrent.ThreadLocalRandom; 10 | 11 | /** 12 | * Give me some ice-cream! :p 13 | * 14 | * @author Alexei KLENIN 15 | */ 16 | public class IceCreamOrder { 17 | private final int id; // order id 18 | private final Map balls; // how much balls of flavor 19 | private final Set mixins; // and some mixins ... 20 | private Instant orderTimestamp; // and give it to me right now ! 21 | 22 | public IceCreamOrder() { 23 | this(Instant.now()); 24 | } 25 | 26 | IceCreamOrder(final Instant orderTimestamp) { 27 | this.id = ThreadLocalRandom.current().nextInt(); 28 | this.balls = new HashMap<>(); 29 | this.mixins = new LinkedHashSet<>(); 30 | this.orderTimestamp = orderTimestamp; 31 | } 32 | 33 | public IceCreamOrder addBall(final Flavor ballFlavor) { 34 | final Integer ballCount = balls.containsKey(ballFlavor) 35 | ? balls.get(ballFlavor) + 1 36 | : 1; 37 | balls.put(ballFlavor, ballCount); 38 | return this; 39 | } 40 | 41 | IceCreamOrder addMixin(final Mixin mixin) { 42 | mixins.add(mixin); 43 | return this; 44 | } 45 | 46 | IceCreamOrder withOrderTimestamp(final Instant orderTimestamp) { 47 | this.orderTimestamp = orderTimestamp; 48 | return this; 49 | } 50 | 51 | public int getId() { 52 | return id; 53 | } 54 | 55 | public Map getBalls() { 56 | return balls; 57 | } 58 | 59 | public Set getMixins() { 60 | return mixins; 61 | } 62 | 63 | public Instant getOrderTimestamp() { 64 | return orderTimestamp; 65 | } 66 | 67 | @Override 68 | public boolean equals(final Object other) { 69 | if (this == other) { 70 | return true; 71 | } 72 | 73 | if (!(other instanceof IceCreamOrder)) { 74 | return false; 75 | } 76 | 77 | final IceCreamOrder another = (IceCreamOrder) other; 78 | return id == another.id 79 | && Objects.equals(balls, another.balls) 80 | && Objects.equals(mixins, another.mixins) 81 | && Objects.equals(orderTimestamp, another.orderTimestamp); 82 | } 83 | 84 | @Override 85 | public int hashCode() { 86 | return Objects.hash(id, balls, mixins, orderTimestamp); 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "IceCreamOrder{" 92 | + " id=" + id 93 | + ", balls=" + balls 94 | + ", mixins=" + mixins 95 | + ", orderTimestamp=" + orderTimestamp 96 | + '}'; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/domain/Mixin.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase.domain; 2 | 3 | import java.util.stream.Collectors; 4 | import java.util.stream.Stream; 5 | 6 | /** 7 | * Ice cream mix-ins. 8 | * 9 | * @author Alexei KLENIN 10 | */ 11 | public enum Mixin { 12 | COOKIES, MNMS, CHOCOLATE_SIROP, STRAWBERRY_SIROP, NUTS, RAINBOW; 13 | 14 | public static final String MIXINS_JSON = Stream 15 | .of(Mixin.values()) 16 | .map(flavor -> "\"" + flavor + "\"") 17 | .collect(Collectors.joining(", ", "[ ", " ]")); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/feign/vertx/testcase/domain/OrderGenerator.java: -------------------------------------------------------------------------------- 1 | package feign.vertx.testcase.domain; 2 | 3 | import java.util.Random; 4 | import java.util.stream.IntStream; 5 | 6 | /** 7 | * Generator of random ice cream orders. 8 | * 9 | * @author Alexei KLENIN 10 | */ 11 | public class OrderGenerator { 12 | private static final int[] BALLS_NUMBER = { 1, 3, 5, 7 }; 13 | private static final int[] MIXIN_NUMBER = { 1, 2, 3 }; 14 | 15 | private static final Random random = new Random(); 16 | 17 | public IceCreamOrder generate() { 18 | final IceCreamOrder order = new IceCreamOrder(); 19 | final int nbBalls = peekBallsNumber(); 20 | final int nbMixins = peekMixinNumber(); 21 | 22 | IntStream 23 | .rangeClosed(1, nbBalls) 24 | .mapToObj(i -> this.peekFlavor()) 25 | .forEach(order::addBall); 26 | 27 | IntStream 28 | .rangeClosed(1, nbMixins) 29 | .mapToObj(i -> this.peekMixin()) 30 | .forEach(order::addMixin); 31 | 32 | return order; 33 | } 34 | 35 | private int peekBallsNumber() { 36 | return BALLS_NUMBER[random.nextInt(BALLS_NUMBER.length)]; 37 | } 38 | 39 | private int peekMixinNumber() { 40 | return MIXIN_NUMBER[random.nextInt(MIXIN_NUMBER.length)]; 41 | } 42 | 43 | private Flavor peekFlavor() { 44 | return Flavor.values()[random.nextInt(Flavor.values().length)]; 45 | } 46 | 47 | private Mixin peekMixin() { 48 | return Mixin.values()[random.nextInt(Mixin.values().length)]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, stdout 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.Target=System.out 5 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 6 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n --------------------------------------------------------------------------------