diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c08074..0ad3cba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,22 +33,24 @@ jobs: - name: Verify core, adapters, and references run: ./gradlew clean verifyAll --stacktrace - - name: Publish all artifacts to Maven Local + - name: Publish clean compatibility artifacts + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven + + - name: Verify Gradle Java consumer run: >- ./gradlew - :publishToMavenLocal - :codes-spring:publishToMavenLocal - :codes-grpc-java:publishToMavenLocal + -p smoke-test-java + -Dmaven.repo.local=${{ github.workspace }}/build/compatibility-maven + clean check --stacktrace - - name: Verify local publication files - run: sh ./scripts/verify-local-publications.sh - - - name: Verify Gradle Java consumer - run: ./gradlew -p smoke-test-java clean check --stacktrace - - name: Verify Gradle Kotlin consumer - run: ./gradlew -p smoke-test-kotlin clean check --stacktrace + run: >- + ./gradlew + -p smoke-test-kotlin + -Dmaven.repo.local=${{ github.workspace }}/build/compatibility-maven + clean check + --stacktrace - name: Verify Maven Java consumer shell: bash @@ -56,24 +58,30 @@ jobs: set -euo pipefail version="$(sh ./scripts/version.sh)" mvn --batch-mode --no-transfer-progress \ + "-Dmaven.repo.local=${GITHUB_WORKSPACE}/build/compatibility-maven" \ -f smoke-test-maven-java/pom.xml \ - -Dcodes.version="$version" \ + "-Dcodes.version=$version" \ clean verify - name: Verify Maven Kotlin consumer + env: + MAVEN_REPO_LOCAL: ${{ github.workspace }}/build/compatibility-maven run: sh ./scripts/verify-kotlin-nullability.sh 2.4.10 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() with: name: coverage-reports - path: build/reports/jacoco + path: | + build/reports/jacoco + codes-spring/build/reports/jacoco + codes-grpc-java/build/reports/jacoco if-no-files-found: warn retention-days: 14 kotlin-consumers: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 25 strategy: fail-fast: false matrix: @@ -92,10 +100,12 @@ jobs: with: cache-provider: basic - - name: Publish core locally - run: ./gradlew :publishToMavenLocal --stacktrace + - name: Publish all artifacts locally + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven - name: Verify Kotlin consumer + env: + MAVEN_REPO_LOCAL: ${{ github.workspace }}/build/compatibility-maven run: sh ./scripts/verify-kotlin-nullability.sh "${{ matrix.kotlin }}" kotlin-consumers-gate: diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..94a260f --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,299 @@ +name: Compatibility + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: compatibility-${{ github.ref }} + cancel-in-progress: true + +jobs: + matrix: + name: Matrix policy + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + adapter: ${{ steps.matrix.outputs.adapter }} + kotlin: ${{ steps.matrix.outputs.kotlin }} + os: ${{ steps.matrix.outputs.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Verify matrix policy + run: python scripts/verify-compatibility-matrix.py + + - id: matrix + name: Export matrices + shell: bash + run: | + python - <<'PY' >> "$GITHUB_OUTPUT" + import json + from pathlib import Path + + matrix = json.loads(Path("compatibility/matrix.json").read_text()) + compact = lambda value: json.dumps(value, separators=(",", ":")) + + print("adapter=" + compact({ + "java": matrix["java"], + "spring": matrix["spring"], + "grpc": matrix["grpc"], + })) + kotlin = matrix["kotlin"] + print("kotlin=" + compact({ + "include": [ + { + "kotlin": version, + "spring": kotlin["spring"], + "grpc": kotlin["grpc"], + } + for version in kotlin["versions"] + ] + })) + print("os=" + compact({"os": matrix["os"]})) + PY + + contracts: + name: Artifact contracts and coverage + runs-on: ubuntu-latest + timeout-minutes: 35 + needs: [matrix] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 + with: + distribution: temurin + java-version: "17" + + - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 + with: + cache-provider: basic + + - name: Verify API, wire, POM, and coverage contracts + run: ./gradlew clean verifyAll --stacktrace + + - name: Build clean publication repository + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + if: always() + with: + name: compatibility-coverage + path: | + build/reports/jacoco + codes-spring/build/reports/jacoco + codes-grpc-java/build/reports/jacoco + if-no-files-found: error + retention-days: 14 + + adapter-versions: + name: JDK ${{ matrix.java }} / Spring ${{ matrix.spring }} / gRPC ${{ matrix.grpc }} + needs: [matrix, contracts] + runs-on: ubuntu-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix.outputs.adapter) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 + with: + cache-provider: basic + + - name: Publish clean artifacts + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven + + - name: Verify Gradle Java consumer + run: >- + ./gradlew + -p smoke-test-java + -Dmaven.repo.local=${{ github.workspace }}/build/compatibility-maven + -PconsumerJavaVersion=${{ matrix.java }} + -PspringFrameworkOverride=${{ matrix.spring }} + -PgrpcVersionOverride=${{ matrix.grpc }} + clean check + --stacktrace + + - name: Verify Maven Java consumer + shell: bash + run: | + version="$(sh scripts/version.sh)" + mvn --batch-mode --no-transfer-progress \ + "-Dmaven.repo.local=${GITHUB_WORKSPACE}/build/compatibility-maven" \ + -f smoke-test-maven-java/pom.xml \ + "-Dcodes.version=$version" \ + "-Dmaven.compiler.release=${{ matrix.java }}" \ + "-Dspring.framework.version=${{ matrix.spring }}" \ + "-Dgrpc.version=${{ matrix.grpc }}" \ + clean verify + + kotlin-nullability: + name: Kotlin ${{ matrix.kotlin }} / Spring ${{ matrix.spring }} / gRPC ${{ matrix.grpc }} JSpecify + needs: [matrix, contracts] + runs-on: ubuntu-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix.outputs.kotlin) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 + with: + distribution: temurin + java-version: "17" + + - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 + with: + cache-provider: basic + + - name: Publish clean artifacts + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven + + - name: Verify Kotlin JSpecify consumer + env: + MAVEN_REPO_LOCAL: ${{ github.workspace }}/build/compatibility-maven + run: >- + sh scripts/verify-kotlin-nullability.sh + "${{ matrix.kotlin }}" + "${{ matrix.spring }}" + "${{ matrix.grpc }}" + + operating-systems: + name: Published consumers / ${{ matrix.os }} + needs: [matrix, contracts] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix.outputs.os) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 + with: + distribution: temurin + java-version: "17" + + - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 + with: + cache-provider: basic + + - name: Publish clean artifacts (Unix) + if: runner.os != 'Windows' + shell: bash + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven + + - name: Publish clean artifacts (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $repo = Join-Path $env:GITHUB_WORKSPACE "build\compatibility-maven" + .\scripts\prepare-compatibility-repository.ps1 -Repository $repo + + - name: Verify Gradle Java and Kotlin consumers (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + repo="${GITHUB_WORKSPACE}/build/compatibility-maven" + ./gradlew -p smoke-test-java "-Dmaven.repo.local=$repo" clean check --stacktrace + ./gradlew -p smoke-test-kotlin "-Dmaven.repo.local=$repo" clean check --stacktrace + + - name: Verify Gradle Java and Kotlin consumers (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $repo = Join-Path $env:GITHUB_WORKSPACE "build\compatibility-maven" + + .\gradlew.bat -p smoke-test-java "-Dmaven.repo.local=$repo" clean check --stacktrace + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + .\gradlew.bat -p smoke-test-kotlin "-Dmaven.repo.local=$repo" clean check --stacktrace + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Verify Maven Java consumer (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + repo="${GITHUB_WORKSPACE}/build/compatibility-maven" + version="$(sh scripts/version.sh)" + mvn --batch-mode --no-transfer-progress \ + "-Dmaven.repo.local=$repo" \ + -f smoke-test-maven-java/pom.xml \ + "-Dcodes.version=$version" \ + clean verify + + - name: Verify Maven Java consumer (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $repo = Join-Path $env:GITHUB_WORKSPACE "build\compatibility-maven" + $version = (Get-Content .\gradle.properties | + Where-Object { $_ -like "VERSION_NAME=*" } | + Select-Object -First 1).Substring("VERSION_NAME=".Length) + mvn --batch-mode --no-transfer-progress "-Dmaven.repo.local=$repo" ` + -f smoke-test-maven-java/pom.xml ` + "-Dcodes.version=$version" ` + clean verify + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + compatibility-gate: + name: Compatibility gate + if: always() + needs: + - matrix + - contracts + - adapter-versions + - kotlin-nullability + - operating-systems + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Require complete compatibility matrix + env: + MATRIX_RESULT: ${{ needs.matrix.result }} + CONTRACTS_RESULT: ${{ needs.contracts.result }} + ADAPTER_RESULT: ${{ needs.adapter-versions.result }} + KOTLIN_RESULT: ${{ needs.kotlin-nullability.result }} + OS_RESULT: ${{ needs.operating-systems.result }} + shell: bash + run: | + set -euo pipefail + + for entry in \ + "matrix:$MATRIX_RESULT" \ + "contracts:$CONTRACTS_RESULT" \ + "adapter-versions:$ADAPTER_RESULT" \ + "kotlin-nullability:$KOTLIN_RESULT" \ + "operating-systems:$OS_RESULT" + do + name="${entry%%:*}" + result="${entry#*:}" + if [ "$result" != "success" ]; then + echo "$name result: $result" + exit 1 + fi + done + + echo "Complete Codes compatibility matrix passed from clean publication artifacts." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 068eb53..fd2b7f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,34 +52,41 @@ jobs: cache-provider: basic - name: Verify release tree - run: ./gradlew clean verifyAll --stacktrace + run: | + python scripts/verify-compatibility-matrix.py + ./gradlew clean verifyAll --stacktrace + + - name: Publish clean release-candidate artifacts + run: sh scripts/prepare-compatibility-repository.sh build/compatibility-maven - - name: Publish all artifacts to Maven Local + - name: Verify Gradle Java consumer run: >- ./gradlew - :publishToMavenLocal - :codes-spring:publishToMavenLocal - :codes-grpc-java:publishToMavenLocal + -p smoke-test-java + -Dmaven.repo.local=${{ github.workspace }}/build/compatibility-maven + clean check --stacktrace - - name: Verify local publication files - run: sh ./scripts/verify-local-publications.sh - - - name: Verify Gradle Java consumer - run: ./gradlew -p smoke-test-java clean check --stacktrace - - name: Verify Gradle Kotlin consumer - run: ./gradlew -p smoke-test-kotlin clean check --stacktrace + run: >- + ./gradlew + -p smoke-test-kotlin + -Dmaven.repo.local=${{ github.workspace }}/build/compatibility-maven + clean check + --stacktrace - name: Verify Maven consumers shell: bash + env: + MAVEN_REPO_LOCAL: ${{ github.workspace }}/build/compatibility-maven run: | set -euo pipefail version="$(sh ./scripts/version.sh)" mvn --batch-mode --no-transfer-progress \ + "-Dmaven.repo.local=${MAVEN_REPO_LOCAL}" \ -f smoke-test-maven-java/pom.xml \ - -Dcodes.version="$version" \ + "-Dcodes.version=$version" \ clean verify sh ./scripts/verify-kotlin-nullability.sh 2.4.10 diff --git a/CHANGELOG.md b/CHANGELOG.md index f519b6b..ebc4d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ * Added Spring RFC 9457 golden response contracts plus MVC and WebFlux compatibility checks. * Added decoded gRPC wire contracts covering safe, public, and explicitly exposed `google.rpc.Status` payloads. * Added gRPC `StatusRuntimeException` trailer round-trip verification. +* Added a production compatibility matrix for Spring 6/7, minimum/current gRPC, Java 17/21/25, Java/Kotlin consumers, Gradle/Maven, and Linux/Windows/macOS. +* Added adapter JSpecify consumer verification and adapter coverage gates. +* Added published-POM verification against clean isolated Maven repositories. ### Changed @@ -22,6 +25,7 @@ * Enforced lossless Codes identity compatibility with `google.rpc.ErrorInfo.domain` and `ErrorInfo.reason`. * Preserved exposed structured issues through `google.rpc.BadRequest` without normalizing coded issue identity. * Reworked the gRPC orders reference to consume `codes-grpc-java` instead of constructing rich error details manually. +* Moved publication consumer verification to freshly emptied Maven repositories so compatibility checks cannot be satisfied by stale local artifacts. ## 0.3.1 diff --git a/codes-grpc-java/build.gradle.kts b/codes-grpc-java/build.gradle.kts index cebf17c..9f855b8 100644 --- a/codes-grpc-java/build.gradle.kts +++ b/codes-grpc-java/build.gradle.kts @@ -1,5 +1,6 @@ plugins { `java-library` + jacoco id("com.vanniktech.maven.publish") } @@ -43,6 +44,7 @@ dependencies { tasks.test { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) systemProperty( "codes.apiSnapshot", rootProject.file("api/codes-grpc-java.api").absolutePath, @@ -53,6 +55,34 @@ tasks.test { ) } +jacoco { + toolVersion = "0.8.14" +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + html.required.set(true) + xml.required.set(true) + } +} + +tasks.jacocoTestCoverageVerification { + dependsOn(tasks.test) + violationRules { + rule { + limit { + counter = "LINE" + minimum = "0.90".toBigDecimal() + } + limit { + counter = "BRANCH" + minimum = "0.80".toBigDecimal() + } + } + } +} + sourceSets.test { java.srcDir(rootProject.file("testing/api-snapshot/src/main/java")) } @@ -105,6 +135,7 @@ tasks.register("verifyPublishedPomContract") { } tasks.check { + dependsOn(tasks.jacocoTestCoverageVerification) dependsOn(tasks.named("verifyPublishedPomContract")) } diff --git a/codes-spring/build.gradle.kts b/codes-spring/build.gradle.kts index 1d2b592..e2603a3 100644 --- a/codes-spring/build.gradle.kts +++ b/codes-spring/build.gradle.kts @@ -1,5 +1,6 @@ plugins { `java-library` + jacoco id("com.vanniktech.maven.publish") } @@ -44,6 +45,7 @@ dependencies { tasks.test { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) systemProperty( "codes.apiSnapshot", rootProject.file("api/codes-spring.api").absolutePath, @@ -54,6 +56,34 @@ tasks.test { ) } +jacoco { + toolVersion = "0.8.14" +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + html.required.set(true) + xml.required.set(true) + } +} + +tasks.jacocoTestCoverageVerification { + dependsOn(tasks.test) + violationRules { + rule { + limit { + counter = "LINE" + minimum = "0.90".toBigDecimal() + } + limit { + counter = "BRANCH" + minimum = "0.80".toBigDecimal() + } + } + } +} + sourceSets.test { java.srcDir(rootProject.file("testing/api-snapshot/src/main/java")) } @@ -106,6 +136,7 @@ tasks.register("verifyPublishedPomContract") { } tasks.check { + dependsOn(tasks.jacocoTestCoverageVerification) dependsOn(tasks.named("verifyPublishedPomContract")) } diff --git a/compatibility/README.md b/compatibility/README.md index cafb1e1..29fb9df 100644 --- a/compatibility/README.md +++ b/compatibility/README.md @@ -1,6 +1,6 @@ -# Semantic compatibility fixtures +# Compatibility contracts -These snapshots protect Codes' semantic and boundary contracts independently from the Java API snapshots under `api/`. +These fixtures and matrix definitions protect Codes' semantic, boundary, publication, and consumer contracts independently from the Java API snapshots under `api/`. They cover: @@ -8,7 +8,8 @@ They cover: * built-in HTTP mappings; * built-in gRPC mappings; * rendered Spring RFC 9457 HTTP problem responses; -* decoded `google.rpc.Status` payloads after `StatusRuntimeException` trailer round trips. +* decoded `google.rpc.Status` payloads after `StatusRuntimeException` trailer round trips; +* published-artifact compatibility across supported framework, JDK, language, build-tool, and operating-system combinations. Core semantic snapshots do not freeze human-readable messages, occurrence details, or issue paths. @@ -22,4 +23,33 @@ A change to that fixture is therefore a reviewed gRPC wire-contract change. A snapshot change represents a contract change and must be reviewed together with the code that requires it. +## Production compatibility matrix + +`compatibility/matrix.json` is the executable support policy for `0.4.x`. + +Current matrix: + +* Java: 17, 21, 25. +* Spring Framework: 6.0.0 and 7.0.9. +* gRPC Java: 1.75.0 and 1.83.1. +* Kotlin compiler consumers: 1.9.24, 2.0.21, 2.1.21, 2.2.20, 2.4.10. +* Kotlin/JSpecify compiler matrix framework baseline: Spring 6.0.0 and gRPC 1.75.0. +* Build tools: Gradle and Maven. +* Consumer languages: Java and Kotlin. +* Operating systems: Linux, Windows, macOS. + +Spring Framework 6.0.0 is the Codes `0.4.x` binary compatibility floor for the Spring 6 generation. Spring 7.0.9 is the current Spring 7 baseline used to build the adapter. This compatibility floor is not a statement about upstream maintenance or security support for old Spring releases. + +Codes explicitly supports gRPC Java 1.75.0 as its `0.4.x` compatibility floor and 1.83.1 as the current build baseline. The minimum is a Codes support policy, not an upstream gRPC LTS declaration. + +The Kotlin/JSpecify compiler matrix intentionally runs all supported Kotlin compiler versions against the minimum supported Spring and gRPC framework versions. This isolates Codes' published JSpecify metadata contract from the Kotlin metadata version used internally by the current Spring release. Current-framework Kotlin consumption remains covered separately by the Gradle Kotlin smoke consumer on the current Spring/gRPC baselines. Do not use Kotlin's metadata-version bypass flags to make an older compiler consume a newer framework's incompatible Kotlin metadata. + +Framework/JDK compatibility is exercised as a full Cartesian product: + +```text +3 Java versions × 2 Spring versions × 2 gRPC versions = 12 published-artifact jobs +``` + +Every compatibility job starts with an empty Maven repository, publishes `codes`, `codes-spring`, and `codes-grpc-java` into that repository, verifies the published POMs, and then runs an external consumer. Gradle consumer repositories exclude `io.github.aalsanie` from Maven Central so Codes artifacts cannot be accidentally resolved remotely. + For HTTP, absence from `http-mappings.snapshot` means `HttpOutcomeMapper.standard()` returns `Unmapped`. diff --git a/compatibility/matrix.json b/compatibility/matrix.json new file mode 100644 index 0000000..5d97255 --- /dev/null +++ b/compatibility/matrix.json @@ -0,0 +1,31 @@ +{ + "java": [ + "17", + "21", + "25" + ], + "spring": [ + "6.0.0", + "7.0.9" + ], + "grpc": [ + "1.75.0", + "1.83.1" + ], + "kotlin": { + "versions": [ + "1.9.24", + "2.0.21", + "2.1.21", + "2.2.20", + "2.4.10" + ], + "spring": "6.0.0", + "grpc": "1.75.0" + }, + "os": [ + "ubuntu-latest", + "windows-latest", + "macos-latest" + ] +} diff --git a/scripts/prepare-compatibility-repository.ps1 b/scripts/prepare-compatibility-repository.ps1 new file mode 100644 index 0000000..9d3965d --- /dev/null +++ b/scripts/prepare-compatibility-repository.ps1 @@ -0,0 +1,53 @@ +param( + [Parameter(Mandatory = $false)] + [string]$Repository = "build\compatibility-maven" +) + +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +if (-not [System.IO.Path]::IsPathRooted($Repository)) { + $Repository = Join-Path $Root $Repository +} +$Repository = [System.IO.Path]::GetFullPath($Repository) + +if (Test-Path $Repository) { + Remove-Item -Recurse -Force $Repository +} +New-Item -ItemType Directory -Force -Path $Repository | Out-Null + +& (Join-Path $Root "gradlew.bat") ` + "-Dmaven.repo.local=$Repository" ` + :publishToMavenLocal ` + :codes-spring:publishToMavenLocal ` + :codes-grpc-java:publishToMavenLocal ` + --stacktrace + +if ($LASTEXITCODE -ne 0) { + throw "Compatibility publication failed with exit code $LASTEXITCODE" +} + +$VersionLine = Get-Content (Join-Path $Root "gradle.properties") | + Where-Object { $_ -like "VERSION_NAME=*" } | + Select-Object -First 1 +$Version = $VersionLine.Substring("VERSION_NAME=".Length) + +foreach ($Artifact in @("codes", "codes-spring", "codes-grpc-java")) { + $ArtifactDir = Join-Path $Repository "io\github\aalsanie\$Artifact\$Version" + $Jar = Join-Path $ArtifactDir "$Artifact-$Version.jar" + $Pom = Join-Path $ArtifactDir "$Artifact-$Version.pom" + + if (-not (Test-Path $Jar) -or (Get-Item $Jar).Length -eq 0) { + throw "Missing compatibility publication JAR: $Jar" + } + if (-not (Test-Path $Pom) -or (Get-Item $Pom).Length -eq 0) { + throw "Missing compatibility publication POM: $Pom" + } +} + +python (Join-Path $Root "scripts\verify-published-pom-contracts.py") $Repository +if ($LASTEXITCODE -ne 0) { + throw "Published POM verification failed with exit code $LASTEXITCODE" +} + +Write-Output $Repository diff --git a/scripts/prepare-compatibility-repository.sh b/scripts/prepare-compatibility-repository.sh new file mode 100644 index 0000000..d3bb282 --- /dev/null +++ b/scripts/prepare-compatibility-repository.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +set -eu + +root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +repository="${1:?Usage: prepare-compatibility-repository.sh }" + +case "$repository" in + /*) ;; + *) repository="$root_dir/$repository" ;; +esac + +rm -rf "$repository" +mkdir -p "$repository" + +"$root_dir/gradlew" \ + "-Dmaven.repo.local=$repository" \ + :publishToMavenLocal \ + :codes-spring:publishToMavenLocal \ + :codes-grpc-java:publishToMavenLocal \ + --stacktrace + +MAVEN_REPO_LOCAL="$repository" sh "$root_dir/scripts/verify-local-publications.sh" +python "$root_dir/scripts/verify-published-pom-contracts.py" "$repository" + +printf '%s\n' "$repository" diff --git a/scripts/verify-compatibility-matrix.py b/scripts/verify-compatibility-matrix.py new file mode 100644 index 0000000..1f3698b --- /dev/null +++ b/scripts/verify-compatibility-matrix.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def properties(path: Path) -> dict[str, str]: + result: dict[str, str] = {} + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + key, value = line.split("=", 1) + result[key] = value + return result + + +matrix = json.loads((ROOT / "compatibility" / "matrix.json").read_text(encoding="utf-8")) +gradle = properties(ROOT / "gradle.properties") + +spring_floor = "6.0.0" +grpc_floor = "1.75.0" +kotlin_versions = ["1.9.24", "2.0.21", "2.1.21", "2.2.20", "2.4.10"] + +expected_axes = { + "java": ["17", "21", "25"], + "spring": [spring_floor, gradle["springFrameworkVersion"]], + "grpc": [grpc_floor, gradle["grpcVersion"]], + "kotlin": { + "versions": kotlin_versions, + "spring": spring_floor, + "grpc": grpc_floor, + }, + "os": ["ubuntu-latest", "windows-latest", "macos-latest"], +} + +if matrix != expected_axes: + raise SystemExit( + "compatibility/matrix.json drifted from the declared 0.4.x support policy.\n" + f"Expected: {expected_axes}\n" + f"Actual: {matrix}" + ) + +if matrix["kotlin"]["spring"] != matrix["spring"][0]: + raise SystemExit("Kotlin/JSpecify Spring version must be the declared Spring compatibility floor.") + +if matrix["kotlin"]["grpc"] != matrix["grpc"][0]: + raise SystemExit("Kotlin/JSpecify gRPC version must be the declared gRPC compatibility floor.") + +print("Compatibility matrix policy verified.") diff --git a/scripts/verify-kotlin-nullability.sh b/scripts/verify-kotlin-nullability.sh index 8442c72..b187f27 100644 --- a/scripts/verify-kotlin-nullability.sh +++ b/scripts/verify-kotlin-nullability.sh @@ -4,23 +4,68 @@ set -eu root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" pom="$root_dir/smoke-test-maven-kotlin/pom.xml" source_dir="$root_dir/smoke-test-maven-kotlin/src/main/kotlin" +matrix_file="$root_dir/compatibility/matrix.json" -kotlin_version="${1:?Usage: verify-kotlin-nullability.sh }" +kotlin_version="${1:?Usage: verify-kotlin-nullability.sh [spring-version] [grpc-version]}" codes_version="$(sh "$root_dir/scripts/version.sh")" +repository="${MAVEN_REPO_LOCAL:-$HOME/.m2/repository}" -nullable_probe="$source_dir/NullableReturnProbe.kt" -null_marked_probe="$source_dir/NullMarkedProbe.kt" +configured_versions="$(python - "$matrix_file" "$kotlin_version" <<'PY' +import json +import sys +from pathlib import Path + +matrix = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +kotlin_version = sys.argv[2] +policy = matrix["kotlin"] + +if kotlin_version not in policy["versions"]: + raise SystemExit(f"Unsupported Kotlin compatibility version: {kotlin_version}") + +print(policy["spring"]) +print(policy["grpc"]) +PY +)" + +configured_spring="$(printf '%s\n' "$configured_versions" | sed -n '1p')" +configured_grpc="$(printf '%s\n' "$configured_versions" | sed -n '2p')" + +spring_version="${2:-$configured_spring}" +grpc_version="${3:-$configured_grpc}" + +if [ "$spring_version" != "$configured_spring" ]; then + echo "Kotlin/JSpecify Spring version must match compatibility policy: $configured_spring" >&2 + exit 1 +fi + +if [ "$grpc_version" != "$configured_grpc" ]; then + echo "Kotlin/JSpecify gRPC version must match compatibility policy: $configured_grpc" >&2 + exit 1 +fi + +core_nullable_probe="$source_dir/CoreNullableReturnProbe.kt" +core_null_marked_probe="$source_dir/CoreNullMarkedProbe.kt" +spring_null_marked_probe="$source_dir/SpringNullMarkedProbe.kt" +grpc_null_marked_probe="$source_dir/GrpcNullMarkedProbe.kt" output_file="$(mktemp)" cleanup() { - rm -f "$nullable_probe" "$null_marked_probe" "$output_file" + rm -f \ + "$core_nullable_probe" \ + "$core_null_marked_probe" \ + "$spring_null_marked_probe" \ + "$grpc_null_marked_probe" \ + "$output_file" } trap cleanup EXIT HUP INT TERM mvn --batch-mode --no-transfer-progress \ + "-Dmaven.repo.local=$repository" \ -f "$pom" \ "-Dcodes.version=$codes_version" \ "-Dkotlin.version=$kotlin_version" \ + "-Dspring.framework.version=$spring_version" \ + "-Dgrpc.version=$grpc_version" \ clean verify expect_compile_failure() { @@ -29,9 +74,12 @@ expect_compile_failure() { set +e mvn --batch-mode --no-transfer-progress --offline \ + "-Dmaven.repo.local=$repository" \ -f "$pom" \ "-Dcodes.version=$codes_version" \ "-Dkotlin.version=$kotlin_version" \ + "-Dspring.framework.version=$spring_version" \ + "-Dgrpc.version=$grpc_version" \ clean compile >"$output_file" 2>&1 status=$? set -e @@ -51,27 +99,43 @@ expect_compile_failure() { rm -f "$probe_file" } -cat > "$nullable_probe" <<'EOF' +cat > "$core_nullable_probe" <<'EOF' import io.github.aalsanie.codes.Outcome import io.github.aalsanie.codes.StandardOutcomes -fun nullableReturnProbe() { +fun coreNullableReturnProbe() { val outcome = Outcome.of(StandardOutcomes.NOT_FOUND) val detail: String = outcome.detail println(detail) } EOF +expect_compile_failure "$core_nullable_probe" -expect_compile_failure "$nullable_probe" - -cat > "$null_marked_probe" <<'EOF' +cat > "$core_null_marked_probe" <<'EOF' import io.github.aalsanie.codes.Outcome -fun nullMarkedProbe() { +fun coreNullMarkedProbe() { Outcome.of(null) } EOF +expect_compile_failure "$core_null_marked_probe" + +cat > "$spring_null_marked_probe" <<'EOF' +import io.github.aalsanie.codes.spring.OutcomeProblemDetailMapper + +fun springNullMarkedProbe() { + OutcomeProblemDetailMapper.safeDefaults().map(null) +} +EOF +expect_compile_failure "$spring_null_marked_probe" + +cat > "$grpc_null_marked_probe" <<'EOF' +import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper -expect_compile_failure "$null_marked_probe" +fun grpcNullMarkedProbe() { + GoogleRpcOutcomeMapper.safeDefaults().map(null) +} +EOF +expect_compile_failure "$grpc_null_marked_probe" -echo "Kotlin $kotlin_version JSpecify nullability contract verified." +echo "Kotlin $kotlin_version JSpecify contracts verified for core, Spring, and gRPC against Spring $spring_version and gRPC $grpc_version." diff --git a/scripts/verify-published-pom-contracts.py b/scripts/verify-published-pom-contracts.py new file mode 100644 index 0000000..e425529 --- /dev/null +++ b/scripts/verify-published-pom-contracts.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +import argparse +import xml.etree.ElementTree as ET +from pathlib import Path + + +def properties(path: Path) -> dict[str, str]: + result: dict[str, str] = {} + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + key, value = line.split("=", 1) + result[key] = value + return result + + +parser = argparse.ArgumentParser() +parser.add_argument("repository", type=Path) +args = parser.parse_args() + +root = Path(__file__).resolve().parent.parent +props = properties(root / "gradle.properties") +version = props["VERSION_NAME"] +group_path = Path("io/github/aalsanie") + +expected = { + "codes": [], + "codes-spring": sorted([ + ("io.github.aalsanie", "codes", version, "compile"), + ("org.springframework", "spring-web", props["springFrameworkVersion"], "compile"), + ]), + "codes-grpc-java": sorted([ + ("io.github.aalsanie", "codes", version, "compile"), + ("com.google.api.grpc", "proto-google-common-protos", props["protoGoogleCommonProtosVersion"], "compile"), + ("io.grpc", "grpc-api", props["grpcVersion"], "compile"), + ("io.grpc", "grpc-protobuf", props["grpcVersion"], "runtime"), + ]), +} + +namespace = {"m": "http://maven.apache.org/POM/4.0.0"} + +for artifact, expected_dependencies in expected.items(): + pom = args.repository / group_path / artifact / version / f"{artifact}-{version}.pom" + jar = pom.with_suffix(".jar") + + if not pom.is_file() or pom.stat().st_size == 0: + raise SystemExit(f"Missing published POM: {pom}") + if not jar.is_file() or jar.stat().st_size == 0: + raise SystemExit(f"Missing published JAR: {jar}") + + document = ET.parse(pom) + actual = [] + for dependency in document.findall("./m:dependencies/m:dependency", namespace): + def value(name: str) -> str: + node = dependency.find(f"m:{name}", namespace) + return "" if node is None or node.text is None else node.text.strip() + + actual.append(( + value("groupId"), + value("artifactId"), + value("version"), + value("scope") or "compile", + )) + + actual.sort() + if actual != expected_dependencies: + raise SystemExit( + f"{artifact} published POM dependency budget changed.\n" + f"Expected: {expected_dependencies}\n" + f"Actual: {actual}" + ) + +print(f"Published POM contracts verified in {args.repository}.") diff --git a/smoke-test-java/build.gradle.kts b/smoke-test-java/build.gradle.kts index 6d63767..ea49585 100644 --- a/smoke-test-java/build.gradle.kts +++ b/smoke-test-java/build.gradle.kts @@ -9,20 +9,40 @@ val codesVersion = Properties().run { getProperty("VERSION_NAME") ?: error("VERSION_NAME is missing from gradle.properties") } +val consumerJavaVersion = providers.gradleProperty("consumerJavaVersion") + .orElse("17") + .map(String::toInt) + +val springFrameworkOverride = providers.gradleProperty("springFrameworkOverride").orNull +val grpcVersionOverride = providers.gradleProperty("grpcVersionOverride").orNull + dependencies { implementation("io.github.aalsanie:codes:$codesVersion") implementation("io.github.aalsanie:codes-spring:$codesVersion") implementation("io.github.aalsanie:codes-grpc-java:$codesVersion") } +configurations.configureEach { + resolutionStrategy.eachDependency { + if (springFrameworkOverride != null && requested.group == "org.springframework") { + useVersion(springFrameworkOverride) + because("Codes Spring compatibility matrix") + } + if (grpcVersionOverride != null && requested.group == "io.grpc") { + useVersion(grpcVersionOverride) + because("Codes gRPC compatibility matrix") + } + } +} + java { toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(consumerJavaVersion.map(JavaLanguageVersion::of)) } } tasks.withType().configureEach { - options.release.set(17) + options.release.set(consumerJavaVersion) options.compilerArgs.addAll(listOf("-Xlint:all,-serial", "-Werror")) } diff --git a/smoke-test-java/settings.gradle.kts b/smoke-test-java/settings.gradle.kts index 5aae76d..5441704 100644 --- a/smoke-test-java/settings.gradle.kts +++ b/smoke-test-java/settings.gradle.kts @@ -1,3 +1,23 @@ -pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } } -dependencyResolutionManagement { repositories { mavenLocal(); mavenCentral() } } +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + mavenLocal { + content { + includeGroup("io.github.aalsanie") + } + } + mavenCentral { + content { + excludeGroup("io.github.aalsanie") + } + } + } +} + rootProject.name = "codes-smoke-java" diff --git a/smoke-test-java/src/main/java/SmokeJava.java b/smoke-test-java/src/main/java/SmokeJava.java index 00310bc..c7cf68c 100644 --- a/smoke-test-java/src/main/java/SmokeJava.java +++ b/smoke-test-java/src/main/java/SmokeJava.java @@ -1,3 +1,4 @@ +import com.google.rpc.ErrorInfo; import io.github.aalsanie.codes.Issue; import io.github.aalsanie.codes.MappingResult; import io.github.aalsanie.codes.Outcome; @@ -7,35 +8,75 @@ import io.github.aalsanie.codes.OutcomeState; import io.github.aalsanie.codes.StandardOutcomes; import io.github.aalsanie.codes.ValidationResult; +import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper; +import io.github.aalsanie.codes.grpc.GrpcOutcomeExceptions; import io.github.aalsanie.codes.protocol.grpc.GrpcOutcomeMapper; import io.github.aalsanie.codes.protocol.grpc.GrpcStatusCode; import io.github.aalsanie.codes.protocol.http.HttpOutcomeMapper; import io.github.aalsanie.codes.protocol.http.HttpStatusCode; -import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper; import io.github.aalsanie.codes.spring.OutcomeProblemDetailMapper; +import io.github.aalsanie.codes.spring.SpringOutcomeExceptions; +import io.grpc.StatusRuntimeException; import java.util.List; import org.springframework.http.ProblemDetail; +import org.springframework.web.ErrorResponseException; public final class SmokeJava { private SmokeJava() { } - public static void main(String[] args) { + public static void main(String[] args) throws Exception { OutcomeCode code = OutcomeCode.of("com.example.orders", "ORDER_REJECTED"); - OutcomeDefinition rejected = OutcomeDefinition.custom(code, OutcomeState.FAILED, "Order rejected."); - Outcome outcome = Outcome.of(rejected, "internal=42", List.of(Issue.at("items[0]", "Unavailable."))); + OutcomeDefinition rejected = OutcomeDefinition.custom( + code, + OutcomeState.FAILED, + "Order rejected." + ); + Outcome outcome = Outcome.of( + rejected, + "internal=42", + List.of(Issue.at("items[0]", "Unavailable.")) + ); - HttpOutcomeMapper http = HttpOutcomeMapper.standard().withMapping(rejected, HttpStatusCode.of(422)); - GrpcOutcomeMapper grpc = GrpcOutcomeMapper.standard().withMapping(rejected, GrpcStatusCode.FAILED_PRECONDITION); + HttpOutcomeMapper http = HttpOutcomeMapper.standard() + .withMapping(rejected, HttpStatusCode.of(422)); + GrpcOutcomeMapper grpc = GrpcOutcomeMapper.standard() + .withMapping(rejected, GrpcStatusCode.FAILED_PRECONDITION); require(http.map(outcome).orNull().getValue() == 422); require(grpc.map(outcome).orNull() == GrpcStatusCode.FAILED_PRECONDITION); Outcome standardFailure = Outcome.of(StandardOutcomes.NOT_FOUND); - ProblemDetail problem = OutcomeProblemDetailMapper.safeDefaults().map(standardFailure).orNull(); + + ProblemDetail problem = OutcomeProblemDetailMapper.safeDefaults() + .map(standardFailure) + .orNull(); require(problem.getStatus() == 404); - com.google.rpc.Status rpcStatus = GoogleRpcOutcomeMapper.safeDefaults().map(standardFailure).orNull(); + + ErrorResponseException springException = SpringOutcomeExceptions + .toErrorResponseException( + standardFailure, + OutcomeProblemDetailMapper.safeDefaults() + ) + .orNull(); + require(springException.getStatusCode().value() == 404); + + com.google.rpc.Status rpcStatus = GoogleRpcOutcomeMapper.safeDefaults() + .map(standardFailure) + .orNull(); require(rpcStatus.getCode() == io.grpc.Status.Code.NOT_FOUND.value()); + ErrorInfo identity = rpcStatus.getDetails(0).unpack(ErrorInfo.class); + require(identity.getDomain().equals(standardFailure.getCode().getNamespace())); + require(identity.getReason().equals(standardFailure.getCode().getName())); + + StatusRuntimeException grpcException = GrpcOutcomeExceptions + .toStatusRuntimeException( + standardFailure, + GoogleRpcOutcomeMapper.safeDefaults() + ) + .orNull(); + require(grpcException.getStatus().getCode() == io.grpc.Status.Code.NOT_FOUND); + ValidationResult validation = ValidationResult.combine( ValidationResult.valid(), ValidationResult.invalid(Issue.at("email", "Invalid.")) diff --git a/smoke-test-kotlin/build.gradle.kts b/smoke-test-kotlin/build.gradle.kts index 49ae8a9..e274015 100644 --- a/smoke-test-kotlin/build.gradle.kts +++ b/smoke-test-kotlin/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Properties +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { kotlin("jvm") version "2.4.10" @@ -9,12 +10,22 @@ val codesVersion = Properties().run { getProperty("VERSION_NAME") ?: error("VERSION_NAME is missing from gradle.properties") } +val consumerJavaVersion = providers.gradleProperty("consumerJavaVersion") + .orElse("17") + .map(String::toInt) + dependencies { implementation("io.github.aalsanie:codes:$codesVersion") + implementation("io.github.aalsanie:codes-spring:$codesVersion") + implementation("io.github.aalsanie:codes-grpc-java:$codesVersion") } kotlin { - jvmToolchain(17) + jvmToolchain(consumerJavaVersion.get()) + compilerOptions { + jvmTarget.set(consumerJavaVersion.map { JvmTarget.fromTarget(it.toString()) }) + freeCompilerArgs.add("-Xjspecify-annotations=strict") + } } val kotlinSmoke by tasks.registering(JavaExec::class) { diff --git a/smoke-test-kotlin/settings.gradle.kts b/smoke-test-kotlin/settings.gradle.kts index 3ed03c5..0e716dc 100644 --- a/smoke-test-kotlin/settings.gradle.kts +++ b/smoke-test-kotlin/settings.gradle.kts @@ -1,3 +1,23 @@ -pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } } -dependencyResolutionManagement { repositories { mavenLocal(); mavenCentral() } } +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + mavenLocal { + content { + includeGroup("io.github.aalsanie") + } + } + mavenCentral { + content { + excludeGroup("io.github.aalsanie") + } + } + } +} + rootProject.name = "codes-smoke-kotlin" diff --git a/smoke-test-kotlin/src/main/kotlin/SmokeKotlin.kt b/smoke-test-kotlin/src/main/kotlin/SmokeKotlin.kt index 4f9a9c8..318323b 100644 --- a/smoke-test-kotlin/src/main/kotlin/SmokeKotlin.kt +++ b/smoke-test-kotlin/src/main/kotlin/SmokeKotlin.kt @@ -1,3 +1,4 @@ +import com.google.rpc.ErrorInfo import io.github.aalsanie.codes.Issue import io.github.aalsanie.codes.Outcome import io.github.aalsanie.codes.OutcomeCode @@ -6,8 +7,10 @@ import io.github.aalsanie.codes.OutcomeRegistry import io.github.aalsanie.codes.OutcomeState import io.github.aalsanie.codes.StandardOutcomes import io.github.aalsanie.codes.ValidationResult +import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper import io.github.aalsanie.codes.protocol.http.HttpOutcomeMapper import io.github.aalsanie.codes.protocol.http.HttpStatusCode +import io.github.aalsanie.codes.spring.OutcomeProblemDetailMapper fun main() { val rejected = OutcomeDefinition.custom( @@ -16,7 +19,11 @@ fun main() { OutcomeState.FAILED, "Order rejected.", ) - val outcome = Outcome.of(rejected, "internal=42", listOf(Issue.at("items[0]", "Unavailable."))) + val outcome = Outcome.of( + rejected, + "internal=42", + listOf(Issue.at("items[0]", "Unavailable.")), + ) val detail: String? = outcome.detail val issueCode: OutcomeCode? = outcome.issues.first().code val mapper = HttpOutcomeMapper.standard().withMapping(rejected, HttpStatusCode.of(422)) @@ -29,6 +36,16 @@ fun main() { check(missingDefinition == null) check(outcome.code.value == "com.example.orders:ORDER_REJECTED") + val standardFailure = Outcome.of(StandardOutcomes.NOT_FOUND) + val problem = OutcomeProblemDetailMapper.safeDefaults().map(standardFailure).orNull() + check(problem?.status == 404) + + val rpcStatus = GoogleRpcOutcomeMapper.safeDefaults().map(standardFailure).orNull() + check(rpcStatus?.code == io.grpc.Status.Code.NOT_FOUND.value()) + val identity = rpcStatus!!.detailsList.first().unpack(ErrorInfo::class.java) + check(identity.domain == standardFailure.code.namespace) + check(identity.reason == standardFailure.code.name) + val validation = ValidationResult.combine( ValidationResult.valid(), ValidationResult.invalid(Issue.at("email", "Invalid.")), @@ -36,5 +53,5 @@ fun main() { check(validation.toOutcome(StandardOutcomes.INVALID_ARGUMENT).issues.size == 1) check(OutcomeRegistry.standard().with(rejected).find(rejected.code) === rejected) - println("Kotlin consumer smoke test passed.") + println("Kotlin consumer smoke test passed for all Codes artifacts.") } diff --git a/smoke-test-maven-java/pom.xml b/smoke-test-maven-java/pom.xml index d257c4e..da35d16 100644 --- a/smoke-test-maven-java/pom.xml +++ b/smoke-test-maven-java/pom.xml @@ -6,11 +6,34 @@ io.github.aalsanie.smoke codes-maven-java-smoke 1.0.0 + 17 UTF-8 0.4.0-SNAPSHOT + 7.0.9 + 1.83.1 + + + + + org.springframework + spring-framework-bom + ${spring.framework.version} + pom + import + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + io.github.aalsanie @@ -28,6 +51,7 @@ ${codes.version} + @@ -35,7 +59,7 @@ maven-compiler-plugin 3.14.1 - 17 + ${maven.compiler.release} -Xlint:all,-serial -Werror diff --git a/smoke-test-maven-kotlin/pom.xml b/smoke-test-maven-kotlin/pom.xml index 98347ec..a32afb7 100644 --- a/smoke-test-maven-kotlin/pom.xml +++ b/smoke-test-maven-kotlin/pom.xml @@ -6,23 +6,57 @@ io.github.aalsanie.smoke codes-maven-kotlin-smoke 1.0.0 + UTF-8 0.4.0-SNAPSHOT 2.4.10 + 6.0.0 + 1.75.0 + + + + + org.springframework + spring-framework-bom + ${spring.framework.version} + pom + import + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + io.github.aalsanie codes ${codes.version} + + io.github.aalsanie + codes-spring + ${codes.version} + + + io.github.aalsanie + codes-grpc-java + ${codes.version} + org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} + ${project.basedir}/src/main/kotlin @@ -35,12 +69,12 @@ compile compile compile - - 17 - - -Xjspecify-annotations=strict - - + + 17 + + -Xjspecify-annotations=strict + + diff --git a/smoke-test-maven-kotlin/src/main/kotlin/SmokeMavenKotlin.kt b/smoke-test-maven-kotlin/src/main/kotlin/SmokeMavenKotlin.kt index 3fb5e26..610c8fb 100644 --- a/smoke-test-maven-kotlin/src/main/kotlin/SmokeMavenKotlin.kt +++ b/smoke-test-maven-kotlin/src/main/kotlin/SmokeMavenKotlin.kt @@ -1,8 +1,22 @@ +import com.google.rpc.ErrorInfo import io.github.aalsanie.codes.Outcome import io.github.aalsanie.codes.StandardOutcomes +import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper import io.github.aalsanie.codes.protocol.http.HttpOutcomeMapper +import io.github.aalsanie.codes.spring.OutcomeProblemDetailMapper fun main() { - check(HttpOutcomeMapper.standard().map(Outcome.of(StandardOutcomes.NOT_FOUND)).orNull()?.value == 404) - println("Maven Kotlin consumer smoke test passed.") + val outcome = Outcome.of(StandardOutcomes.NOT_FOUND) + + check(HttpOutcomeMapper.standard().map(outcome).orNull()?.value == 404) + check(OutcomeProblemDetailMapper.safeDefaults().map(outcome).orNull()?.status == 404) + + val rpcStatus = GoogleRpcOutcomeMapper.safeDefaults().map(outcome).orNull() + check(rpcStatus?.code == io.grpc.Status.Code.NOT_FOUND.value()) + + val identity = rpcStatus!!.detailsList.first().unpack(ErrorInfo::class.java) + check(identity.domain == outcome.code.namespace) + check(identity.reason == outcome.code.name) + + println("Maven Kotlin consumer smoke test passed for all Codes artifacts.") }