From 9c3e10cae5fa5a73d963baecc80f3826e48d88eb Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 14 Oct 2026 08:48:13 -0300 Subject: [PATCH 1/3] Add feign-jdk-json codec for JDK 28 incubator Simple JSON API and fix Dependabot auto-merge Signed-off-by: Marvin Froeder --- .circleci/config.yml | 17 +- .circleci/toolchains.xml | 11 +- .github/workflows/auto-merge-dependabot.yml | 2 +- AGENTS.md | 2 + feign-bom/pom.xml | 5 + jdk-json/README.md | 69 ++++++ jdk-json/pom.xml | 68 ++++++ .../java/feign/jdkjson/JdkJsonDecoder.java | 79 +++++++ .../java/feign/jdkjson/JdkJsonEncoder.java | 46 ++++ .../java/feign/jdkjson/JdkJsonCodecTest.java | 197 ++++++++++++++++++ pom.xml | 13 ++ scripts/release.sh | 6 +- 12 files changed, 506 insertions(+), 9 deletions(-) create mode 100644 jdk-json/README.md create mode 100644 jdk-json/pom.xml create mode 100644 jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java create mode 100644 jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java create mode 100644 jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java diff --git a/.circleci/config.yml b/.circleci/config.yml index 7baff2e69b..cb5c009078 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,7 @@ commands: - run: name: 'Check if cache was restored' command: | - if [ -d "$HOME/.sdkman/candidates/java/25.0.2-tem" ] && [ -d ~/.m2/repository/io/github/openfeign ]; then + if [ -d "$HOME/.sdkman/candidates/java/25.0.2-tem" ] && [ -d "$HOME/.sdkman/candidates/java/28-ea+19" ] && [ -d ~/.m2/repository/io/github/openfeign ]; then echo "Complete cache hit detected - SDKMAN and Maven dependencies available." circleci step halt elif [ -d "$HOME/.sdkman/candidates/java/21.0.2-tem" ]; then @@ -57,6 +57,15 @@ commands: fi done sdk default java 25.0.2-tem + - run: + name: 'Install JDK 28 early access' + command: | + jdk28="$HOME/.sdkman/candidates/java/28-ea+19" + if [ ! -d "$jdk28" ]; then + mkdir -p "$jdk28" + curl -sSfL https://download.java.net/java/early_access/jdk28/19/GPL/openjdk-28-ea+19_linux-x64_bin.tar.gz \ + | tar xz --strip-components=1 -C "$jdk28" + fi - run: name: 'Configure Maven Toolchain' command: | @@ -69,7 +78,7 @@ commands: ./mvnw -ntp -B \ org.apache.maven.plugins:maven-dependency-plugin:3.8.1:go-offline \ de.qaware.maven:go-offline-maven-plugin:1.2.8:resolve-dependencies \ - -T2 -pl -:feign-benchmark + -T2 -Pjdk28 -pl -:feign-benchmark - save_cache: paths: - ~/.sdkman @@ -100,7 +109,7 @@ commands: no_output_timeout: 30m command: | source "$HOME/.sdkman/bin/sdkman-init.sh" - ./mvnw -ntp -nsu --serial -s .circleci/settings.xml -P release -pl -:feign-benchmark,-:feign-vertx4-test,-:feign-vertx5-test,-:feign-example-github,-:feign-example-github-with-coroutine,-:feign-example-wikipedia,-:feign-example-wikipedia-with-springboot -DskipTests=true deploy + ./mvnw -ntp -nsu --serial -s .circleci/settings.xml -P release,jdk28 -pl -:feign-benchmark,-:feign-vertx4-test,-:feign-vertx5-test,-:feign-example-github,-:feign-example-github-with-coroutine,-:feign-example-wikipedia,-:feign-example-wikipedia-with-springboot -DskipTests=true deploy # our job defaults defaults: &defaults @@ -148,7 +157,7 @@ jobs: name: 'Test' command: | source "$HOME/.sdkman/bin/sdkman-init.sh" - ./mvnw -ntp -B verify + ./mvnw -ntp -B -Pjdk28 verify - verify-formatting deploy: diff --git a/.circleci/toolchains.xml b/.circleci/toolchains.xml index 48764b609f..a75390eaf9 100644 --- a/.circleci/toolchains.xml +++ b/.circleci/toolchains.xml @@ -59,4 +59,13 @@ /home/circleci/.sdkman/candidates/java/25.0.2-tem - \ No newline at end of file + + jdk + + 28 + + + /home/circleci/.sdkman/candidates/java/28-ea+19 + + + diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index cc3a140b96..a1332925c7 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -25,7 +25,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge for Dependabot PRs - run: gh pr merge --auto --merge "$PR_URL" + run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/AGENTS.md b/AGENTS.md index 929d41c041..e3af4bdfe8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ Feign is a declarative HTTP client library with a modular design: - `micrometer/`, `dropwizard-metrics4/5/` - Metrics integration - `validation/`, `validation-jakarta/` - JSR-303 / Jakarta Bean Validation via the `MethodInterceptor` extension point - `http-cache/` - Conditional revalidation (`ETag` / `Last-Modified` / `304 Not Modified`) via the `MethodInterceptor` extension point +- `jdk-json/` - Codec for the JDK Simple JSON API (`jdk.incubator.json`, JEP 540); built only with `-Pjdk28` and a JDK 28 toolchain ### Key Design Patterns - **Builder Pattern**: `Feign.builder()` for configuring clients @@ -106,6 +107,7 @@ Example: - `dropwizard-metrics5/` - Java 17 (for Metrics 5.x) - `apt-test-generator/` - Java 17 (for Handlebars 4.5.0+) - `soap-jakarta/`, `jaxb-jakarta/` - Java 11 (for Jakarta namespace) +- `jdk-json/` - Java 28 (for `jdk.incubator.json`); lives in the `jdk28` profile so builds without a JDK 28 toolchain still work. CI installs the JDK 28 early-access build from jdk.java.net; bump the build number in `.circleci/config.yml` and `.circleci/toolchains.xml` when it disappears ### Dependabot Configuration diff --git a/feign-bom/pom.xml b/feign-bom/pom.xml index dd67dedb46..4ab4d71f06 100644 --- a/feign-bom/pom.xml +++ b/feign-bom/pom.xml @@ -54,6 +54,11 @@ feign-http-cache 13.18-SNAPSHOT + + io.github.openfeign + feign-jdk-json + 13.18-SNAPSHOT + io.github.openfeign feign-jaxrs diff --git a/jdk-json/README.md b/jdk-json/README.md new file mode 100644 index 0000000000..0dfe5229d2 --- /dev/null +++ b/jdk-json/README.md @@ -0,0 +1,69 @@ +JDK JSON Codec +=================== + +This module adds support for encoding and decoding JSON via the JDK's built-in [Simple JSON API][JEP 540] (`jdk.incubator.json`). +No third party JSON library is needed. + +The API is an incubator module targeted at JDK 28, so this module requires Java 28+ and the module must be resolved explicitly: + +``` +java --add-modules jdk.incubator.json ... +``` + +Incubator APIs can change incompatibly between JDK releases, so expect this module to follow along. + +## Dependency + +```xml + + io.github.openfeign + feign-jdk-json + ${feign.version} + +``` + +## Usage + +Add `JdkJsonEncoder` and/or `JdkJsonDecoder` to your `Feign.Builder`: + +```java +interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + JsonArray contributors(@Param("owner") String owner, @Param("repo") String repo); + + @RequestLine("POST /repos/{owner}/{repo}/issues") + @Headers("Content-Type: application/json") + JsonObject createIssue(@Param("owner") String owner, @Param("repo") String repo, JsonValue issue); +} + +GitHub github = Feign.builder() + .decoder(new JdkJsonDecoder()) + .encoder(new JdkJsonEncoder()) + .target(GitHub.class, "https://api.github.com"); + +for (JsonValue contributor : github.contributors("OpenFeign", "feign").asList()) { + System.out.println(contributor.get("login").asString()); +} + +JsonObject issue = github.createIssue("OpenFeign", "feign", JsonObject.of(Map.of( + "title", JsonString.of("Found a bug"), + "labels", JsonArray.of(List.of(JsonString.of("bug")))))); +``` + +The JDK API has no data binding, so the codec works with the JSON tree: + +* `JdkJsonDecoder` decodes into `JsonValue` or any of its subtypes (`JsonObject`, `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`, `JsonNull`), or the raw `String` body. + Malformed JSON, including duplicate member names, fails with a `DecodeException` caused by `JsonParseException`; a document of the wrong JSON type (e.g. an array for a `JsonObject` method) fails with a `DecodeException`. + `404` (with `dismiss404()`), `204` and empty bodies decode to `null`. +* `JdkJsonEncoder` writes any `JsonValue` as compact JSON. + +## Building + +The module is part of the `jdk28` Maven profile and needs a JDK 28 entry in `~/.m2/toolchains.xml`: + +``` +./mvnw -Pjdk28 -pl jdk-json -am install +``` + +[JEP 540]: https://openjdk.org/jeps/540 diff --git a/jdk-json/pom.xml b/jdk-json/pom.xml new file mode 100644 index 0000000000..1d92eb9c1f --- /dev/null +++ b/jdk-json/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + + io.github.openfeign + feign-parent + 13.18-SNAPSHOT + + + feign-jdk-json + Feign JDK JSON + Feign codec for the JDK Simple JSON API (jdk.incubator.json, JEP 540) + + + 28 + 28 + 28 + 28 + -Duser.language=en --add-modules jdk.incubator.json + true + + + + + ${project.groupId} + feign-core + + + + ${project.groupId} + feign-mock + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + --add-modules + jdk.incubator.json + + + + + + diff --git a/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java b/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java new file mode 100644 index 0000000000..762c34b87e --- /dev/null +++ b/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java @@ -0,0 +1,79 @@ +/* + * 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.jdkjson; + +import static java.lang.String.format; + +import feign.Response; +import feign.Util; +import feign.codec.DecodeException; +import feign.codec.Decoder; +import feign.codec.PredicatedDecoder; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import jdk.incubator.json.Json; +import jdk.incubator.json.JsonParseException; +import jdk.incubator.json.JsonValue; + +public class JdkJsonDecoder implements Decoder, PredicatedDecoder { + + @Override + public Object decode(Response response, Type type) throws IOException { + if (!(type instanceof Class target) + || !(String.class.equals(target) || JsonValue.class.isAssignableFrom(target))) { + throw new DecodeException( + response.status(), + format("%s is not a type supported by this decoder.", type), + response.request()); + } + if (response.status() == 404 || response.status() == 204 || response.body() == null) { + return null; + } + String body; + try (Reader reader = response.body().asReader(response.charset())) { + body = Util.toString(reader); + } + if (body.isEmpty()) { + return null; + } + if (String.class.equals(target)) { + return body; + } + return parse(response, target, body); + } + + private Object parse(Response response, Class target, String body) { + JsonValue value; + try { + value = Json.parse(body); + } catch (JsonParseException e) { + throw new DecodeException(response.status(), e.getMessage(), response.request(), e); + } + if (!target.isInstance(value)) { + throw new DecodeException( + response.status(), + format("Expected %s but got %s", target.getSimpleName(), value), + response.request()); + } + return value; + } + + @Override + public boolean canDecode(Response response, Type type) { + return Util.isJsonContentType(response); + } +} diff --git a/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java b/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java new file mode 100644 index 0000000000..c5d9d89712 --- /dev/null +++ b/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java @@ -0,0 +1,46 @@ +/* + * 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.jdkjson; + +import static java.lang.String.format; + +import feign.RequestTemplate; +import feign.Util; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.PredicatedEncoder; +import java.lang.reflect.Type; +import jdk.incubator.json.JsonValue; + +public class JdkJsonEncoder implements Encoder, PredicatedEncoder { + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + if (object == null) { + return; + } + if (!(object instanceof JsonValue value)) { + throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType)); + } + template.body(value.toString()); + } + + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return Util.isJsonContentType(template); + } +} diff --git a/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java b/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java new file mode 100644 index 0000000000..0036f19d2a --- /dev/null +++ b/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java @@ -0,0 +1,197 @@ +/* + * 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.jdkjson; + +import static feign.Util.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import feign.Feign; +import feign.Param; +import feign.Request; +import feign.RequestLine; +import feign.codec.DecodeException; +import feign.codec.EncodeException; +import feign.mock.HttpMethod; +import feign.mock.MockClient; +import feign.mock.MockTarget; +import java.util.List; +import java.util.Map; +import jdk.incubator.json.JsonArray; +import jdk.incubator.json.JsonNumber; +import jdk.incubator.json.JsonObject; +import jdk.incubator.json.JsonParseException; +import jdk.incubator.json.JsonString; +import jdk.incubator.json.JsonValue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JdkJsonCodecTest { + + interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + JsonArray contributors(@Param("owner") String owner, @Param("repo") String repo); + + @RequestLine("GET /repos/{owner}/{repo}") + JsonObject repo(@Param("owner") String owner, @Param("repo") String repo); + + @RequestLine("GET /repos/{owner}/{repo}") + JsonValue repoValue(@Param("owner") String owner, @Param("repo") String repo); + + @RequestLine("GET /repos/{owner}/{repo}") + String repoRaw(@Param("owner") String owner, @Param("repo") String repo); + + @RequestLine("GET /repos/{owner}/{repo}") + Map repoMap(@Param("owner") String owner, @Param("repo") String repo); + + @RequestLine("POST /repos/{owner}/{repo}/contributors") + JsonObject create( + @Param("owner") String owner, @Param("repo") String repo, JsonValue contributor); + + @RequestLine("POST /repos/{owner}/{repo}/contributors") + void createRaw(@Param("owner") String owner, @Param("repo") String repo, Object contributor); + } + + private MockClient mockClient; + private GitHub github; + + @BeforeEach + void setUp() { + mockClient = new MockClient(); + github = + Feign.builder() + .decoder(new JdkJsonDecoder()) + .encoder(new JdkJsonEncoder()) + .dismiss404() + .client(mockClient) + .target(new MockTarget<>(GitHub.class)); + } + + @Test + void decodesArray() { + mockClient.ok( + HttpMethod.GET, + "/repos/openfeign/feign/contributors", + "[{\"login\":\"velo\",\"contributions\":42},{\"login\":\"kdavisk6\",\"contributions\":7}]"); + + JsonArray contributors = github.contributors("openfeign", "feign"); + + assertThat(contributors.asList()).hasSize(2); + assertThat(contributors.get(0).get("login").asString()).isEqualTo("velo"); + assertThat(contributors.get(1).get("contributions").asInt()).isEqualTo(7); + } + + @Test + void decodesObject() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":\"feign\",\"stars\":9600}"); + + JsonObject repo = github.repo("openfeign", "feign"); + + assertThat(repo.get("name").asString()).isEqualTo("feign"); + assertThat(repo.get("stars").asLong()).isEqualTo(9600L); + } + + @Test + void decodesAnyJsonValue() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "\"feign\""); + + JsonValue value = github.repoValue("openfeign", "feign"); + + assertThat(value).isInstanceOf(JsonString.class); + assertThat(value.asString()).isEqualTo("feign"); + } + + @Test + void decodesRawString() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{ \"name\" : \"feign\" }"); + + assertThat(github.repoRaw("openfeign", "feign")).isEqualTo("{ \"name\" : \"feign\" }"); + } + + @Test + void notFoundDecodesToNull() { + mockClient.add(HttpMethod.GET, "/repos/openfeign/feign", 404); + + assertThat(github.repo("openfeign", "feign")).isNull(); + } + + @Test + void emptyBodyDecodesToNull() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", ""); + + assertThat(github.repo("openfeign", "feign")).isNull(); + } + + @Test + void malformedJsonFailsWithParseCause() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":"); + + assertThatThrownBy(() -> github.repo("openfeign", "feign")) + .isInstanceOf(DecodeException.class) + .hasCauseInstanceOf(JsonParseException.class); + } + + @Test + void duplicateMemberNamesAreRejected() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":\"a\",\"name\":\"b\"}"); + + assertThatThrownBy(() -> github.repo("openfeign", "feign")) + .isInstanceOf(DecodeException.class) + .hasCauseInstanceOf(JsonParseException.class); + } + + @Test + void mismatchedJsonTypeFails() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "[1,2]"); + + assertThatThrownBy(() -> github.repo("openfeign", "feign")) + .isInstanceOf(DecodeException.class) + .hasMessage("Expected JsonObject but got [1,2]"); + } + + @Test + void unsupportedReturnTypeFails() { + mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{}"); + + assertThatThrownBy(() -> github.repoMap("openfeign", "feign")) + .isInstanceOf(DecodeException.class) + .hasMessageContaining("is not a type supported by this decoder"); + } + + @Test + void encodesJsonValue() { + mockClient.ok(HttpMethod.POST, "/repos/openfeign/feign/contributors", "{\"id\":1}"); + JsonObject contributor = + JsonObject.of( + Map.of( + "login", JsonString.of("velo"), + "repos", JsonArray.of(List.of(JsonNumber.of(1), JsonNumber.of(2))))); + + JsonObject created = github.create("openfeign", "feign", contributor); + + Request request = mockClient.verifyOne(HttpMethod.POST, "/repos/openfeign/feign/contributors"); + assertThat(new String(request.body(), UTF_8)).isEqualTo(contributor.toString()); + assertThat(created.get("id").asInt()).isEqualTo(1); + } + + @Test + void unsupportedBodyTypeFails() { + assertThatThrownBy(() -> github.createRaw("openfeign", "feign", Map.of("login", "velo"))) + .isInstanceOf(EncodeException.class) + .hasMessageContaining("is not a type supported by this encoder"); + } +} diff --git a/pom.xml b/pom.xml index 6845cf2989..d4277e68d8 100644 --- a/pom.xml +++ b/pom.xml @@ -287,6 +287,12 @@ ${project.version} + + ${project.groupId} + feign-jdk-json + ${project.version} + + ${project.groupId} feign-httpclient @@ -1002,6 +1008,13 @@ + + jdk28 + + jdk-json + + + quickbuild diff --git a/scripts/release.sh b/scripts/release.sh index 7fb51e3cc8..7273050e5e 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -38,13 +38,13 @@ echo "release version is: ${tag} and next snapshot is: ${snapshot}" # Update the versions, removing the snapshots, then create a new tag for the release, this will # start the travis-ci release process. if [ -n "$1" ]; then - ./mvnw -B versions:set -DnewVersion="${tag}" -DgenerateBackupPoms=false + ./mvnw -B -Pjdk28 versions:set -DnewVersion="${tag}" -DgenerateBackupPoms=false fi -./mvnw -B versions:set license:format scm:checkin -DremoveSnapshot -DgenerateBackupPoms=false -Dmessage="prepare release ${tag}" -DpushChanges=false +./mvnw -B -Pjdk28 versions:set license:format scm:checkin -DremoveSnapshot -DgenerateBackupPoms=false -Dmessage="prepare release ${tag}" -DpushChanges=false # tag the release echo "pushing tag ${tag}" ./mvnw scm:tag # Update the versions to the next snapshot -./mvnw -B versions:set scm:checkin -DnewVersion="${snapshot}" -DgenerateBackupPoms=false -Dmessage="[ci skip] updating versions to next development iteration ${snapshot}" +./mvnw -B -Pjdk28 versions:set scm:checkin -DnewVersion="${snapshot}" -DgenerateBackupPoms=false -Dmessage="[ci skip] updating versions to next development iteration ${snapshot}" From a3aa9da99bd591c21cd152af2c7706e8c521eef6 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 14 Oct 2026 08:52:19 -0300 Subject: [PATCH 2/3] Pin JDK 28 EA via Dependabot-tracked openjdk image digest Signed-off-by: Marvin Froeder --- .circleci/config.yml | 19 +++++++++++-------- .circleci/jdk28/Dockerfile | 1 + .circleci/toolchains.xml | 2 +- .github/dependabot.yml | 6 ++++++ AGENTS.md | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 .circleci/jdk28/Dockerfile diff --git a/.circleci/config.yml b/.circleci/config.yml index cb5c009078..f31425ee2a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,11 +25,11 @@ commands: steps: - restore_cache: keys: - - dependencies-v1-{{ checksum "pom.xml" }} + - dependencies-v1-{{ checksum "pom.xml" }}-{{ checksum ".circleci/jdk28/Dockerfile" }} - run: name: 'Check if cache was restored' command: | - if [ -d "$HOME/.sdkman/candidates/java/25.0.2-tem" ] && [ -d "$HOME/.sdkman/candidates/java/28-ea+19" ] && [ -d ~/.m2/repository/io/github/openfeign ]; then + if [ -d "$HOME/.sdkman/candidates/java/25.0.2-tem" ] && [ -d "$HOME/.sdkman/candidates/java/28-ea" ] && [ -d ~/.m2/repository/io/github/openfeign ]; then echo "Complete cache hit detected - SDKMAN and Maven dependencies available." circleci step halt elif [ -d "$HOME/.sdkman/candidates/java/21.0.2-tem" ]; then @@ -60,11 +60,14 @@ commands: - run: name: 'Install JDK 28 early access' command: | - jdk28="$HOME/.sdkman/candidates/java/28-ea+19" + jdk28="$HOME/.sdkman/candidates/java/28-ea" if [ ! -d "$jdk28" ]; then - mkdir -p "$jdk28" - curl -sSfL https://download.java.net/java/early_access/jdk28/19/GPL/openjdk-28-ea+19_linux-x64_bin.tar.gz \ - | tar xz --strip-components=1 -C "$jdk28" + image=$(sed -n 's/^FROM //p' .circleci/jdk28/Dockerfile) + docker pull -q "$image" + java_home=$(docker run --rm --entrypoint sh "$image" -c 'echo "$JAVA_HOME"') + container=$(docker create "$image") + docker cp "$container:$java_home" "$jdk28" + docker rm "$container" fi - run: name: 'Configure Maven Toolchain' @@ -83,13 +86,13 @@ commands: paths: - ~/.sdkman - ~/.m2 - key: dependencies-v1-{{ checksum "pom.xml" }} + key: dependencies-v1-{{ checksum "pom.xml" }}-{{ checksum ".circleci/jdk28/Dockerfile" }} restore-build-environment: description: 'Restore cached build environment' steps: - restore_cache: keys: - - dependencies-v1-{{ checksum "pom.xml" }} + - dependencies-v1-{{ checksum "pom.xml" }}-{{ checksum ".circleci/jdk28/Dockerfile" }} verify-formatting: steps: - run: diff --git a/.circleci/jdk28/Dockerfile b/.circleci/jdk28/Dockerfile new file mode 100644 index 0000000000..5f634a0bba --- /dev/null +++ b/.circleci/jdk28/Dockerfile @@ -0,0 +1 @@ +FROM openjdk:28-ea-jdk@sha256:74a5c5b7deb04aa7aba908ffc2a420e66e9de33088f10e63eb377f8386aa339c diff --git a/.circleci/toolchains.xml b/.circleci/toolchains.xml index a75390eaf9..4dd9dd749f 100644 --- a/.circleci/toolchains.xml +++ b/.circleci/toolchains.xml @@ -65,7 +65,7 @@ 28 - /home/circleci/.sdkman/candidates/java/28-ea+19 + /home/circleci/.sdkman/candidates/java/28-ea diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d25a05febb..434cb88a3b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -102,3 +102,9 @@ updates: ignore: - dependency-name: "io.vertx:*" update-types: ["version-update:semver-major"] + + # JDK 28 early-access image used by CircleCI to build jdk-json + - package-ecosystem: "docker" + directory: "/.circleci/jdk28" + schedule: + interval: "daily" diff --git a/AGENTS.md b/AGENTS.md index e3af4bdfe8..eee7cc2537 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,7 +107,7 @@ Example: - `dropwizard-metrics5/` - Java 17 (for Metrics 5.x) - `apt-test-generator/` - Java 17 (for Handlebars 4.5.0+) - `soap-jakarta/`, `jaxb-jakarta/` - Java 11 (for Jakarta namespace) -- `jdk-json/` - Java 28 (for `jdk.incubator.json`); lives in the `jdk28` profile so builds without a JDK 28 toolchain still work. CI installs the JDK 28 early-access build from jdk.java.net; bump the build number in `.circleci/config.yml` and `.circleci/toolchains.xml` when it disappears +- `jdk-json/` - Java 28 (for `jdk.incubator.json`); lives in the `jdk28` profile so builds without a JDK 28 toolchain still work. CI extracts the JDK 28 early-access build from the digest-pinned `openjdk:28-ea-jdk` image in `.circleci/jdk28/Dockerfile`, which Dependabot keeps current ### Dependabot Configuration From ebc838692b16c78ee133bf0a4ef85b96abef3c8c Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 14 Oct 2026 09:05:26 -0300 Subject: [PATCH 3/3] Apply review fixes: javadoc add-modules, separate JDK 28 cache, decoder cleanup and naming Signed-off-by: Marvin Froeder --- .circleci/config.yml | 48 ++++++----- .circleci/toolchains.xml | 2 +- .github/dependabot.yml | 1 - AGENTS.md | 4 +- jdk-json/README.md | 16 ++-- jdk-json/pom.xml | 12 ++- .../java/feign/jdkjson/JdkJsonDecoder.java | 66 ++++++++++----- .../java/feign/jdkjson/JdkJsonEncoder.java | 4 +- .../java/feign/jdkjson/JdkJsonCodecTest.java | 83 ++++++++++++------- pom.xml | 2 +- scripts/release.sh | 6 +- 11 files changed, 153 insertions(+), 91 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f31425ee2a..0d49242ae5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,11 +25,30 @@ commands: steps: - restore_cache: keys: - - dependencies-v1-{{ checksum "pom.xml" }}-{{ checksum ".circleci/jdk28/Dockerfile" }} + - jdk28-v1-{{ checksum ".circleci/jdk28/Dockerfile" }} + - run: + name: 'Install JDK 28 early access' + command: | + jdk28_home="$HOME/jdk28" + if [ ! -d "$jdk28_home" ]; then + image=$(sed -n 's/^FROM //p' .circleci/jdk28/Dockerfile) + docker pull -q "$image" + image_java_home=$(docker run --rm --entrypoint sh "$image" -c 'echo "$JAVA_HOME"') + container=$(docker create "$image") + docker cp "$container:$image_java_home" "$jdk28_home" + docker rm "$container" + fi + - save_cache: + paths: + - ~/jdk28 + key: jdk28-v1-{{ checksum ".circleci/jdk28/Dockerfile" }} + - restore_cache: + keys: + - dependencies-v1-{{ checksum "pom.xml" }} - run: name: 'Check if cache was restored' command: | - if [ -d "$HOME/.sdkman/candidates/java/25.0.2-tem" ] && [ -d "$HOME/.sdkman/candidates/java/28-ea" ] && [ -d ~/.m2/repository/io/github/openfeign ]; then + if [ -d "$HOME/.sdkman/candidates/java/25.0.2-tem" ] && [ -d ~/.m2/repository/io/github/openfeign ]; then echo "Complete cache hit detected - SDKMAN and Maven dependencies available." circleci step halt elif [ -d "$HOME/.sdkman/candidates/java/21.0.2-tem" ]; then @@ -57,18 +76,6 @@ commands: fi done sdk default java 25.0.2-tem - - run: - name: 'Install JDK 28 early access' - command: | - jdk28="$HOME/.sdkman/candidates/java/28-ea" - if [ ! -d "$jdk28" ]; then - image=$(sed -n 's/^FROM //p' .circleci/jdk28/Dockerfile) - docker pull -q "$image" - java_home=$(docker run --rm --entrypoint sh "$image" -c 'echo "$JAVA_HOME"') - container=$(docker create "$image") - docker cp "$container:$java_home" "$jdk28" - docker rm "$container" - fi - run: name: 'Configure Maven Toolchain' command: | @@ -81,18 +88,21 @@ commands: ./mvnw -ntp -B \ org.apache.maven.plugins:maven-dependency-plugin:3.8.1:go-offline \ de.qaware.maven:go-offline-maven-plugin:1.2.8:resolve-dependencies \ - -T2 -Pjdk28 -pl -:feign-benchmark + -T2 -Pjdk28-modules -pl -:feign-benchmark - save_cache: paths: - ~/.sdkman - ~/.m2 - key: dependencies-v1-{{ checksum "pom.xml" }}-{{ checksum ".circleci/jdk28/Dockerfile" }} + key: dependencies-v1-{{ checksum "pom.xml" }} restore-build-environment: description: 'Restore cached build environment' steps: - restore_cache: keys: - - dependencies-v1-{{ checksum "pom.xml" }}-{{ checksum ".circleci/jdk28/Dockerfile" }} + - jdk28-v1-{{ checksum ".circleci/jdk28/Dockerfile" }} + - restore_cache: + keys: + - dependencies-v1-{{ checksum "pom.xml" }} verify-formatting: steps: - run: @@ -112,7 +122,7 @@ commands: no_output_timeout: 30m command: | source "$HOME/.sdkman/bin/sdkman-init.sh" - ./mvnw -ntp -nsu --serial -s .circleci/settings.xml -P release,jdk28 -pl -:feign-benchmark,-:feign-vertx4-test,-:feign-vertx5-test,-:feign-example-github,-:feign-example-github-with-coroutine,-:feign-example-wikipedia,-:feign-example-wikipedia-with-springboot -DskipTests=true deploy + ./mvnw -ntp -nsu --serial -s .circleci/settings.xml -P release,jdk28-modules -pl -:feign-benchmark,-:feign-vertx4-test,-:feign-vertx5-test,-:feign-example-github,-:feign-example-github-with-coroutine,-:feign-example-wikipedia,-:feign-example-wikipedia-with-springboot -DskipTests=true deploy # our job defaults defaults: &defaults @@ -160,7 +170,7 @@ jobs: name: 'Test' command: | source "$HOME/.sdkman/bin/sdkman-init.sh" - ./mvnw -ntp -B -Pjdk28 verify + ./mvnw -ntp -B -Pjdk28-modules verify - verify-formatting deploy: diff --git a/.circleci/toolchains.xml b/.circleci/toolchains.xml index 4dd9dd749f..47657e793f 100644 --- a/.circleci/toolchains.xml +++ b/.circleci/toolchains.xml @@ -65,7 +65,7 @@ 28 - /home/circleci/.sdkman/candidates/java/28-ea + /home/circleci/jdk28 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 434cb88a3b..80ef2547d2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -103,7 +103,6 @@ updates: - dependency-name: "io.vertx:*" update-types: ["version-update:semver-major"] - # JDK 28 early-access image used by CircleCI to build jdk-json - package-ecosystem: "docker" directory: "/.circleci/jdk28" schedule: diff --git a/AGENTS.md b/AGENTS.md index eee7cc2537..5859b36517 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Feign is a declarative HTTP client library with a modular design: - `micrometer/`, `dropwizard-metrics4/5/` - Metrics integration - `validation/`, `validation-jakarta/` - JSR-303 / Jakarta Bean Validation via the `MethodInterceptor` extension point - `http-cache/` - Conditional revalidation (`ETag` / `Last-Modified` / `304 Not Modified`) via the `MethodInterceptor` extension point -- `jdk-json/` - Codec for the JDK Simple JSON API (`jdk.incubator.json`, JEP 540); built only with `-Pjdk28` and a JDK 28 toolchain +- `jdk-json/` - Codec for the JDK Simple JSON API (`jdk.incubator.json`, JEP 540). It only builds if you use `-Pjdk28-modules` and a JDK 28 toolchain. ### Key Design Patterns - **Builder Pattern**: `Feign.builder()` for configuring clients @@ -107,7 +107,7 @@ Example: - `dropwizard-metrics5/` - Java 17 (for Metrics 5.x) - `apt-test-generator/` - Java 17 (for Handlebars 4.5.0+) - `soap-jakarta/`, `jaxb-jakarta/` - Java 11 (for Jakarta namespace) -- `jdk-json/` - Java 28 (for `jdk.incubator.json`); lives in the `jdk28` profile so builds without a JDK 28 toolchain still work. CI extracts the JDK 28 early-access build from the digest-pinned `openjdk:28-ea-jdk` image in `.circleci/jdk28/Dockerfile`, which Dependabot keeps current +- `jdk-json/` - This uses Java 28 (for `jdk.incubator.json`). It is in the `jdk28-modules` profile. This means builds without a JDK 28 toolchain still work. CI extracts the JDK 28 early-access build from the `openjdk:28-ea-jdk` image. This image is pinned to a specific digest in `.circleci/jdk28/Dockerfile`. Dependabot keeps this image up to date. ### Dependabot Configuration diff --git a/jdk-json/README.md b/jdk-json/README.md index 0dfe5229d2..413fbd1bba 100644 --- a/jdk-json/README.md +++ b/jdk-json/README.md @@ -1,10 +1,10 @@ JDK JSON Codec =================== -This module adds support for encoding and decoding JSON via the JDK's built-in [Simple JSON API][JEP 540] (`jdk.incubator.json`). -No third party JSON library is needed. +This module lets you encode and decode JSON using the JDK's built-in [Simple JSON API][JEP 540] (`jdk.incubator.json`). +You do not need to use any third party JSON libraries. -The API is an incubator module targeted at JDK 28, so this module requires Java 28+ and the module must be resolved explicitly: +This API is an incubator module for JDK 28, so this module requires Java 28 or higher. You also must resolve the module explicitly: ``` java --add-modules jdk.incubator.json ... @@ -53,17 +53,17 @@ JsonObject issue = github.createIssue("OpenFeign", "feign", JsonObject.of(Map.of The JDK API has no data binding, so the codec works with the JSON tree: -* `JdkJsonDecoder` decodes into `JsonValue` or any of its subtypes (`JsonObject`, `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`, `JsonNull`), or the raw `String` body. - Malformed JSON, including duplicate member names, fails with a `DecodeException` caused by `JsonParseException`; a document of the wrong JSON type (e.g. an array for a `JsonObject` method) fails with a `DecodeException`. - `404` (with `dismiss404()`), `204` and empty bodies decode to `null`. +* `JdkJsonDecoder` decodes into `JsonValue` or any of its subtypes (`JsonObject`, `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`, `JsonNull`). It can also return the raw `String` body. + If the JSON is malformed (like having duplicate member names), it throws a `DecodeException` caused by a `JsonParseException`. If the JSON type is wrong (for example, you get an array but asked for a `JsonObject`), it throws a `DecodeException`. + `404` (if you use `dismiss404()`), `204`, and empty bodies decode to `null`. * `JdkJsonEncoder` writes any `JsonValue` as compact JSON. ## Building -The module is part of the `jdk28` Maven profile and needs a JDK 28 entry in `~/.m2/toolchains.xml`: +The module is part of the `jdk28-modules` Maven profile and needs a JDK 28 entry in `~/.m2/toolchains.xml`: ``` -./mvnw -Pjdk28 -pl jdk-json -am install +./mvnw -Pjdk28-modules -pl jdk-json -am install ``` [JEP 540]: https://openjdk.org/jeps/540 diff --git a/jdk-json/pom.xml b/jdk-json/pom.xml index 1d92eb9c1f..faedd3aacf 100644 --- a/jdk-json/pom.xml +++ b/jdk-json/pom.xml @@ -32,8 +32,6 @@ 28 28 - 28 - 28 -Duser.language=en --add-modules jdk.incubator.json true @@ -63,6 +61,16 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + + + --add-modules + jdk.incubator.json + + + diff --git a/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java b/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java index 762c34b87e..5278557c2b 100644 --- a/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java +++ b/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java @@ -26,50 +26,74 @@ import java.io.Reader; import java.lang.reflect.Type; import jdk.incubator.json.Json; +import jdk.incubator.json.JsonArray; +import jdk.incubator.json.JsonBoolean; +import jdk.incubator.json.JsonNull; +import jdk.incubator.json.JsonNumber; +import jdk.incubator.json.JsonObject; import jdk.incubator.json.JsonParseException; +import jdk.incubator.json.JsonString; import jdk.incubator.json.JsonValue; public class JdkJsonDecoder implements Decoder, PredicatedDecoder { @Override public Object decode(Response response, Type type) throws IOException { - if (!(type instanceof Class target) - || !(String.class.equals(target) || JsonValue.class.isAssignableFrom(target))) { - throw new DecodeException( - response.status(), - format("%s is not a type supported by this decoder.", type), - response.request()); + Class expectedClass = supportedTarget(response, type); + String bodyText = readBody(response); + if (bodyText == null || expectedClass == String.class) { + return bodyText; } + return parseJsonValueAs(expectedClass, bodyText, response); + } + + private static Class supportedTarget(Response response, Type type) { + if (type instanceof Class expectedClass + && (expectedClass == String.class || JsonValue.class.isAssignableFrom(expectedClass))) { + return expectedClass; + } + throw new DecodeException( + response.status(), + format("%s is not a type supported by this decoder.", type), + response.request()); + } + + private static String readBody(Response response) throws IOException { if (response.status() == 404 || response.status() == 204 || response.body() == null) { return null; } - String body; try (Reader reader = response.body().asReader(response.charset())) { - body = Util.toString(reader); - } - if (body.isEmpty()) { - return null; + String bodyText = Util.toString(reader); + return bodyText.isEmpty() ? null : bodyText; } - if (String.class.equals(target)) { - return body; - } - return parse(response, target, body); } - private Object parse(Response response, Class target, String body) { - JsonValue value; + private static Object parseJsonValueAs( + Class expectedClass, String bodyText, Response response) { + JsonValue parsedJson; try { - value = Json.parse(body); + parsedJson = Json.parse(bodyText); } catch (JsonParseException e) { throw new DecodeException(response.status(), e.getMessage(), response.request(), e); } - if (!target.isInstance(value)) { + if (!expectedClass.isInstance(parsedJson)) { throw new DecodeException( response.status(), - format("Expected %s but got %s", target.getSimpleName(), value), + format("Expected %s but got a JSON %s", expectedClass.getSimpleName(), kind(parsedJson)), response.request()); } - return value; + return parsedJson; + } + + private static String kind(JsonValue parsedJson) { + return switch (parsedJson) { + case JsonObject _ -> "object"; + case JsonArray _ -> "array"; + case JsonString _ -> "string"; + case JsonNumber _ -> "number"; + case JsonBoolean _ -> "boolean"; + case JsonNull _ -> "null"; + }; } @Override diff --git a/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java b/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java index c5d9d89712..4f7ee8728e 100644 --- a/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java +++ b/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java @@ -33,10 +33,10 @@ public void encode(Object object, Type bodyType, RequestTemplate template) if (object == null) { return; } - if (!(object instanceof JsonValue value)) { + if (!(object instanceof JsonValue jsonBody)) { throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType)); } - template.body(value.toString()); + template.body(jsonBody.toString()); } @Override diff --git a/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java b/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java index 0036f19d2a..47824d4e39 100644 --- a/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java +++ b/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java @@ -23,6 +23,7 @@ import feign.Param; import feign.Request; import feign.RequestLine; +import feign.RequestTemplate; import feign.codec.DecodeException; import feign.codec.EncodeException; import feign.mock.HttpMethod; @@ -44,26 +45,27 @@ class JdkJsonCodecTest { interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") - JsonArray contributors(@Param("owner") String owner, @Param("repo") String repo); + JsonArray listContributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("GET /repos/{owner}/{repo}") - JsonObject repo(@Param("owner") String owner, @Param("repo") String repo); + JsonObject getRepo(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("GET /repos/{owner}/{repo}") - JsonValue repoValue(@Param("owner") String owner, @Param("repo") String repo); + JsonValue getRepoAsJsonValue(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("GET /repos/{owner}/{repo}") - String repoRaw(@Param("owner") String owner, @Param("repo") String repo); + String getRepoAsRawJson(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("GET /repos/{owner}/{repo}") - Map repoMap(@Param("owner") String owner, @Param("repo") String repo); + Map getRepoAsMap(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/contributors") - JsonObject create( + JsonObject createContributor( @Param("owner") String owner, @Param("repo") String repo, JsonValue contributor); @RequestLine("POST /repos/{owner}/{repo}/contributors") - void createRaw(@Param("owner") String owner, @Param("repo") String repo, Object contributor); + void createContributorWithObjectBody( + @Param("owner") String owner, @Param("repo") String repo, Object contributor); } private MockClient mockClient; @@ -82,13 +84,13 @@ void setUp() { } @Test - void decodesArray() { + void decodesJsonArray() { mockClient.ok( HttpMethod.GET, "/repos/openfeign/feign/contributors", "[{\"login\":\"velo\",\"contributions\":42},{\"login\":\"kdavisk6\",\"contributions\":7}]"); - JsonArray contributors = github.contributors("openfeign", "feign"); + JsonArray contributors = github.listContributors("openfeign", "feign"); assertThat(contributors.asList()).hasSize(2); assertThat(contributors.get(0).get("login").asString()).isEqualTo("velo"); @@ -96,78 +98,85 @@ void decodesArray() { } @Test - void decodesObject() { + void decodesJsonObject() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":\"feign\",\"stars\":9600}"); - JsonObject repo = github.repo("openfeign", "feign"); + JsonObject repo = github.getRepo("openfeign", "feign"); assertThat(repo.get("name").asString()).isEqualTo("feign"); assertThat(repo.get("stars").asLong()).isEqualTo(9600L); } @Test - void decodesAnyJsonValue() { + void decodesJsonValueReturnTypeAsJsonString() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "\"feign\""); - JsonValue value = github.repoValue("openfeign", "feign"); + JsonValue repoName = github.getRepoAsJsonValue("openfeign", "feign"); - assertThat(value).isInstanceOf(JsonString.class); - assertThat(value.asString()).isEqualTo("feign"); + assertThat(repoName).isInstanceOf(JsonString.class); + assertThat(repoName.asString()).isEqualTo("feign"); } @Test - void decodesRawString() { + void stringReturnTypeReturnsBodyVerbatim() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{ \"name\" : \"feign\" }"); - assertThat(github.repoRaw("openfeign", "feign")).isEqualTo("{ \"name\" : \"feign\" }"); + assertThat(github.getRepoAsRawJson("openfeign", "feign")).isEqualTo("{ \"name\" : \"feign\" }"); } @Test void notFoundDecodesToNull() { mockClient.add(HttpMethod.GET, "/repos/openfeign/feign", 404); - assertThat(github.repo("openfeign", "feign")).isNull(); + assertThat(github.getRepo("openfeign", "feign")).isNull(); + } + + @Test + void noContentDecodesToNull() { + mockClient.add(HttpMethod.GET, "/repos/openfeign/feign", 204, "{}"); + + assertThat(github.getRepo("openfeign", "feign")).isNull(); } @Test void emptyBodyDecodesToNull() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", ""); - assertThat(github.repo("openfeign", "feign")).isNull(); + assertThat(github.getRepo("openfeign", "feign")).isNull(); } @Test void malformedJsonFailsWithParseCause() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":"); - assertThatThrownBy(() -> github.repo("openfeign", "feign")) + assertThatThrownBy(() -> github.getRepo("openfeign", "feign")) .isInstanceOf(DecodeException.class) .hasCauseInstanceOf(JsonParseException.class); } @Test - void duplicateMemberNamesAreRejected() { + void duplicateMemberNamesFailWithParseCause() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":\"a\",\"name\":\"b\"}"); - assertThatThrownBy(() -> github.repo("openfeign", "feign")) + assertThatThrownBy(() -> github.getRepo("openfeign", "feign")) .isInstanceOf(DecodeException.class) .hasCauseInstanceOf(JsonParseException.class); } @Test - void mismatchedJsonTypeFails() { + void arrayBodyFailsToDecodeAsJsonObject() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "[1,2]"); - assertThatThrownBy(() -> github.repo("openfeign", "feign")) + assertThatThrownBy(() -> github.getRepo("openfeign", "feign")) .isInstanceOf(DecodeException.class) - .hasMessage("Expected JsonObject but got [1,2]"); + .hasMessage("Expected JsonObject but got a JSON array"); } @Test - void unsupportedReturnTypeFails() { + void unsupportedReturnTypeFailsToDecode() { mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{}"); - assertThatThrownBy(() -> github.repoMap("openfeign", "feign")) + assertThatThrownBy(() -> github.getRepoAsMap("openfeign", "feign")) .isInstanceOf(DecodeException.class) .hasMessageContaining("is not a type supported by this decoder"); } @@ -181,17 +190,29 @@ void encodesJsonValue() { "login", JsonString.of("velo"), "repos", JsonArray.of(List.of(JsonNumber.of(1), JsonNumber.of(2))))); - JsonObject created = github.create("openfeign", "feign", contributor); + JsonObject createdContributor = github.createContributor("openfeign", "feign", contributor); Request request = mockClient.verifyOne(HttpMethod.POST, "/repos/openfeign/feign/contributors"); assertThat(new String(request.body(), UTF_8)).isEqualTo(contributor.toString()); - assertThat(created.get("id").asInt()).isEqualTo(1); + assertThat(createdContributor.get("id").asInt()).isEqualTo(1); } @Test - void unsupportedBodyTypeFails() { - assertThatThrownBy(() -> github.createRaw("openfeign", "feign", Map.of("login", "velo"))) + void nonJsonValueBodyFailsToEncode() { + assertThatThrownBy( + () -> + github.createContributorWithObjectBody( + "openfeign", "feign", Map.of("login", "velo"))) .isInstanceOf(EncodeException.class) .hasMessageContaining("is not a type supported by this encoder"); } + + @Test + void nullBodyEncodesNothing() { + RequestTemplate template = new RequestTemplate(); + + new JdkJsonEncoder().encode(null, JsonValue.class, template); + + assertThat(template.body()).isNull(); + } } diff --git a/pom.xml b/pom.xml index d4277e68d8..3ca01fb46b 100644 --- a/pom.xml +++ b/pom.xml @@ -1009,7 +1009,7 @@ - jdk28 + jdk28-modules jdk-json diff --git a/scripts/release.sh b/scripts/release.sh index 7273050e5e..7fb51e3cc8 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -38,13 +38,13 @@ echo "release version is: ${tag} and next snapshot is: ${snapshot}" # Update the versions, removing the snapshots, then create a new tag for the release, this will # start the travis-ci release process. if [ -n "$1" ]; then - ./mvnw -B -Pjdk28 versions:set -DnewVersion="${tag}" -DgenerateBackupPoms=false + ./mvnw -B versions:set -DnewVersion="${tag}" -DgenerateBackupPoms=false fi -./mvnw -B -Pjdk28 versions:set license:format scm:checkin -DremoveSnapshot -DgenerateBackupPoms=false -Dmessage="prepare release ${tag}" -DpushChanges=false +./mvnw -B versions:set license:format scm:checkin -DremoveSnapshot -DgenerateBackupPoms=false -Dmessage="prepare release ${tag}" -DpushChanges=false # tag the release echo "pushing tag ${tag}" ./mvnw scm:tag # Update the versions to the next snapshot -./mvnw -B -Pjdk28 versions:set scm:checkin -DnewVersion="${snapshot}" -DgenerateBackupPoms=false -Dmessage="[ci skip] updating versions to next development iteration ${snapshot}" +./mvnw -B versions:set scm:checkin -DnewVersion="${snapshot}" -DgenerateBackupPoms=false -Dmessage="[ci skip] updating versions to next development iteration ${snapshot}"