From 21edcb41e10f0fe7ea1e9d08bf8058aa177e2ade Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:35:24 +0000 Subject: [PATCH 01/19] Create auto-merge-dependabot.yml --- .github/workflows/auto-merge-dependabot.yml | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/auto-merge-dependabot.yml diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml new file mode 100644 index 000000000..61313f87b --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml @@ -0,0 +1,31 @@ +name: Auto Merge Dependabot PRs + +on: + pull_request: + types: + - opened + - synchronize + - reopened + +jobs: + auto-merge-dependabot: + runs-on: ubuntu-latest + + if: github.actor == 'dependabot[bot]' + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Auto-approve Dependabot PRs + if: success() + uses: hmarr/auto-approve-action@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-merge Dependabot PRs + if: success() + uses: "peter-evans/enable-pull-request-automerge@v3" + with: + pull-request-number: ${{ github.event.pull_request.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} From ed436bb288b2ed2ff5fe4b030bf2aca13b232ae7 Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:40:34 +0000 Subject: [PATCH 02/19] Update auto-merge-dependabot.yml --- .github/workflows/auto-merge-dependabot.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 61313f87b..0311d8006 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -1,7 +1,7 @@ name: Auto Merge Dependabot PRs on: - pull_request: + pull_request_target: types: - opened - synchronize @@ -17,6 +17,11 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Set up Node.js (required by `github-script`) + uses: actions/setup-node@v3 + with: + node-version: '16' + - name: Auto-approve Dependabot PRs if: success() uses: hmarr/auto-approve-action@v3 From 4d122bfc182f61fba5e06c5d25edb4900c6673e4 Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:44:09 +0000 Subject: [PATCH 03/19] Merge PRs manually --- .github/workflows/auto-merge-dependabot.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 0311d8006..0fcc69a6f 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -28,9 +28,14 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Auto-merge Dependabot PRs + - name: Install GitHub CLI + run: | + sudo apt-get update + sudo apt-get install -y gh + + - name: Manually merge Dependabot PR if: success() - uses: "peter-evans/enable-pull-request-automerge@v3" - with: - pull-request-number: ${{ github.event.pull_request.number }} - github-token: ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr merge ${{ github.event.pull_request.number }} --merge --repo "${{ github.repository }}" From 68db559cb52fde208eca6d9bdbb96776fa083749 Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:46:19 +0000 Subject: [PATCH 04/19] Force merge dependabot PRs --- .github/workflows/auto-merge-dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 0fcc69a6f..16d7e7533 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -38,4 +38,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh pr merge ${{ github.event.pull_request.number }} --merge --repo "${{ github.repository }}" + gh pr merge ${{ github.event.pull_request.number }} --merge --admin --repo "${{ github.repository }}" From 7361633b0f93a78a9ac0b8439fa5608b6a3dae0a Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:52:36 +0000 Subject: [PATCH 05/19] Use auto merge --- .github/workflows/auto-merge-dependabot.yml | 50 +++++++-------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 16d7e7533..44bd7df43 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -1,41 +1,23 @@ -name: Auto Merge Dependabot PRs +name: Dependabot auto-merge +on: pull_request -on: - pull_request_target: - types: - - opened - - synchronize - - reopened +permissions: + contents: write + pull-requests: write jobs: - auto-merge-dependabot: + dependabot: runs-on: ubuntu-latest - - if: github.actor == 'dependabot[bot]' - + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'owner/my_repo' steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js (required by `github-script`) - uses: actions/setup-node@v3 + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d with: - node-version: '16' - - - name: Auto-approve Dependabot PRs - if: success() - uses: hmarr/auto-approve-action@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install GitHub CLI - run: | - sudo apt-get update - sudo apt-get install -y gh - - - name: Manually merge Dependabot PR - if: success() + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Enable auto-merge for Dependabot PRs + if: contains(steps.metadata.outputs.dependency-names, 'my-dependency') && steps.metadata.outputs.update-type == 'version-update:semver-patch' + run: gh pr merge --auto --merge "$PR_URL" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr merge ${{ github.event.pull_request.number }} --merge --admin --repo "${{ github.repository }}" + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} From b243f8b461dfc1a4d1608dc2c27abfb1a489982e Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:55:19 +0000 Subject: [PATCH 06/19] Fix repo owner on automerge --- .github/workflows/auto-merge-dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 44bd7df43..b277a8dd1 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -8,7 +8,7 @@ permissions: jobs: dependabot: runs-on: ubuntu-latest - if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'owner/my_repo' + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'OpenFeign/feign' steps: - name: Dependabot metadata id: metadata From 98faed0cafc57dbc3c0e6700db1d7e40035a5966 Mon Sep 17 00:00:00 2001 From: Marvin Date: Wed, 30 Oct 2024 16:59:27 +0000 Subject: [PATCH 07/19] Use dependabot/fetch-metadata@v2.2.0 --- .github/workflows/auto-merge-dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index b277a8dd1..09481d410 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d + uses: dependabot/fetch-metadata@v2.2.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs From 056569c022723323aa6e811bf841052039838a7e Mon Sep 17 00:00:00 2001 From: Marvin Date: Thu, 31 Oct 2024 15:05:40 +0000 Subject: [PATCH 08/19] Update auto-merge-dependabot.yml --- .github/workflows/auto-merge-dependabot.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 09481d410..82ba47359 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -16,7 +16,6 @@ jobs: with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs - if: contains(steps.metadata.outputs.dependency-names, 'my-dependency') && steps.metadata.outputs.update-type == 'version-update:semver-patch' run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} From 225c4340e347cb3472de9a99eccd77219937ae93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:06:41 +0000 Subject: [PATCH 09/19] Bump org.openrewrite.recipe:rewrite-migrate-java from 2.27.1 to 2.28.0 Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 2.27.1 to 2.28.0. - [Release notes](https://github.com/openrewrite/rewrite-migrate-java/releases) - [Commits](https://github.com/openrewrite/rewrite-migrate-java/compare/v2.27.1...v2.28.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-migrate-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3770bfe3c..f036d388b 100644 --- a/pom.xml +++ b/pom.xml @@ -996,7 +996,7 @@ org.openrewrite.recipe rewrite-migrate-java - 2.27.1 + 2.28.0 From 4f3adb67e612907bd234621fb95190d47f710f63 Mon Sep 17 00:00:00 2001 From: Marvin Date: Thu, 31 Oct 2024 17:02:48 +0000 Subject: [PATCH 10/19] Auto approve dependabot PRs --- .github/workflows/auto-merge-dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 82ba47359..c434f8c53 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -15,6 +15,12 @@ jobs: uses: dependabot/fetch-metadata@v2.2.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-approve Dependabot PRs + uses: hmarr/auto-approve-action@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --merge "$PR_URL" env: From 343e02a540280cda135597de8858c673f1891f8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:03:42 +0000 Subject: [PATCH 11/19] Bump org.openrewrite.maven:rewrite-maven-plugin from 5.42.2 to 5.43.3 Bumps [org.openrewrite.maven:rewrite-maven-plugin](https://github.com/openrewrite/rewrite-maven-plugin) from 5.42.2 to 5.43.3. - [Release notes](https://github.com/openrewrite/rewrite-maven-plugin/releases) - [Commits](https://github.com/openrewrite/rewrite-maven-plugin/compare/v5.42.2...v5.43.3) --- updated-dependencies: - dependency-name: org.openrewrite.maven:rewrite-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f036d388b..55902fc11 100644 --- a/pom.xml +++ b/pom.xml @@ -985,7 +985,7 @@ org.openrewrite.maven rewrite-maven-plugin - 5.42.2 + 5.43.3 From 7ecfc546e3cb36f4a18d86a24a6e064dec073efd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:11:42 +0000 Subject: [PATCH 12/19] Bump org.springframework.boot:spring-boot-starter-web Bumps [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) from 3.3.4 to 3.3.5. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- feign-form-spring/pom.xml | 4 ++-- feign-form/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/feign-form-spring/pom.xml b/feign-form-spring/pom.xml index 83de408c3..6093a9ad4 100644 --- a/feign-form-spring/pom.xml +++ b/feign-form-spring/pom.xml @@ -71,7 +71,7 @@ org.springframework.boot spring-boot-starter-web - 3.3.4 + 3.3.5 test @@ -100,7 +100,7 @@ org.springframework.boot spring-boot-starter-web - 3.3.4 + 3.3.5 test diff --git a/feign-form/pom.xml b/feign-form/pom.xml index 4cc452f17..d202167ef 100644 --- a/feign-form/pom.xml +++ b/feign-form/pom.xml @@ -56,7 +56,7 @@ org.springframework.boot spring-boot-starter-web - 3.3.4 + 3.3.5 test From dcf2f890762ceb2b5b2509ec22952081619d53fc Mon Sep 17 00:00:00 2001 From: Marvin Date: Thu, 31 Oct 2024 17:25:22 +0000 Subject: [PATCH 13/19] Update dependabot.yml --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8bffe2fee..8bccb05c2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,5 +8,5 @@ updates: - package-ecosystem: "maven" directory: "/" schedule: - interval: "weekly" + interval: "daily" open-pull-requests-limit: 100 From c32cc1d945fd03c78b1cb73a03382973a193fb82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:26:11 +0000 Subject: [PATCH 14/19] Bump jackson.version from 2.18.0 to 2.18.1 Bumps `jackson.version` from 2.18.0 to 2.18.1. Updates `com.fasterxml.jackson.core:jackson-databind` from 2.18.0 to 2.18.1 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.0 to 2.18.1 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.0...jackson-core-2.18.1) Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.18.0 to 2.18.1 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.jr:jackson-jr-objects` from 2.18.0 to 2.18.1 - [Commits](https://github.com/FasterXML/jackson-jr/compare/jackson-jr-parent-2.18.0...jackson-jr-parent-2.18.1) Updates `com.fasterxml.jackson.jr:jackson-jr-annotation-support` from 2.18.0 to 2.18.1 - [Commits](https://github.com/FasterXML/jackson-jr/compare/jackson-jr-parent-2.18.0...jackson-jr-parent-2.18.1) Updates `com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider` from 2.18.0 to 2.18.1 --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.jr:jackson-jr-objects dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.jr:jackson-jr-annotation-support dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 55902fc11..0a24bd6ff 100644 --- a/pom.xml +++ b/pom.xml @@ -172,7 +172,7 @@ 20240303 5.11.2 - 2.18.0 + 2.18.1 3.26.3 5.14.2 2.0.53 From 8a73c27568aa6ebfe5a4d390b6a6aa474f31dc44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:36:50 +0000 Subject: [PATCH 15/19] Bump org.apache.httpcomponents.client5:httpclient5 from 5.4 to 5.4.1 Bumps [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client) from 5.4 to 5.4.1. - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.4.1/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.4...rel/v5.4.1) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- hc5/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hc5/pom.xml b/hc5/pom.xml index b22f4c9e2..c90e95009 100644 --- a/hc5/pom.xml +++ b/hc5/pom.xml @@ -38,7 +38,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.4 + 5.4.1 From 13b06769afe13e1607e53eaac1f1a9243dcf7a56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:46:06 +0000 Subject: [PATCH 16/19] Bump org.openrewrite.recipe:rewrite-testing-frameworks Bumps [org.openrewrite.recipe:rewrite-testing-frameworks](https://github.com/openrewrite/rewrite-testing-frameworks) from 2.20.1 to 2.21.0. - [Release notes](https://github.com/openrewrite/rewrite-testing-frameworks/releases) - [Commits](https://github.com/openrewrite/rewrite-testing-frameworks/compare/v2.20.1...v2.21.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-testing-frameworks dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0a24bd6ff..f925ad379 100644 --- a/pom.xml +++ b/pom.xml @@ -991,7 +991,7 @@ org.openrewrite.recipe rewrite-testing-frameworks - 2.20.1 + 2.21.0 org.openrewrite.recipe From 8a67b471abca045e6af49509b99d2f8bb37422ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:12:14 +0000 Subject: [PATCH 17/19] Bump org.springframework.boot:spring-boot-starter-test Bumps [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) from 3.3.4 to 3.3.5. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- feign-form-spring/pom.xml | 2 +- feign-form/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/feign-form-spring/pom.xml b/feign-form-spring/pom.xml index 6093a9ad4..2a2606036 100644 --- a/feign-form-spring/pom.xml +++ b/feign-form-spring/pom.xml @@ -106,7 +106,7 @@ org.springframework.boot spring-boot-starter-test - 3.3.4 + 3.3.5 test diff --git a/feign-form/pom.xml b/feign-form/pom.xml index d202167ef..7cd363aa2 100644 --- a/feign-form/pom.xml +++ b/feign-form/pom.xml @@ -62,7 +62,7 @@ org.springframework.boot spring-boot-starter-test - 3.3.4 + 3.3.5 test From 55b7b1ca86e29c6c710c5bb30664a0b255d83996 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:19:15 +0000 Subject: [PATCH 18/19] Bump org.junit:junit-bom from 5.11.2 to 5.11.3 Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.2 to 5.11.3. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.2...r5.11.3) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f925ad379..2574aa33f 100644 --- a/pom.xml +++ b/pom.xml @@ -171,7 +171,7 @@ 2.0.16 20240303 - 5.11.2 + 5.11.3 2.18.1 3.26.3 5.14.2 From 02764c67eea079b39a6ddf4f995523b38167e172 Mon Sep 17 00:00:00 2001 From: Alexei KLENIN Date: Wed, 30 Oct 2024 21:11:46 +0100 Subject: [PATCH 19/19] Integrate Feign Vertx into the main project --- .../feign/RequestTemplateFactoryResolver.java | 12 +- pom.xml | 5 + vertx/README.md | 78 +++ vertx/pom.xml | 123 +++++ vertx/run-tests.zsh | 43 ++ vertx/src/main/java/feign/VertxFeign.java | 476 ++++++++++++++++++ .../java/feign/VertxInvocationHandler.java | 120 +++++ .../main/java/feign/VertxMethodHandler.java | 314 ++++++++++++ vertx/src/main/java/feign/package-info.java | 22 + .../feign/vertx/VertxDelegatingContract.java | 63 +++ .../java/feign/vertx/VertxHttpClient.java | 148 ++++++ .../vertx/AbstractClientReconnectTest.java | 131 +++++ .../feign/vertx/AbstractFeignVertxTest.java | 44 ++ .../feign/vertx/ConnectionsLeakTests.java | 127 +++++ .../vertx/Http11ClientReconnectTest.java | 43 ++ .../feign/vertx/Http2ClientReconnectTest.java | 45 ++ .../java/feign/vertx/QueryMapEncoderTest.java | 117 +++++ .../java/feign/vertx/RawContractTest.java | 124 +++++ .../feign/vertx/RequestPreProcessorTest.java | 89 ++++ .../test/java/feign/vertx/RetryingTest.java | 140 ++++++ .../src/test/java/feign/vertx/TestUtils.java | 36 ++ .../java/feign/vertx/TimeoutHandlingTest.java | 122 +++++ .../java/feign/vertx/VertxHttpClientTest.java | 325 ++++++++++++ .../feign/vertx/VertxHttpOptionsTest.java | 109 ++++ .../feign/vertx/testcase/HelloServiceAPI.java | 33 ++ .../vertx/testcase/IcecreamServiceApi.java | 52 ++ .../testcase/IcecreamServiceApiBroken.java | 50 ++ .../feign/vertx/testcase/RawServiceAPI.java | 38 ++ .../feign/vertx/testcase/domain/Bill.java | 82 +++ .../feign/vertx/testcase/domain/Flavor.java | 38 ++ .../vertx/testcase/domain/IceCreamOrder.java | 111 ++++ .../feign/vertx/testcase/domain/Mixin.java | 38 ++ .../vertx/testcase/domain/OrderGenerator.java | 59 +++ vertx/src/test/resources/log4j.properties | 6 + 34 files changed, 3357 insertions(+), 6 deletions(-) create mode 100644 vertx/README.md create mode 100644 vertx/pom.xml create mode 100755 vertx/run-tests.zsh create mode 100644 vertx/src/main/java/feign/VertxFeign.java create mode 100644 vertx/src/main/java/feign/VertxInvocationHandler.java create mode 100644 vertx/src/main/java/feign/VertxMethodHandler.java create mode 100644 vertx/src/main/java/feign/package-info.java create mode 100644 vertx/src/main/java/feign/vertx/VertxDelegatingContract.java create mode 100644 vertx/src/main/java/feign/vertx/VertxHttpClient.java create mode 100644 vertx/src/test/java/feign/vertx/AbstractClientReconnectTest.java create mode 100644 vertx/src/test/java/feign/vertx/AbstractFeignVertxTest.java create mode 100644 vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java create mode 100644 vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java create mode 100644 vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java create mode 100644 vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java create mode 100644 vertx/src/test/java/feign/vertx/RawContractTest.java create mode 100644 vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java create mode 100644 vertx/src/test/java/feign/vertx/RetryingTest.java create mode 100644 vertx/src/test/java/feign/vertx/TestUtils.java create mode 100644 vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java create mode 100644 vertx/src/test/java/feign/vertx/VertxHttpClientTest.java create mode 100644 vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/HelloServiceAPI.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApi.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApiBroken.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/RawServiceAPI.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/domain/Bill.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/domain/Flavor.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/domain/IceCreamOrder.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/domain/Mixin.java create mode 100644 vertx/src/test/java/feign/vertx/testcase/domain/OrderGenerator.java create mode 100644 vertx/src/test/resources/log4j.properties diff --git a/core/src/main/java/feign/RequestTemplateFactoryResolver.java b/core/src/main/java/feign/RequestTemplateFactoryResolver.java index e39160b18..0d64fd743 100644 --- a/core/src/main/java/feign/RequestTemplateFactoryResolver.java +++ b/core/src/main/java/feign/RequestTemplateFactoryResolver.java @@ -47,7 +47,7 @@ public RequestTemplate.Factory resolve(Target target, MethodMetadata md) { } } - private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { + static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { private final QueryMapEncoder queryMapEncoder; @@ -56,7 +56,7 @@ private static class BuildTemplateByResolvingArgs implements RequestTemplate.Fac private final Map indexToExpander = new LinkedHashMap(); - private BuildTemplateByResolvingArgs( + BuildTemplateByResolvingArgs( MethodMetadata metadata, QueryMapEncoder queryMapEncoder, Target target) { this.metadata = metadata; this.target = target; @@ -212,11 +212,11 @@ protected RequestTemplate resolve( } } - private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { + static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { private final Encoder encoder; - private BuildFormEncodedTemplateFromArgs( + BuildFormEncodedTemplateFromArgs( MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder, Target target) { super(metadata, queryMapEncoder, target); this.encoder = encoder; @@ -242,11 +242,11 @@ protected RequestTemplate resolve( } } - private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { + static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { private final Encoder encoder; - private BuildEncodedTemplateFromArgs( + BuildEncodedTemplateFromArgs( MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder, Target target) { super(metadata, queryMapEncoder, target); this.encoder = encoder; diff --git a/pom.xml b/pom.xml index 2574aa33f..3c970a220 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,10 @@ Guillaume Simard + + Alexei KLENIN + alexei.klenin@gmail.com + @@ -122,6 +126,7 @@ fastjson2 feign-form feign-form-spring + vertx diff --git a/vertx/README.md b/vertx/README.md new file mode 100644 index 000000000..b077fe001 --- /dev/null +++ b/vertx/README.md @@ -0,0 +1,78 @@ +# Feign Vertx + +Implementation of Feign on Vertx. Brings you the best of two worlds together : +concise syntax of Feign to write client side API on fast, asynchronous and +non-blocking HTTP client of Vertx. + +## Installation + +### With Maven + +```xml + + ... + + io.github.openfeign + feign-vertx + 14.0 + + ... + +``` + +### With Gradle + +```groovy +compile group: 'io.github.openfeign', name: 'feign-vertx', version: '14.0' +``` + +## Compatibility + +Feign | Vertx +---------------------- | ---------------------- +14.x | 4.x + +## Usage + +Write Feign API as usual, but every method of interface must return +`io.vertx.core.Future`. + +```java +@Headers({ "Accept: application/json" }) +interface IcecreamServiceApi { + + @RequestLine("GET /icecream/flavors") + Future> getAvailableFlavors(); + + @RequestLine("GET /icecream/mixins") + Future> getAvailableMixins(); + + @RequestLine("POST /icecream/orders") + @Headers("Content-Type: application/json") + Future makeOrder(IceCreamOrder order); + + @RequestLine("GET /icecream/orders/{orderId}") + Future findOrder(@Param("orderId") int orderId); + + @RequestLine("POST /icecream/bills/pay") + @Headers("Content-Type: application/json") + Future payBill(Bill bill); +} +``` +Build the client : + +```java +Vertx vertx = Vertx.vertx(); // get Vertx instance + +/* Create instance of your API */ +IcecreamServiceApi icecreamApi = VertxFeign + .builder() + .vertx(vertx) // provide vertx instance + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .target(IcecreamServiceApi.class, "http://www.icecreame.com"); + +/* Execute requests asynchronously */ +Future> flavorsFuture = icecreamApi.getAvailableFlavors(); +Future> mixinsFuture = icecreamApi.getAvailableMixins(); +``` diff --git a/vertx/pom.xml b/vertx/pom.xml new file mode 100644 index 000000000..22985f296 --- /dev/null +++ b/vertx/pom.xml @@ -0,0 +1,123 @@ + + + + 4.0.0 + + io.github.openfeign + parent + 13.6-SNAPSHOT + + + feign-vertx + + Feign Vertx + Implementation of Feign on Vertx web client. + + + 4.5.10 + 2.12.0 + 1.8.0-beta0 + 2.35.1 + + + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + + + + io.github.openfeign + feign-core + + + + + io.vertx + vertx-core + ${vertx.version} + provided + + + + + io.vertx + vertx-junit5 + ${vertx.version} + test + + + + org.assertj + assertj-core + test + + + + io.github.openfeign + feign-jackson + test + + + + com.fasterxml.jackson.core + jackson-annotations + test + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + test + + + + io.github.openfeign + feign-slf4j + test + + + + org.slf4j + slf4j-log4j12 + ${slf4j-log4j12.version} + test + + + + com.github.tomakehurst + wiremock-jre8 + ${wiremock.version} + test + + + org.junit + junit-bom + + + + + diff --git a/vertx/run-tests.zsh b/vertx/run-tests.zsh new file mode 100755 index 000000000..409e90f05 --- /dev/null +++ b/vertx/run-tests.zsh @@ -0,0 +1,43 @@ +#!/usr/bin/env zsh + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +CHECK_CHAR='\U2713' +CROSS_CHAR='\U2717' + +function print_result() { + version=$1 + result=$2 + + if [[ $result == 0 ]]; + then + mark=$CHECK_CHAR + color=$GREEN + else + mark=$CROSS_CHAR + color=$RED + fi + + echo "\t${color}${version} ${mark}${NC}" +} + +declare -A vertx_versions +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" ) +v40x=( "4.0.2" ) +v41x=( "4.1.8" ) +v42x=( "4.2.7" ) +v43x=( "4.3.2" ) +v44x=( "4.4.9" ) +v45x=( "4.5.10" ) + +for version in ${(k)vertx_versions}; do + echo "Tests with Vertx ${vertx_versions[${version}]}:" + + for vertx_version in ${(P)version}; do + printf "\tRun tests with Vertx %s...\n" "${vertx_version}" + mvn clean compile test -Dvertx.version="$vertx_version" &> /dev/null + print_result "$vertx_version" $? + done +done diff --git a/vertx/src/main/java/feign/VertxFeign.java b/vertx/src/main/java/feign/VertxFeign.java new file mode 100644 index 000000000..df3e8706a --- /dev/null +++ b/vertx/src/main/java/feign/VertxFeign.java @@ -0,0 +1,476 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign; + +import static feign.Util.checkNotNull; +import static feign.Util.isDefault; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.querymap.FieldQueryMapEncoder; +import feign.vertx.VertxDelegatingContract; +import feign.vertx.VertxHttpClient; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +/** + * Allows Feign interfaces to return Vert.x {@link io.vertx.core.Future Future}s. + * + * @author Alexei KLENIN + * @author Gordon McKinney + */ +public final class VertxFeign extends Feign { + private final ParseHandlersByName targetToHandlersByName; + private final InvocationHandlerFactory factory; + + private VertxFeign( + final ParseHandlersByName targetToHandlersByName, final InvocationHandlerFactory factory) { + this.targetToHandlersByName = targetToHandlersByName; + this.factory = factory; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + @SuppressWarnings("unchecked") + public T newInstance(final Target target) { + checkNotNull(target, "Argument target must be not null"); + + final Map nameToHandler = targetToHandlersByName.apply(target); + final Map methodToHandler = new HashMap<>(); + final List defaultMethodHandlers = new ArrayList<>(); + + for (final Method method : target.type().getMethods()) { + if (isDefault(method)) { + final DefaultMethodHandler handler = new DefaultMethodHandler(method); + defaultMethodHandlers.add(handler); + methodToHandler.put(method, handler); + } else { + methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); + } + } + + final InvocationHandler handler = factory.create(target, methodToHandler); + final T proxy = + (T) + Proxy.newProxyInstance( + target.type().getClassLoader(), new Class[] {target.type()}, handler); + + for (final DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { + defaultMethodHandler.bindTo(proxy); + } + + return proxy; + } + + /** VertxFeign builder. */ + public static final class Builder extends Feign.Builder { + private Vertx vertx; + private final List requestInterceptors = new ArrayList<>(); + private Logger.Level logLevel = Logger.Level.NONE; + private Contract contract = new VertxDelegatingContract(new Contract.Default()); + private Retryer retryer = new Retryer.Default(); + private Logger logger = new Logger.NoOpLogger(); + private Encoder encoder = new Encoder.Default(); + private Decoder decoder = new Decoder.Default(); + private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder(); + private List capabilities = new ArrayList<>(); + private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); + private HttpClientOptions options = new HttpClientOptions(); + private long timeout = -1; + private boolean decode404; + private UnaryOperator requestPreProcessor = UnaryOperator.identity(); + + /** Unsupported operation. */ + @Override + public Builder client(final Client client) { + throw new UnsupportedOperationException(); + } + + /** Unsupported operation. */ + @Override + public Builder invocationHandlerFactory( + final InvocationHandlerFactory invocationHandlerFactory) { + throw new UnsupportedOperationException(); + } + + /** + * Sets a vertx instance to use to make the client. + * + * @param vertx vertx instance + * @return this builder + */ + public Builder vertx(final Vertx vertx) { + this.vertx = checkNotNull(vertx, "Argument vertx must be not null"); + return this; + } + + /** + * Sets log level. + * + * @param logLevel log level + * @return this builder + */ + @Override + public Builder logLevel(final Logger.Level logLevel) { + this.logLevel = checkNotNull(logLevel, "Argument logLevel must be not null"); + return this; + } + + /** + * Sets contract. Provided contract will be wrapped in {@link VertxDelegatingContract}. + * + * @param contract contract + * @return this builder + */ + @Override + public Builder contract(final Contract contract) { + checkNotNull(contract, "Argument contract must be not null"); + this.contract = new VertxDelegatingContract(contract); + return this; + } + + /** + * Sets retryer. + * + * @param retryer retryer + * @return this builder + */ + @Override + public Builder retryer(final Retryer retryer) { + this.retryer = checkNotNull(retryer, "Argument retryer must be not null"); + return this; + } + + /** + * Sets logger. + * + * @param logger logger + * @return this builder + */ + @Override + public Builder logger(final Logger logger) { + this.logger = checkNotNull(logger, "Argument logger must be not null"); + return this; + } + + /** + * Sets encoder. + * + * @param encoder encoder + * @return this builder + */ + @Override + public Builder encoder(final Encoder encoder) { + this.encoder = checkNotNull(encoder, "Argument encoder must be not null"); + return this; + } + + /** + * Sets decoder. + * + * @param decoder decoder + * @return this builder + */ + @Override + public Builder decoder(final Decoder decoder) { + this.decoder = checkNotNull(decoder, "Argument decoder must be not null"); + return this; + } + + /** + * Sets query map encoder. + * + * @param queryMapEncoder query map encoder + * @return this builder + */ + @Override + public Builder queryMapEncoder(final QueryMapEncoder queryMapEncoder) { + this.queryMapEncoder = + checkNotNull(queryMapEncoder, "Argument queryMapEncoder must be not null"); + return this; + } + + /** + * Adds a single capability to the builder. + * + * @param capability capability + * @return this builder + */ + @Override + public Builder addCapability(Capability capability) { + checkNotNull(capability, "Argument capability must be not null"); + this.capabilities.add(capability); + return this; + } + + /** + * This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with + * 404 status, specifically returning null or empty instead of throwing {@link FeignException}. + * + *

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

This flag only works with 404, as opposed to all or arbitrary status codes. This was an + * explicit decision: 404 - empty is safe, common and doesn't complicate redirection, retry or + * fallback policy. + * + * @return this builder + */ + @Override + public Builder decode404() { + this.decode404 = true; + return this; + } + + /** + * Sets error decoder. + * + * @param errorDecoder error deoceder + * @return this builder + */ + @Override + public Builder errorDecoder(final ErrorDecoder errorDecoder) { + this.errorDecoder = checkNotNull(errorDecoder, "Argument errorDecoder must be not null"); + return this; + } + + /** + * Sets request options using Vert.x {@link HttpClientOptions}. + * + * @param options {@code HttpClientOptions} for full customization of the underlying Vert.x + * {@link HttpClient} + * @return this builder + */ + public Builder options(final HttpClientOptions options) { + this.options = checkNotNull(options, "Argument options must be not null"); + return this; + } + + /** + * Sets request options using Feign {@link Request.Options}. + * + * @param options Feign {@code Request.Options} object + * @return this builder + */ + @Override + public Builder options(final Request.Options options) { + checkNotNull(options, "Argument options must be not null"); + this.options = + new HttpClientOptions() + .setConnectTimeout(options.connectTimeoutMillis()) + .setIdleTimeout(options.readTimeoutMillis()); + return this; + } + + /** + * Configures the amount of time in milliseconds after which if the request does not return any + * data within the timeout period an {@link java.util.concurrent.TimeoutException} fails the + * request. + * + *

Setting zero or a negative {@code value} disables the timeout. + * + * @param timeout The quantity of time in milliseconds. + * @return this builder + */ + public Builder timeout(long timeout) { + this.timeout = timeout; + return this; + } + + /** + * Defines operation to execute on each {@link HttpClientRequest} before it is sent. Used to + * make setup on request level. + * + *

Example: + * + *

+     * var client = VertxFeign
+     *     .builder()
+     *     .vertx(vertx)
+     *     .requestPreProcessor(req -> req.putHeader("version", "v1"));
+     * 
+ * + * @param requestPreProcessor operation to execute on each request + * @return updated request + */ + public Builder requestPreProcessor(UnaryOperator requestPreProcessor) { + this.requestPreProcessor = + checkNotNull(requestPreProcessor, "Argument requestPreProcessor must be not null"); + return this; + } + + /** + * Adds a single request interceptor to the builder. + * + * @param requestInterceptor request interceptor to add + * @return this builder + */ + @Override + public Builder requestInterceptor(final RequestInterceptor requestInterceptor) { + checkNotNull(requestInterceptor, "Argument requestInterceptor must be not null"); + this.requestInterceptors.add(requestInterceptor); + return this; + } + + /** + * Sets the full set of request interceptors for the builder, overwriting any previous + * interceptors. + * + * @param requestInterceptors set of request interceptors + * @return this builder + */ + @Override + public Builder requestInterceptors(final Iterable requestInterceptors) { + checkNotNull(requestInterceptors, "Argument requestInterceptors must be not null"); + + this.requestInterceptors.clear(); + + for (final RequestInterceptor requestInterceptor : requestInterceptors) { + this.requestInterceptors.add(requestInterceptor); + } + + return this; + } + + /** + * Defines target and builds client. + * + * @param apiType API interface + * @param url base URL + * @param class of API interface + * @return built client + */ + @Override + public T target(final Class apiType, final String url) { + checkNotNull(apiType, "Argument apiType must be not null"); + checkNotNull(url, "Argument url must be not null"); + + return target(new Target.HardCodedTarget<>(apiType, url)); + } + + /** + * Defines target and builds client. + * + * @param target target instance + * @param class of API interface + * @return built client + */ + @Override + public T target(final Target target) { + return build().newInstance(target); + } + + @Override + public VertxFeign internalBuild() { + checkNotNull(this.vertx, "Vertx instance wasn't provided in VertxFeign builder"); + + final VertxHttpClient client = + new VertxHttpClient(vertx, options, timeout, requestPreProcessor); + final VertxMethodHandler.Factory methodHandlerFactory = + new VertxMethodHandler.Factory( + client, retryer, requestInterceptors, logger, logLevel, decode404); + final ParseHandlersByName handlersByName = + new ParseHandlersByName( + contract, + options, + encoder, + decoder, + queryMapEncoder, + capabilities, + errorDecoder, + methodHandlerFactory); + final InvocationHandlerFactory invocationHandlerFactory = + new VertxInvocationHandler.Factory(); + + return new VertxFeign(handlersByName, invocationHandlerFactory); + } + } + + private static final class ParseHandlersByName { + private final Contract contract; + private final HttpClientOptions options; + private final Encoder encoder; + private final Decoder decoder; + private final QueryMapEncoder queryMapEncoder; + private final List capabilities; + private final ErrorDecoder errorDecoder; + private final VertxMethodHandler.Factory factory; + + private ParseHandlersByName( + final Contract contract, + final HttpClientOptions options, + final Encoder encoder, + final Decoder decoder, + final QueryMapEncoder queryMapEncoder, + final List capabilities, + final ErrorDecoder errorDecoder, + final VertxMethodHandler.Factory factory) { + this.contract = contract; + this.options = options; + this.factory = factory; + this.encoder = encoder; + this.decoder = decoder; + this.queryMapEncoder = queryMapEncoder; + this.capabilities = capabilities; + this.errorDecoder = errorDecoder; + } + + private Map apply(final Target target) { + final List metadata = contract.parseAndValidateMetadata(target.type()); + final Map result = new HashMap<>(); + + for (final MethodMetadata metadatum : metadata) { + RequestTemplateFactoryResolver.BuildTemplateByResolvingArgs buildTemplate; + + if (!metadatum.formParams().isEmpty() && metadatum.template().bodyTemplate() == null) { + buildTemplate = + new RequestTemplateFactoryResolver.BuildFormEncodedTemplateFromArgs( + metadatum, encoder, queryMapEncoder, target); + } else if (metadatum.bodyIndex() != null || metadatum.alwaysEncodeBody()) { + buildTemplate = + new RequestTemplateFactoryResolver.BuildEncodedTemplateFromArgs( + metadatum, encoder, queryMapEncoder, target); + } else { + buildTemplate = + new RequestTemplateFactoryResolver.BuildTemplateByResolvingArgs( + metadatum, queryMapEncoder, target); + } + + result.put( + metadatum.configKey(), + factory.create(target, metadatum, buildTemplate, decoder, errorDecoder)); + } + + return result; + } + } +} diff --git a/vertx/src/main/java/feign/VertxInvocationHandler.java b/vertx/src/main/java/feign/VertxInvocationHandler.java new file mode 100644 index 000000000..1611c491e --- /dev/null +++ b/vertx/src/main/java/feign/VertxInvocationHandler.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.vertx.VertxHttpClient; +import io.vertx.core.Future; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Map; + +/** + * {@link InvocationHandler} implementation that transforms calls to methods of feign contract into + * asynchronous HTTP requests via vertx. + * + * @author Alexei KLENIN + */ +final class VertxInvocationHandler implements InvocationHandler { + private final Target target; + private final Map dispatch; + + private VertxInvocationHandler( + final Target target, final Map dispatch) { + this.target = target; + this.dispatch = dispatch; + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) { + switch (method.getName()) { + case "equals": + final Object otherHandler = + args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; + return equals(otherHandler); + case "hashCode": + return hashCode(); + case "toString": + return toString(); + default: + if (isReturnsFuture(method)) { + return invokeRequestMethod(method, args); + } else { + final String message = + String.format( + "Method %s of contract %s doesn't return io.vertx.core.Future", + method.getName(), method.getDeclaringClass().getSimpleName()); + throw new FeignException(-1, message); + } + } + } + + /** + * Transforms method invocation into request that executed by {@link VertxHttpClient}. + * + * @param method invoked method + * @param args provided arguments to method + * @return future with decoded result or occurred exception + */ + private Future invokeRequestMethod(final Method method, final Object[] args) { + try { + return (Future) dispatch.get(method).invoke(args); + } catch (Throwable throwable) { + return Future.failedFuture(throwable); + } + } + + /** + * Checks if method must return vertx {@code Future}. + * + * @param method invoked method + * @return true if method must return Future, false if not + */ + private boolean isReturnsFuture(final Method method) { + return Future.class.isAssignableFrom(method.getReturnType()); + } + + @Override + public boolean equals(final Object other) { + if (other instanceof VertxInvocationHandler) { + final VertxInvocationHandler otherHandler = (VertxInvocationHandler) other; + return this.target.equals(otherHandler.target); + } + + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } + + /** Factory for VertxInvocationHandler. */ + static final class Factory implements InvocationHandlerFactory { + + @Override + public InvocationHandler create( + final Target target, final Map dispatch) { + return new VertxInvocationHandler(target, dispatch); + } + } +} diff --git a/vertx/src/main/java/feign/VertxMethodHandler.java b/vertx/src/main/java/feign/VertxMethodHandler.java new file mode 100644 index 000000000..1e9975cf4 --- /dev/null +++ b/vertx/src/main/java/feign/VertxMethodHandler.java @@ -0,0 +1,314 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign; + +import static feign.FeignException.errorExecuting; +import static feign.FeignException.errorReading; +import static feign.Util.ensureClosed; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.codec.DecodeException; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; +import feign.vertx.VertxHttpClient; +import io.vertx.core.Future; +import io.vertx.core.VertxException; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +/** + * Method handler for asynchronous HTTP requests via {@link VertxHttpClient}. Inspired by {@link + * SynchronousMethodHandler}. + * + * @author Alexei KLENIN + * @author Gordon McKinney + */ +final class VertxMethodHandler implements MethodHandler { + private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L; + + private final MethodMetadata metadata; + private final Target target; + private final VertxHttpClient client; + private final Retryer retryer; + private final List requestInterceptors; + private final Logger logger; + private final Logger.Level logLevel; + private final RequestTemplate.Factory buildTemplateFromArgs; + private final Decoder decoder; + private final ErrorDecoder errorDecoder; + private final boolean decode404; + + private VertxMethodHandler( + final Target target, + final VertxHttpClient client, + final Retryer retryer, + final List requestInterceptors, + final Logger logger, + final Logger.Level logLevel, + final MethodMetadata metadata, + final RequestTemplate.Factory buildTemplateFromArgs, + final Decoder decoder, + final ErrorDecoder errorDecoder, + final boolean decode404) { + this.target = target; + this.client = client; + this.retryer = retryer; + this.requestInterceptors = requestInterceptors; + this.logger = logger; + this.logLevel = logLevel; + this.metadata = metadata; + this.buildTemplateFromArgs = buildTemplateFromArgs; + this.errorDecoder = errorDecoder; + this.decoder = decoder; + this.decode404 = decode404; + } + + @Override + @SuppressWarnings("unchecked") + public Future invoke(final Object[] argv) { + final RequestTemplate template = buildTemplateFromArgs.create(argv); + final Retryer retryer = this.retryer.clone(); + + final RetryRecoverer recoverer = new RetryRecoverer<>(template, retryer); + return executeAndDecode(template).recover(recoverer); + } + + /** + * Executes request from {@code template} with {@code this.client} and decodes the response. + * Result or occurred error wrapped in returned Future. + * + * @param template request template + * @return future with decoded result or occurred error + */ + private Future executeAndDecode(final RequestTemplate template) { + final Request request = targetRequest(template); + + logRequest(request); + + final Instant start = Instant.now(); + + return client + .execute(request) + .compose( + response -> { + final long elapsedTime = Duration.between(start, Instant.now()).toMillis(); + boolean shouldClose = true; + + try { + // TODO: check why this buffering is needed + if (logLevel != Logger.Level.NONE) { + response = + logger.logAndRebufferResponse( + metadata.configKey(), logLevel, response, elapsedTime); + } + + if (Response.class == metadata.returnType()) { + if (response.body() == null) { + return Future.succeededFuture(response); + } else if (response.body().length() == null + || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { + shouldClose = false; + return Future.succeededFuture(response); + } else { + return Future.succeededFuture( + Response.builder() + .status(response.status()) + .reason(response.reason()) + .headers(response.headers()) + .request(response.request()) + .body(response.body()) + .build()); + } + } else if (response.status() >= 200 && response.status() < 300) { + if (Void.class == metadata.returnType()) { + return Future.succeededFuture(); + } else { + return Future.succeededFuture(decode(response, request)); + } + } else if (decode404 && response.status() == 404) { + return Future.succeededFuture(decoder.decode(response, metadata.returnType())); + } else { + return Future.failedFuture(errorDecoder.decode(metadata.configKey(), response)); + } + } catch (final IOException ioException) { + logIoException(ioException, elapsedTime); + return Future.failedFuture(errorReading(request, response, ioException)); + } catch (FeignException exception) { + return Future.failedFuture(exception); + } finally { + if (shouldClose) { + ensureClosed(response.body()); + } + } + }, + failure -> { + if (failure instanceof VertxException || failure instanceof TimeoutException) { + return Future.failedFuture(failure); + } else if (failure.getCause() instanceof IOException) { + final long elapsedTime = Duration.between(start, Instant.now()).toMillis(); + logIoException((IOException) failure.getCause(), elapsedTime); + return Future.failedFuture( + errorExecuting(request, (IOException) failure.getCause())); + } else { + return Future.failedFuture(failure.getCause()); + } + }); + } + + /** + * Associates request to defined target. + * + * @param template request template + * @return fully formed request + */ + private Request targetRequest(final RequestTemplate template) { + for (final RequestInterceptor interceptor : requestInterceptors) { + interceptor.apply(template); + } + + return target.apply(template); + } + + /** + * Transforms HTTP response body into object using decoder. + * + * @param response HTTP response + * @param request HTTP request + * @return decoded result + * @throws IOException IO exception during the reading of InputStream of response + * @throws DecodeException when decoding failed due to a checked or unchecked exception besides + * IOException + * @throws FeignException when decoding succeeds, but conveys the operation failed + */ + private Object decode(final Response response, final Request request) + throws IOException, FeignException { + try { + return decoder.decode(response, metadata.returnType()); + } catch (final FeignException feignException) { + /* All feign exception including decode exceptions */ + throw feignException; + } catch (final RuntimeException unexpectedException) { + /* Any unexpected exception */ + throw new DecodeException(-1, unexpectedException.getMessage(), request, unexpectedException); + } + } + + /** + * Logs request. + * + * @param request HTTP request + */ + private void logRequest(final Request request) { + if (logLevel != Logger.Level.NONE) { + logger.logRequest(metadata.configKey(), logLevel, request); + } + } + + /** + * Logs IO exception. + * + * @param exception IO exception + * @param elapsedTime time spent to execute request + */ + private void logIoException(final IOException exception, final long elapsedTime) { + if (logLevel != Logger.Level.NONE) { + logger.logIOException(metadata.configKey(), logLevel, exception, elapsedTime); + } + } + + /** Logs retry. */ + private void logRetry() { + if (logLevel != Logger.Level.NONE) { + logger.logRetry(metadata.configKey(), logLevel); + } + } + + static final class Factory { + private final VertxHttpClient client; + private final Retryer retryer; + private final List requestInterceptors; + private final Logger logger; + private final Logger.Level logLevel; + private final boolean decode404; + + Factory( + final VertxHttpClient client, + final Retryer retryer, + final List requestInterceptors, + final Logger logger, + final Logger.Level logLevel, + final boolean decode404) { + this.client = client; + this.retryer = retryer; + this.requestInterceptors = requestInterceptors; + this.logger = logger; + this.logLevel = logLevel; + this.decode404 = decode404; + } + + MethodHandler create( + final Target target, + final MethodMetadata metadata, + final RequestTemplate.Factory buildTemplateFromArgs, + final Decoder decoder, + final ErrorDecoder errorDecoder) { + return new VertxMethodHandler( + target, + client, + retryer, + requestInterceptors, + logger, + logLevel, + metadata, + buildTemplateFromArgs, + decoder, + errorDecoder, + decode404); + } + } + + /** + * Handler for failures able to retry execution of request. In this case handler passed to new + * request. + * + * @param type of response + */ + private final class RetryRecoverer implements Function> { + private final RequestTemplate template; + private final Retryer retryer; + + private RetryRecoverer(final RequestTemplate template, final Retryer retryer) { + this.template = template; + this.retryer = retryer; + } + + @Override + @SuppressWarnings("unchecked") + public Future apply(final Throwable throwable) { + if (throwable instanceof RetryableException) { + this.retryer.continueOrPropagate((RetryableException) throwable); + logRetry(); + return ((Future) executeAndDecode(this.template)).recover(this); + } else { + return Future.failedFuture(throwable); + } + } + } +} diff --git a/vertx/src/main/java/feign/package-info.java b/vertx/src/main/java/feign/package-info.java new file mode 100644 index 000000000..77ef0ae74 --- /dev/null +++ b/vertx/src/main/java/feign/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Package for extensions that must be able use package-private classes of {@code feign}. + * + * @author Alexei KLENIN + */ +package feign; diff --git a/vertx/src/main/java/feign/vertx/VertxDelegatingContract.java b/vertx/src/main/java/feign/vertx/VertxDelegatingContract.java new file mode 100644 index 000000000..07799acdb --- /dev/null +++ b/vertx/src/main/java/feign/vertx/VertxDelegatingContract.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static feign.Types.resolveLastTypeParameter; +import static feign.Util.checkNotNull; + +import feign.Contract; +import feign.MethodMetadata; +import io.vertx.core.Future; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Contract allowing only {@link Future} return type. + * + * @author Alexei KLENIN + */ +public final class VertxDelegatingContract implements Contract { + private final Contract delegate; + + public VertxDelegatingContract(final Contract delegate) { + this.delegate = checkNotNull(delegate, "delegate must not be null"); + } + + @Override + public List parseAndValidateMetadata(final Class targetType) { + checkNotNull(targetType, "Argument targetType must be not null"); + + final List metadatas = delegate.parseAndValidateMetadata(targetType); + + for (final MethodMetadata metadata : metadatas) { + final Type type = metadata.returnType(); + + if (type instanceof ParameterizedType + && ((ParameterizedType) type).getRawType().equals(Future.class)) { + final Type actualType = resolveLastTypeParameter(type, Future.class); + metadata.returnType(actualType); + } else { + throw new IllegalStateException( + String.format( + "Method %s of contract %s doesn't returns io.vertx.core.Future", + metadata.configKey(), targetType.getSimpleName())); + } + } + + return metadatas; + } +} diff --git a/vertx/src/main/java/feign/vertx/VertxHttpClient.java b/vertx/src/main/java/feign/vertx/VertxHttpClient.java new file mode 100644 index 000000000..5e545c6d8 --- /dev/null +++ b/vertx/src/main/java/feign/vertx/VertxHttpClient.java @@ -0,0 +1,148 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static feign.Util.checkNotNull; + +import feign.Request; +import feign.Response; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.*; +import io.vertx.core.http.impl.headers.HeadersMultiMap; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Like {@link feign.Client} but method {@link #execute} returns {@link Future} with {@link + * Response}. HTTP request is executed asynchronously with Vert.x + * + * @author Alexei KLENIN + * @author Gordon McKinney + */ +@SuppressWarnings("unused") +public final class VertxHttpClient { + private final HttpClient httpClient; + private final long timeout; + private final UnaryOperator requestPreProcessor; + + /** + * Constructor from {@link Vertx} instance, HTTP client options and request timeout. + * + * @param vertx vertx instance + * @param options HTTP options + * @param timeout request timeout + * @param requestPreProcessor request pre-processor + */ + public VertxHttpClient( + final Vertx vertx, + final HttpClientOptions options, + final long timeout, + final UnaryOperator requestPreProcessor) { + checkNotNull(vertx, "Argument vertx must not be null"); + checkNotNull(options, "Argument options must be not null"); + checkNotNull(requestPreProcessor, "Argument requestPreProcessor must be not null"); + + this.httpClient = vertx.createHttpClient(options); + this.timeout = timeout; + this.requestPreProcessor = requestPreProcessor; + } + + /** + * Executes HTTP request and returns {@link Future} with response. + * + * @param request request + * @return future of HTTP response + */ + public Future execute(final Request request) { + checkNotNull(request, "Argument request must be not null"); + + final Future httpClientRequest; + + try { + httpClientRequest = makeHttpClientRequest(request); + } catch (final MalformedURLException unexpectedException) { + return Future.failedFuture(unexpectedException); + } + + final Future responseFuture = + httpClientRequest.compose( + req -> request.body() != null ? req.send(Buffer.buffer(request.body())) : req.send()); + + return responseFuture.compose( + response -> { + final Map> responseHeaders = + StreamSupport.stream(response.headers().spliterator(), false) + .collect( + Collectors.groupingBy( + Map.Entry::getKey, + Collectors.mapping( + Map.Entry::getValue, Collectors.toCollection(ArrayList::new)))); + + return response + .body() + .map( + body -> + Response.builder() + .status(response.statusCode()) + .reason(response.statusMessage()) + .headers(responseHeaders) + .body(body.getBytes()) + .request(request) + .build()); + }); + } + + private Future makeHttpClientRequest(final Request request) + throws MalformedURLException { + final URL url = new URL(request.url()); + final String host = url.getHost(); + final String requestUri = url.getFile(); + + int port; + if (url.getPort() > -1) { + port = url.getPort(); + } else if (url.getProtocol().equalsIgnoreCase("https")) { + port = 443; + } else { + port = HttpClientOptions.DEFAULT_DEFAULT_PORT; + } + + final HttpMethod httpMethod = HttpMethod.valueOf(request.httpMethod().name()); + + final MultiMap headers = new HeadersMultiMap(); + request.headers().forEach((key, values) -> values.forEach(value -> headers.add(key, value))); + + final RequestOptions requestOptions = + new RequestOptions() + .setMethod(httpMethod) + .setHost(host) + .setPort(port) + .setURI(requestUri) + .setTimeout(timeout) + .setHeaders(headers); + + return this.httpClient.request(requestOptions).map(requestPreProcessor); + } +} diff --git a/vertx/src/test/java/feign/vertx/AbstractClientReconnectTest.java b/vertx/src/test/java/feign/vertx/AbstractClientReconnectTest.java new file mode 100644 index 000000000..04f20448c --- /dev/null +++ b/vertx/src/test/java/feign/vertx/AbstractClientReconnectTest.java @@ -0,0 +1,131 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import feign.vertx.testcase.HelloServiceAPI; +import io.vertx.core.AsyncResult; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxTestContext; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.*; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractClientReconnectTest extends AbstractFeignVertxTest { + static String baseUrl; + static int serverPort; + + HelloServiceAPI client = null; + + @BeforeAll + static void setupMockServer() { + serverPort = wireMock.port(); + baseUrl = wireMock.baseUrl(); + wireMock.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(200))); + } + + @BeforeAll + protected abstract void createClient(Vertx vertx); + + @Test + @DisplayName("All requests should be answered") + void testAllRequestsShouldBeAnswered(VertxTestContext testContext) { + sendRequests(10).compose(responses -> assertAllRequestsAnswered(responses, testContext)); + } + + @Nested + @DisplayName("After server has became unavailable") + class AfterServerBecameUnavailable { + + @BeforeEach + void shutDownServer() { + wireMock.stop(); + } + + @Test + @DisplayName("All requests should fail") + void testAllRequestsShouldFail(VertxTestContext testContext) { + sendRequests(10) + .onComplete( + responses -> + testContext.verify( + () -> { + if (responses.succeeded()) { + testContext.failNow( + new IllegalStateException( + "Client should not get responses from unavailable server")); + } + + try { + assertThat(responses.cause().getMessage()).startsWith("Connection "); + testContext.completeNow(); + } catch (Throwable assertionException) { + testContext.failNow(assertionException); + } + })); + } + + @Nested + @DisplayName("After server is available again") + class AfterServerIsBack { + WireMockServer restartedServer = new WireMockServer(options().port(serverPort)); + + @BeforeEach + void restartServer() { + restartedServer.start(); + restartedServer.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(200))); + } + + @AfterEach + void shutDownServer() { + restartedServer.stop(); + } + + @Test + @DisplayName("All requests should be answered") + void testAllRequestsShouldBeAnswered(VertxTestContext testContext) { + sendRequests(10).compose(responses -> assertAllRequestsAnswered(responses, testContext)); + } + } + } + + CompositeFuture sendRequests(int requests) { + List requestList = + IntStream.range(0, requests) + .mapToObj(ignored -> client.hello()) + .collect(Collectors.toList()); + return CompositeFuture.all(requestList); + } + + Future assertAllRequestsAnswered( + AsyncResult responses, VertxTestContext testContext) { + if (responses.succeeded()) { + testContext.completeNow(); + return Future.succeededFuture(); + } else { + testContext.failNow(responses.cause()); + return Future.failedFuture(responses.cause()); + } + } +} diff --git a/vertx/src/test/java/feign/vertx/AbstractFeignVertxTest.java b/vertx/src/test/java/feign/vertx/AbstractFeignVertxTest.java new file mode 100644 index 000000000..3a4ab6264 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/AbstractFeignVertxTest.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +import com.github.tomakehurst.wiremock.WireMockServer; +import feign.vertx.testcase.domain.OrderGenerator; +import io.vertx.junit5.VertxExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +public abstract class AbstractFeignVertxTest { + protected static WireMockServer wireMock = new WireMockServer(options().dynamicPort()); + protected static final OrderGenerator generator = new OrderGenerator(); + + @BeforeAll + @DisplayName("Setup WireMock server") + static void setupWireMockServer() { + wireMock.start(); + } + + @AfterAll + @DisplayName("Shutdown WireMock server") + static void shutdownWireMockServer() { + wireMock.stop(); + } +} diff --git a/vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java new file mode 100644 index 000000000..779e63a95 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -0,0 +1,127 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.vertx.testcase.HelloServiceAPI; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.*; +import io.vertx.core.impl.ConcurrentHashSet; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +@DisplayName("Test that connections does not leak") +public class ConnectionsLeakTests { + private static final HttpServerOptions serverOptions = + new HttpServerOptions().setLogActivity(true).setPort(8091).setSsl(false); + + HttpServer httpServer; + + private final Set connections = new ConcurrentHashSet<>(); + + @BeforeEach + public void initServer(Vertx vertx) { + httpServer = vertx.createHttpServer(serverOptions); + httpServer.requestHandler( + request -> { + if (request.connection() != null) { + this.connections.add(request.connection()); + } + request.response().end("Hello world"); + }); + httpServer.listen(); + } + + @AfterEach + public void shutdownServer() { + httpServer.close(); + connections.clear(); + } + + @Test + @DisplayName("when use HTTP 1.1") + public void testHttp11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { + int pollSize = 3; + int nbRequests = 100; + + HttpClientOptions options = new HttpClientOptions().setMaxPoolSize(pollSize); + + HelloServiceAPI client = + VertxFeign.builder() + .vertx(vertx) + .options(options) + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .target(HelloServiceAPI.class, "http://localhost:8091"); + + assertNotLeaks(client, testContext, nbRequests, pollSize); + } + + @Test + @DisplayName("when use HTTP 2") + public void testHttp2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { + int pollSize = 1; + int nbRequests = 100; + + HttpClientOptions options = + new HttpClientOptions().setProtocolVersion(HttpVersion.HTTP_2).setHttp2MaxPoolSize(1); + + HelloServiceAPI client = + VertxFeign.builder() + .vertx(vertx) + .options(options) + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .target(HelloServiceAPI.class, "http://localhost:8091"); + + assertNotLeaks(client, testContext, nbRequests, pollSize); + } + + void assertNotLeaks( + HelloServiceAPI client, VertxTestContext testContext, int nbRequests, int pollSize) { + List futures = + IntStream.range(0, nbRequests).mapToObj(ignored -> client.hello()).collect(toList()); + + CompositeFuture.all(futures) + .onComplete( + ignored -> + testContext.verify( + () -> { + try { + assertThat(this.connections.size()).isEqualTo(pollSize); + testContext.completeNow(); + } catch (Throwable assertionFailure) { + testContext.failNow(assertionFailure); + } + })); + } +} diff --git a/vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java new file mode 100644 index 000000000..1c443e1ee --- /dev/null +++ b/vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.vertx.testcase.HelloServiceAPI; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("Tests of reconnection with HTTP 1.1") +public class Http11ClientReconnectTest extends AbstractClientReconnectTest { + + @BeforeAll + @Override + protected void createClient(final Vertx vertx) { + HttpClientOptions options = new HttpClientOptions().setMaxPoolSize(3); + + client = + VertxFeign.builder() + .vertx(vertx) + .options(options) + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .target(HelloServiceAPI.class, baseUrl); + } +} diff --git a/vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java b/vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java new file mode 100644 index 000000000..23c4d4eb2 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/Http2ClientReconnectTest.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.vertx.testcase.HelloServiceAPI; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpVersion; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("Tests of reconnection with HTTP 2") +public class Http2ClientReconnectTest extends AbstractClientReconnectTest { + + @BeforeAll + @Override + protected void createClient(Vertx vertx) { + HttpClientOptions options = + new HttpClientOptions().setProtocolVersion(HttpVersion.HTTP_2).setHttp2MaxPoolSize(1); + + client = + VertxFeign.builder() + .vertx(vertx) + .options(options) + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .target(HelloServiceAPI.class, baseUrl); + } +} diff --git a/vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java b/vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java new file mode 100644 index 000000000..ef46034cc --- /dev/null +++ b/vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java @@ -0,0 +1,117 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import feign.*; +import feign.jackson.JacksonDecoder; +import feign.slf4j.Slf4jLogger; +import feign.vertx.testcase.domain.Bill; +import feign.vertx.testcase.domain.Flavor; +import feign.vertx.testcase.domain.IceCreamOrder; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.junit5.VertxTestContext; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Tests of QueryMapEncoder") +public class QueryMapEncoderTest extends AbstractFeignVertxTest { + interface Api { + + @RequestLine("POST /icecream/orders") + Future makeOrder(@QueryMap IceCreamOrder order); + } + + Api client; + + @BeforeEach + void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .options(new HttpClientOptions().setLogActivity(true)) + .queryMapEncoder(new CustomQueryMapEncoder()) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(Api.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("QueryMapEncoder will be used") + void testWillMakeOrder(VertxTestContext testContext) { + + /* Given */ + IceCreamOrder order = new IceCreamOrder(); + order.addBall(Flavor.PISTACHIO); + order.addBall(Flavor.PISTACHIO); + order.addBall(Flavor.STRAWBERRY); + order.addBall(Flavor.BANANA); + order.addBall(Flavor.VANILLA); + + Bill bill = Bill.makeBill(order); + String billStr = TestUtils.encodeAsJsonString(bill); + + wireMock.stubFor( + post(urlPathEqualTo("/icecream/orders")) + .withQueryParam("balls", equalTo("BANANA:1,PISTACHIO:2,STRAWBERRY:1,VANILLA:1")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(billStr))); + + /* When */ + Future billFuture = client.makeOrder(order); + + /* Then */ + billFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + assertThat(res.result()).isEqualTo(bill); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + + class CustomQueryMapEncoder implements QueryMapEncoder { + @Override + public Map encode(final Object o) { + IceCreamOrder order = (IceCreamOrder) o; + + String balls = + order.getBalls().entrySet().stream() + .sorted(Comparator.comparing(en -> en.getKey().toString())) + .map(entry -> entry.getKey().toString() + ':' + entry.getValue()) + .collect(Collectors.joining(",")); + + return Collections.singletonMap("balls", balls); + } + } +} diff --git a/vertx/src/test/java/feign/vertx/RawContractTest.java b/vertx/src/test/java/feign/vertx/RawContractTest.java new file mode 100644 index 000000000..c447fa414 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/RawContractTest.java @@ -0,0 +1,124 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.Response; +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.vertx.testcase.RawServiceAPI; +import feign.vertx.testcase.domain.Bill; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxTestContext; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("When creating client from 'raw' contract") +public class RawContractTest extends AbstractFeignVertxTest { + static RawServiceAPI client; + + @BeforeAll + static void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .encoder(new JacksonEncoder(TestUtils.MAPPER)) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .target(RawServiceAPI.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("should get available flavors") + public void testGetAvailableFlavors(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + + /* When */ + Future flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + Response response = res.result(); + try { + String content = + new BufferedReader(new InputStreamReader(response.body().asInputStream())) + .lines() + .collect(Collectors.joining("\n")); + + assertThat(response.status()).isEqualTo(200); + assertThat(content).isEqualTo(FLAVORS_JSON); + testContext.completeNow(); + } catch (IOException ioException) { + testContext.failNow(ioException); + } + } else { + testContext.failNow(res.cause()); + } + })); + } + + @Test + @DisplayName("should pay bill") + public void testPayBill(VertxTestContext testContext) { + + /* Given */ + Bill bill = Bill.makeBill(generator.generate()); + String billStr = TestUtils.encodeAsJsonString(bill); + + wireMock.stubFor( + post(urlEqualTo("/icecream/bills/pay")) + .withHeader("Content-Type", equalTo("application/json")) + .withRequestBody(equalToJson(billStr)) + .willReturn(aResponse().withStatus(200))); + + /* When */ + Future payedFuture = client.payBill(bill); + + /* Then */ + payedFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } +} diff --git a/vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java b/vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java new file mode 100644 index 000000000..1ae97c226 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; + +import feign.Logger; +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.slf4j.Slf4jLogger; +import feign.vertx.testcase.IcecreamServiceApi; +import feign.vertx.testcase.domain.Flavor; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.junit5.VertxTestContext; +import java.util.Arrays; +import java.util.Collection; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Test request pre processor") +public class RequestPreProcessorTest extends AbstractFeignVertxTest { + IcecreamServiceApi client; + + @BeforeEach + void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .options(new HttpClientOptions().setLogActivity(true)) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .requestPreProcessor(req -> req.putHeader("version", "v1")) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("request pre processor must be applied") + void testRequestPreProcessorMustApply(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("version", equalTo("v1")) + .willReturn( + aResponse() + .withStatus(200) + .withFixedDelay(100) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + + Future> flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + Collection flavors = res.result(); + Assertions.assertThat(flavors) + .hasSize(Flavor.values().length) + .containsAll(Arrays.asList(Flavor.values())); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } +} diff --git a/vertx/src/test/java/feign/vertx/RetryingTest.java b/vertx/src/test/java/feign/vertx/RetryingTest.java new file mode 100644 index 000000000..f6b5b2743 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/RetryingTest.java @@ -0,0 +1,140 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; +import static feign.vertx.TestUtils.MAPPER; +import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.Logger; +import feign.RetryableException; +import feign.Retryer; +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.slf4j.Slf4jLogger; +import feign.vertx.testcase.IcecreamServiceApi; +import feign.vertx.testcase.domain.Flavor; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxTestContext; +import java.util.Arrays; +import java.util.Collection; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("When server ask client to retry") +public class RetryingTest extends AbstractFeignVertxTest { + static IcecreamServiceApi client; + + @BeforeAll + static void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .decoder(new JacksonDecoder(MAPPER)) + .retryer(new Retryer.Default(100, SECONDS.toMillis(1), 5)) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("should succeed when client retries less than max attempts") + public void testRetrying_success(VertxTestContext testContext) { + + /* Given */ + String scenario = "testRetrying_success"; + + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .inScenario(scenario) + .whenScenarioStateIs(STARTED) + .willReturn(aResponse().withStatus(503).withHeader("Retry-After", "1")) + .willSetStateTo("attempt1")); + + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .inScenario(scenario) + .whenScenarioStateIs("attempt1") + .willReturn(aResponse().withStatus(503).withHeader("Retry-After", "1")) + .willSetStateTo("attempt2")); + + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .inScenario(scenario) + .whenScenarioStateIs("attempt2") + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + + /* When */ + Future> flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + assertThat(res.result()) + .hasSize(Flavor.values().length) + .containsAll(Arrays.asList(Flavor.values())); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + + @Test + @DisplayName("should fail when after max number of attempts") + public void testRetrying_noMoreAttempts(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse().withStatus(503).withHeader("Retry-After", "1"))); + + /* When */ + Future> flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.failed()) { + assertThat(res.cause()) + .isInstanceOf(RetryableException.class) + .hasMessageContaining("503 Service Unavailable"); + testContext.completeNow(); + } else { + testContext.failNow( + new IllegalStateException("RetryableException excepted but not occurred")); + } + })); + } +} diff --git a/vertx/src/test/java/feign/vertx/TestUtils.java b/vertx/src/test/java/feign/vertx/TestUtils.java new file mode 100644 index 000000000..6b9306eb1 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/TestUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +class TestUtils { + static final ObjectMapper MAPPER = new ObjectMapper(); + + static { + MAPPER.registerModule(new JavaTimeModule()); + } + + static String encodeAsJsonString(final Object object) { + try { + return MAPPER.writeValueAsString(object); + } catch (JsonProcessingException unexpectedException) { + return ""; + } + } +} diff --git a/vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java b/vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java new file mode 100644 index 000000000..db093e014 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java @@ -0,0 +1,122 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.Logger; +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.slf4j.Slf4jLogger; +import feign.vertx.testcase.IcecreamServiceApi; +import feign.vertx.testcase.domain.Flavor; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.junit5.VertxTestContext; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Tests of handling of timeouts") +public class TimeoutHandlingTest extends AbstractFeignVertxTest { + IcecreamServiceApi client; + + @BeforeEach + void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .timeout(1000) + .options(new HttpClientOptions().setLogActivity(true)) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("when timeout is reached") + void testWhenTimeoutIsReached(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withFixedDelay(1500) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + + Future> flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + testContext.failNow("should timeout!"); + } else { + assertThat(res.cause()) + .isInstanceOf(TimeoutException.class) + .hasMessageContaining("timeout"); + testContext.completeNow(); + } + })); + } + + @Test + @DisplayName("when timeout is not reached") + void testWhenTimeoutIsNotReached(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withFixedDelay(100) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + + Future> flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + Collection flavors = res.result(); + assertThat(flavors) + .hasSize(Flavor.values().length) + .containsAll(Arrays.asList(Flavor.values())); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } +} diff --git a/vertx/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/src/test/java/feign/vertx/VertxHttpClientTest.java new file mode 100644 index 000000000..9d4980ada --- /dev/null +++ b/vertx/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -0,0 +1,325 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; +import static feign.vertx.testcase.domain.Mixin.MIXINS_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import feign.FeignException; +import feign.Logger; +import feign.Request; +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.slf4j.Slf4jLogger; +import feign.vertx.testcase.IcecreamServiceApi; +import feign.vertx.testcase.IcecreamServiceApiBroken; +import feign.vertx.testcase.domain.Bill; +import feign.vertx.testcase.domain.Flavor; +import feign.vertx.testcase.domain.IceCreamOrder; +import feign.vertx.testcase.domain.Mixin; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxTestContext; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("FeignVertx client") +public class VertxHttpClientTest extends AbstractFeignVertxTest { + + @Nested + @DisplayName("When make a GET request") + class WhenMakeGetRequest { + IcecreamServiceApi client; + + @BeforeEach + void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .options(new Request.Options(5L, TimeUnit.SECONDS, 5L, TimeUnit.SECONDS, true)) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("will get flavors") + void testWillGetFlavors(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + + /* When */ + Future> flavorsFuture = client.getAvailableFlavors(); + + /* Then */ + flavorsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + Collection flavors = res.result(); + + Assertions.assertThat(flavors) + .hasSize(Flavor.values().length) + .containsAll(Arrays.asList(Flavor.values())); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + + @Test + @DisplayName("will get mixins") + void testWillGetMixins(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/mixins")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(MIXINS_JSON))); + + /* When */ + Future> mixinsFuture = client.getAvailableMixins(); + + /* Then */ + mixinsFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + Collection mixins = res.result(); + + Assertions.assertThat(mixins) + .hasSize(Mixin.values().length) + .containsAll(Arrays.asList(Mixin.values())); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + + @Test + @DisplayName("will get order by id") + void testWillGetOrderById(VertxTestContext testContext) { + + /* Given */ + IceCreamOrder order = generator.generate(); + int orderId = order.getId(); + String orderStr = TestUtils.encodeAsJsonString(order); + + wireMock.stubFor( + get(urlEqualTo("/icecream/orders/" + orderId)) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(orderStr))); + + /* When */ + Future orderFuture = client.findOrder(orderId); + + /* Then */ + orderFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + assertThat(res.result()).isEqualTo(order); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + + @Test + @DisplayName("will return 404 when try to get non-existing order by id") + void testWillReturn404WhenTryToGetNonExistingOrderById(VertxTestContext testContext) { + + /* Given */ + wireMock.stubFor( + get(urlEqualTo("/icecream/orders/123")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse().withStatus(404))); + + /* When */ + Future orderFuture = client.findOrder(123); + + /* Then */ + orderFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.failed()) { + assertThat(res.cause()) + .isInstanceOf(FeignException.class) + .hasMessageContaining("404 Not Found"); + testContext.completeNow(); + } else { + testContext.failNow( + new IllegalStateException("FeignException excepted but not occurred")); + } + })); + } + } + + @Nested + @DisplayName("When make a POST request") + class WhenMakePostRequest { + IcecreamServiceApi client; + + @BeforeEach + void createClient(Vertx vertx) { + client = + VertxFeign.builder() + .vertx(vertx) + .encoder(new JacksonEncoder(TestUtils.MAPPER)) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + } + + @Test + @DisplayName("will make an order") + void testWillMakeOrder(VertxTestContext testContext) { + + /* Given */ + IceCreamOrder order = generator.generate(); + Bill bill = Bill.makeBill(order); + String orderStr = TestUtils.encodeAsJsonString(order); + String billStr = TestUtils.encodeAsJsonString(bill); + + wireMock.stubFor( + post(urlEqualTo("/icecream/orders")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(orderStr)) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(billStr))); + + /* When */ + Future billFuture = client.makeOrder(order); + + /* Then */ + billFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + assertThat(res.result()).isEqualTo(bill); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + + @Test + @DisplayName("will pay bill") + void testWillPayBill(VertxTestContext testContext) { + + /* Given */ + Bill bill = Bill.makeBill(generator.generate()); + String billStr = TestUtils.encodeAsJsonString(bill); + + wireMock.stubFor( + post(urlEqualTo("/icecream/bills/pay")) + .withHeader("Content-Type", equalTo("application/json")) + .withRequestBody(equalToJson(billStr)) + .willReturn(aResponse().withStatus(200))); + + /* When */ + Future payedFuture = client.payBill(bill); + + /* Then */ + payedFuture.onComplete( + res -> + testContext.verify( + () -> { + if (res.succeeded()) { + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + })); + } + } + + @Nested + @DisplayName("Should fail client instantiation") + class ShouldFailedClientInstantiation { + + @Test + @DisplayName("when Vertx is not provided") + void testWhenVertxMissing() { + + /* Given */ + ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = + () -> VertxFeign.builder().target(IcecreamServiceApi.class, wireMock.baseUrl()); + + /* Then */ + assertThatCode(instantiateContractForgottenVertx) + .isInstanceOf(NullPointerException.class) + .hasMessage("Vertx instance wasn't provided in VertxFeign builder"); + } + + @Test + @DisplayName("when try to instantiate contract that have method that not return future") + void testWhenTryToInstantiateBrokenContract(Vertx vertx) { + + /* Given */ + ThrowableAssert.ThrowingCallable instantiateBrokenContract = + () -> + VertxFeign.builder() + .vertx(vertx) + .target(IcecreamServiceApiBroken.class, wireMock.baseUrl()); + + /* Then */ + assertThatCode(instantiateBrokenContract) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("IcecreamServiceApiBroken#findOrder(int)"); + } + } +} diff --git a/vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java b/vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java new file mode 100644 index 000000000..c779fb7a6 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static feign.vertx.testcase.domain.Flavor.FLAVORS_JSON; + +import feign.Logger; +import feign.Request; +import feign.VertxFeign; +import feign.jackson.JacksonDecoder; +import feign.slf4j.Slf4jLogger; +import feign.vertx.testcase.IcecreamServiceApi; +import feign.vertx.testcase.domain.Flavor; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpVersion; +import io.vertx.junit5.VertxTestContext; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("FeignVertx client should be created from") +public class VertxHttpOptionsTest extends AbstractFeignVertxTest { + + @BeforeAll + static void setupStub() { + wireMock.stubFor( + get(urlEqualTo("/icecream/flavors")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(FLAVORS_JSON))); + } + + @Test + @DisplayName("HttpClientOptions from Vertx") + public void testHttpClientOptions(Vertx vertx, VertxTestContext testContext) { + HttpClientOptions options = + new HttpClientOptions() + .setProtocolVersion(HttpVersion.HTTP_2) + .setHttp2MaxPoolSize(1) + .setConnectTimeout(5000) + .setIdleTimeout(5000); + + IcecreamServiceApi client = + VertxFeign.builder() + .vertx(vertx) + .options(options) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + + testClient(client, testContext); + } + + @Test + @DisplayName("Request Options from Feign") + public void testRequestOptions(Vertx vertx, VertxTestContext testContext) { + IcecreamServiceApi client = + VertxFeign.builder() + .vertx(vertx) + .options(new Request.Options(5L, TimeUnit.SECONDS, 5L, TimeUnit.SECONDS, true)) + .decoder(new JacksonDecoder(TestUtils.MAPPER)) + .logger(new Slf4jLogger()) + .logLevel(Logger.Level.FULL) + .target(IcecreamServiceApi.class, wireMock.baseUrl()); + + testClient(client, testContext); + } + + private void testClient(IcecreamServiceApi client, VertxTestContext testContext) { + client + .getAvailableFlavors() + .onComplete( + res -> { + if (res.succeeded()) { + Collection flavors = res.result(); + + Assertions.assertThat(flavors) + .hasSize(Flavor.values().length) + .containsAll(Arrays.asList(Flavor.values())); + testContext.completeNow(); + } else { + testContext.failNow(res.cause()); + } + }); + } +} diff --git a/vertx/src/test/java/feign/vertx/testcase/HelloServiceAPI.java b/vertx/src/test/java/feign/vertx/testcase/HelloServiceAPI.java new file mode 100644 index 000000000..0cfec3631 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/HelloServiceAPI.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase; + +import feign.Headers; +import feign.RequestLine; +import feign.Response; +import io.vertx.core.Future; + +/** + * Example of an API to to test number of Http2 connections of Feign. + * + * @author James Xu + */ +@Headers({"Accept: application/json"}) +public interface HelloServiceAPI { + + @RequestLine("GET /hello") + Future hello(); +} diff --git a/vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApi.java b/vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApi.java new file mode 100644 index 000000000..3963a8df9 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApi.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase; + +import feign.Headers; +import feign.Param; +import feign.RequestLine; +import feign.vertx.testcase.domain.Bill; +import feign.vertx.testcase.domain.Flavor; +import feign.vertx.testcase.domain.IceCreamOrder; +import feign.vertx.testcase.domain.Mixin; +import io.vertx.core.Future; +import java.util.Collection; + +/** + * API of an iceream web service. + * + * @author Alexei KLENIN + */ +@Headers({"Accept: application/json"}) +public interface IcecreamServiceApi { + + @RequestLine("GET /icecream/flavors") + Future> getAvailableFlavors(); + + @RequestLine("GET /icecream/mixins") + Future> getAvailableMixins(); + + @RequestLine("POST /icecream/orders") + @Headers("Content-Type: application/json") + Future makeOrder(IceCreamOrder order); + + @RequestLine("GET /icecream/orders/{orderId}") + Future findOrder(@Param("orderId") int orderId); + + @RequestLine("POST /icecream/bills/pay") + @Headers("Content-Type: application/json") + Future payBill(Bill bill); +} diff --git a/vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApiBroken.java b/vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApiBroken.java new file mode 100644 index 000000000..d50d7d9b7 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/IcecreamServiceApiBroken.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase; + +import feign.Headers; +import feign.Param; +import feign.RequestLine; +import feign.vertx.VertxDelegatingContract; +import feign.vertx.testcase.domain.Bill; +import feign.vertx.testcase.domain.Flavor; +import feign.vertx.testcase.domain.IceCreamOrder; +import feign.vertx.testcase.domain.Mixin; +import io.vertx.core.Future; +import java.util.Collection; + +/** + * API of an iceream web service with one method that doesn't returns {@link Future} and violates + * {@link VertxDelegatingContract}s rules. + * + * @author Alexei KLENIN + */ +public interface IcecreamServiceApiBroken { + + @RequestLine("GET /icecream/flavors") + Future> getAvailableFlavors(); + + @RequestLine("GET /icecream/mixins") + Future> getAvailableMixins(); + + @RequestLine("POST /icecream/orders") + @Headers("Content-Type: application/json") + Future makeOrder(IceCreamOrder order); + + /** Method that doesn't respects contract. */ + @RequestLine("GET /icecream/orders/{orderId}") + IceCreamOrder findOrder(@Param("orderId") int orderId); +} diff --git a/vertx/src/test/java/feign/vertx/testcase/RawServiceAPI.java b/vertx/src/test/java/feign/vertx/testcase/RawServiceAPI.java new file mode 100644 index 000000000..ae0dd93e3 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/RawServiceAPI.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase; + +import feign.Headers; +import feign.RequestLine; +import feign.Response; +import feign.vertx.testcase.domain.Bill; +import io.vertx.core.Future; + +/** + * Example of an API to to test rarely used features of Feign. + * + * @author Alexei KLENIN + */ +@Headers({"Accept: application/json"}) +public interface RawServiceAPI { + + @RequestLine("GET /icecream/flavors") + Future getAvailableFlavors(); + + @RequestLine("POST /icecream/bills/pay") + @Headers("Content-Type: application/json") + Future payBill(Bill bill); +} diff --git a/vertx/src/test/java/feign/vertx/testcase/domain/Bill.java b/vertx/src/test/java/feign/vertx/testcase/domain/Bill.java new file mode 100644 index 000000000..49f257b99 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/domain/Bill.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase.domain; + +import java.util.Map; +import java.util.Objects; + +/** + * Bill for consumed ice cream. + * + * @author Alexei KLENIN + */ +public class Bill { + private static final Map PRICES = + Map.of( + 1, (float) 2.00, // two euros for one ball (expensive!) + 3, (float) 2.85, // 2.85€ for 3 balls + 5, (float) 4.30, // 4.30€ for 5 balls + 7, (float) 5); // only five euros for seven balls! Wow + + private static final float MIXIN_PRICE = (float) 0.6; // price per mixin + + private Float price; + + public Bill() {} + + public Bill(final Float price) { + this.price = price; + } + + public Float getPrice() { + return price; + } + + public void setPrice(final Float price) { + this.price = price; + } + + /** + * Makes a bill from an order. + * + * @param order ice cream order + * @return bill + */ + public static Bill makeBill(final IceCreamOrder order) { + int nbBalls = order.getBalls().values().stream().mapToInt(Integer::intValue).sum(); + Float price = PRICES.get(nbBalls) + order.getMixins().size() * MIXIN_PRICE; + return new Bill(price); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof Bill)) { + return false; + } + + final Bill another = (Bill) other; + return Objects.equals(price, another.price); + } + + @Override + public int hashCode() { + return Objects.hash(price); + } +} diff --git a/vertx/src/test/java/feign/vertx/testcase/domain/Flavor.java b/vertx/src/test/java/feign/vertx/testcase/domain/Flavor.java new file mode 100644 index 000000000..34e0c3d5b --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/domain/Flavor.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase.domain; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Ice cream flavors. + * + * @author Alexei KLENIN + */ +public enum Flavor { + STRAWBERRY, + CHOCOLATE, + BANANA, + PISTACHIO, + MELON, + VANILLA; + + public static final String FLAVORS_JSON = + Stream.of(Flavor.values()) + .map(flavor -> "\"" + flavor + "\"") + .collect(Collectors.joining(", ", "[ ", " ]")); +} diff --git a/vertx/src/test/java/feign/vertx/testcase/domain/IceCreamOrder.java b/vertx/src/test/java/feign/vertx/testcase/domain/IceCreamOrder.java new file mode 100644 index 000000000..2569eebee --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/domain/IceCreamOrder.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase.domain; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Give me some ice-cream! :p + * + * @author Alexei KLENIN + */ +public class IceCreamOrder { + private final int id; // order id + private final Map balls; // how much balls of flavor + private final Set mixins; // and some mixins ... + private Instant orderTimestamp; // and give it to me right now ! + + public IceCreamOrder() { + this(Instant.now()); + } + + IceCreamOrder(final Instant orderTimestamp) { + this.id = ThreadLocalRandom.current().nextInt(); + this.balls = new HashMap<>(); + this.mixins = new LinkedHashSet<>(); + this.orderTimestamp = orderTimestamp; + } + + public IceCreamOrder addBall(final Flavor ballFlavor) { + final Integer ballCount = balls.containsKey(ballFlavor) ? balls.get(ballFlavor) + 1 : 1; + balls.put(ballFlavor, ballCount); + return this; + } + + IceCreamOrder addMixin(final Mixin mixin) { + mixins.add(mixin); + return this; + } + + IceCreamOrder withOrderTimestamp(final Instant orderTimestamp) { + this.orderTimestamp = orderTimestamp; + return this; + } + + public int getId() { + return id; + } + + public Map getBalls() { + return balls; + } + + public Set getMixins() { + return mixins; + } + + public Instant getOrderTimestamp() { + return orderTimestamp; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof IceCreamOrder)) { + return false; + } + + final IceCreamOrder another = (IceCreamOrder) other; + return id == another.id + && Objects.equals(balls, another.balls) + && Objects.equals(mixins, another.mixins) + && Objects.equals(orderTimestamp, another.orderTimestamp); + } + + @Override + public int hashCode() { + return Objects.hash(id, balls, mixins, orderTimestamp); + } + + @Override + public String toString() { + return "IceCreamOrder{" + + " id=" + + id + + ", balls=" + + balls + + ", mixins=" + + mixins + + ", orderTimestamp=" + + orderTimestamp + + '}'; + } +} diff --git a/vertx/src/test/java/feign/vertx/testcase/domain/Mixin.java b/vertx/src/test/java/feign/vertx/testcase/domain/Mixin.java new file mode 100644 index 000000000..18db1a450 --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/domain/Mixin.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase.domain; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Ice cream mix-ins. + * + * @author Alexei KLENIN + */ +public enum Mixin { + COOKIES, + MNMS, + CHOCOLATE_SIROP, + STRAWBERRY_SIROP, + NUTS, + RAINBOW; + + public static final String MIXINS_JSON = + Stream.of(Mixin.values()) + .map(flavor -> "\"" + flavor + "\"") + .collect(Collectors.joining(", ", "[ ", " ]")); +} diff --git a/vertx/src/test/java/feign/vertx/testcase/domain/OrderGenerator.java b/vertx/src/test/java/feign/vertx/testcase/domain/OrderGenerator.java new file mode 100644 index 000000000..e411b18cd --- /dev/null +++ b/vertx/src/test/java/feign/vertx/testcase/domain/OrderGenerator.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.vertx.testcase.domain; + +import java.util.Random; +import java.util.stream.IntStream; + +/** + * Generator of random ice cream orders. + * + * @author Alexei KLENIN + */ +public class OrderGenerator { + private static final int[] BALLS_NUMBER = {1, 3, 5, 7}; + private static final int[] MIXIN_NUMBER = {1, 2, 3}; + + private static final Random random = new Random(); + + public IceCreamOrder generate() { + final IceCreamOrder order = new IceCreamOrder(); + final int nbBalls = peekBallsNumber(); + final int nbMixins = peekMixinNumber(); + + IntStream.rangeClosed(1, nbBalls).mapToObj(i -> this.peekFlavor()).forEach(order::addBall); + + IntStream.rangeClosed(1, nbMixins).mapToObj(i -> this.peekMixin()).forEach(order::addMixin); + + return order; + } + + private int peekBallsNumber() { + return BALLS_NUMBER[random.nextInt(BALLS_NUMBER.length)]; + } + + private int peekMixinNumber() { + return MIXIN_NUMBER[random.nextInt(MIXIN_NUMBER.length)]; + } + + private Flavor peekFlavor() { + return Flavor.values()[random.nextInt(Flavor.values().length)]; + } + + private Mixin peekMixin() { + return Mixin.values()[random.nextInt(Mixin.values().length)]; + } +} diff --git a/vertx/src/test/resources/log4j.properties b/vertx/src/test/resources/log4j.properties new file mode 100644 index 000000000..2f1bc83fc --- /dev/null +++ b/vertx/src/test/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootLogger=DEBUG, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file