Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 0 additions & 44 deletions .github/workflows/rc-adopter-gate.yml

This file was deleted.

16 changes: 8 additions & 8 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}

real-adopters:
external-integrations:
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 45
Expand All @@ -121,24 +121,24 @@ jobs:
distribution: temurin
java-version: "21"

- name: Verify two external adopters from Maven Central
- name: Verify external integrations from Maven Central
run: >-
bash scripts/verify-rc-adopters.sh
bash scripts/verify-rc-integrations.sh
"${{ needs.publish.outputs.version }}"
build/reports/rc1-adopters.md
build/reports/rc-integrations.md

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: rc1-adopter-gate
name: rc-external-integrations
path: |
build/reports/rc1-adopters.md
build/rc-adopters/*.log
build/reports/rc-integrations.md
build/rc-integrations/*.log
if-no-files-found: warn
retention-days: 30

github-prerelease:
needs: [publish, real-adopters]
needs: [publish, external-integrations]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
out/
.DS_Store
**/target/
__pycache__/
*.py[cod]
56 changes: 13 additions & 43 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,43 @@

### Added

* Added Maven publications for `codes-spring` and `codes-grpc-java`.
* Added adapter API snapshots and exact published dependency budgets.
* Added clean Gradle and Maven consumer checks that resolve all three artifacts from isolated publication repositories.
* Added application-owned Spring problem-type URI mappings.
* Added a thin failed-`Outcome` to Spring `ErrorResponseException` bridge.
* 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.
* Added ten-minute Spring and gRPC boundary examples.
* Added a reproducible release-candidate adopter gate against two pinned external codebases.
* Added a dedicated release-candidate workflow that publishes to Maven Central before running the external adopter gate.
* Added a final-release promotion gate that requires a successful same-line RC and allows only version substitution between the accepted RC and final release.
* Added `codes-spring` for mapping failed outcomes to Spring RFC 9457 `ProblemDetail`.
* Added application-owned problem-type URI mappings and explicit Spring exposure policies.
* Added conversion from failed outcomes to Spring `ErrorResponseException`.
* Added `codes-grpc-java` for mapping failed outcomes to `google.rpc.Status` with stable identity in `ErrorInfo`.
* Added conversion to gRPC `StatusRuntimeException` and structured request-field issues through `BadRequest` for `INVALID_ARGUMENT` and `OUT_OF_RANGE`.
* Added Spring and gRPC integration guides.

### Changed

* Set all publishable modules to `0.4.0-RC1`.
* Corrected Spring problem details so reusable outcome messages are titles for explicitly mapped problem types and occurrence details use RFC `detail`.
* Kept stable Codes identity in the Spring `code` extension for every mapped failure.
* Reworked the Spring orders reference to consume `codes-spring` instead of duplicating adapter behavior.
* Enforced lossless Codes identity compatibility with `google.rpc.ErrorInfo.domain` and `ErrorInfo.reason`.
* Restricted exposed `google.rpc.BadRequest` issues to `INVALID_ARGUMENT` and `OUT_OF_RANGE`, requiring request-field paths and rejecting incompatible representations.
* 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.
* Documented cases where Codes should not be introduced.
* Set all published artifacts to `0.4.0-RC1`.
* Spring problem details preserve Codes identity in the `code` extension, use the reusable outcome message as the title for explicitly mapped problem types, and keep occurrence detail in RFC `detail`.
* gRPC mappings reject lossy `ErrorInfo` identities and incompatible `BadRequest` issue representations instead of normalizing or coercing them.

## 0.3.1

### Changed

* Restored the published core to zero runtime dependencies.
* Kept JSpecify as a compile-time-only dependency for nullability metadata.
* Kept JSpecify as compile-time-only nullability metadata.
* Preserved Kotlin nullability semantics across the supported Kotlin compiler matrix.

### Added

* Added publication verification that prevents runtime dependencies from being introduced.
* Added verification that the published Maven POM remains dependency-free.

## 0.3.0

### Changed

* Reimplemented the published core in Java 17.
* Removed the Kotlin standard library from the core dependency graph.
* Added `org.jspecify:jspecify:1.0.0` as the core's only dependency for nullability annotations.
* Replaced Kotlin-specific ABI compatibility with a deterministic public Java API snapshot.
* Removed the Kotlin standard library from the core runtime dependency graph.
* Added JSpecify nullability metadata for Java and Kotlin consumers.
* Preserved the 17 standard outcomes, `OutcomeState` assignments, and built-in HTTP and gRPC mappings from `0.2.0`.

### Added

* Added Gradle and Maven consumer verification for Java and Kotlin.
* Added Kotlin compiler compatibility and JSpecify nullability contract verification.
* Added publication checks for the expected runtime and Maven dependency contract.

### Removed

* Removed Kotlin compiler-generated API such as `Companion`, default-argument bridges, and `OutcomeState.entries`.
* Removed the Kotlin `Outcome.toException` extension; use `OutcomeExceptions.toException(...)` or `new OutcomeException(...)`.

### Breaking

`0.3.0` is source and binary incompatible with `0.2.x` for callers that depend on the removed Kotlin-generated API or `Outcome.toException`.
The standard outcome identities, states, and built-in protocol mappings are unchanged from `0.2.0`.

Expand All @@ -85,10 +59,6 @@ The standard outcome identities, states, and built-in protocol mappings are unch

* Removed `StandardOutcomes.CREATED`, `ACCEPTED`, `NO_CONTENT`, and `PAYLOAD_TOO_LARGE`.

### Added

* Added semantic compatibility checks for standard outcome codes, states, and built-in mappings.

## 0.1.0

* Initial public release.
39 changes: 12 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
[![CI](https://github.com/aalsanie/codes/actions/workflows/ci.yml/badge.svg)](https://github.com/aalsanie/codes/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)

Codes provides stable application outcome identities and explicit boundary mappings for JVM applications. Applications keep their own domain result or error model and use Codes where multiple parts of a system need to agree on outcome meaning without coupling that meaning to HTTP, gRPC, serialization, or a framework.

A domain outcome can keep the same identity across boundaries:
Codes provides stable application outcome identities and explicit boundary mappings for JVM applications. Applications keep their existing domain result or error model; Codes gives the system one machine identity for an outcome while HTTP and gRPC remain boundary decisions.

```text
com.example.payments:PAYMENT_DECLINED
|
+-- HTTP 422
+-- gRPC FAILED_PRECONDITION
+-- logs/metrics keep PAYMENT_DECLINED
com.example.orders:ORDER_NOT_FOUND
-> HTTP 404
-> gRPC NOT_FOUND
-> metric label
-> log identity
-> test assertion
```

The same `OutcomeCode` can be used wherever application code, observability, and protocol boundaries need to agree on the outcome.

## Install

`0.4.0-RC1` is the current pre-release on Maven Central.

Core:

```kotlin
Expand Down Expand Up @@ -120,7 +123,7 @@ ValidationResult validation = ValidationResult.invalid(
Outcome outcome = validation.toOutcome(StandardOutcomes.INVALID_ARGUMENT);
```

`ValidationResult` is a small convenience for aggregating issues. It is not intended to replace an application's result, validation, or functional programming model.
`ValidationResult` is a small convenience for aggregating issues and converting them into an outcome.

## HTTP

Expand Down Expand Up @@ -158,29 +161,11 @@ check(outcome.code == StandardOutcomes.NOT_FOUND.code)
check(status?.value == 404)
```

## When not to use Codes

Do not add Codes only to standardize a single controller's error body. Framework-native errors are usually enough for a small application with one boundary.

Codes is also the wrong tool when:

* the application does not need a stable outcome identity outside one protocol boundary;
* you want a `Result`, `Either`, validation framework, exception hierarchy, or business workflow engine;
* you want Spring Boot auto-configuration, exception scanning, annotations, or hidden mapping conventions;
* an existing public error schema is fixed and migration cost is larger than the value of cross-boundary identity;
* you need protocol adapters beyond the ones Codes actually provides and do not want to own that adapter;
* you need a central outcome registry, governance service, code generator, or schema distribution system;
* the application has not yet decided which domain outcomes are stable enough to become machine identities.

Codes is useful when the identity itself matters independently of HTTP or gRPC. If that is not true, another abstraction is probably unnecessary.

## Reference

* [Semantic contract](docs/semantic-contract.md)
* [HTTP and gRPC mappings](docs/protocol-mappings.md)
* [Compatibility policy](docs/compatibility-policy.md)
* [Artifact contracts](docs/artifact-contracts.md)
* [RC1 real-adopter gate](docs/rc1-adopter-gate.md)

## License

Expand Down
29 changes: 0 additions & 29 deletions adoption/rc1/manifest.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@

@ExceptionHandler(value = {InvalidEventProcessingStatus.class})
--- /dev/null
+++ b/src/test/java/io/github/aalsanie/irp/CodesSpringAdoptionTest.java
+++ b/src/test/java/io/github/aalsanie/irp/CodesSpringIntegrationTest.java
@@ -0,0 +1,84 @@
+package io.github.aalsanie.irp;
+
Expand Down Expand Up @@ -99,7 +99,7 @@
+@SpringBootTest
+@AutoConfigureMockMvc
+@Import(TestcontainersConfiguration.class)
+class CodesSpringAdoptionTest {
+class CodesSpringIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
Expand All @@ -122,7 +122,7 @@
+ IntegrationConnection connection = connectionRepository.saveAndFlush(
+ new IntegrationConnection(
+ UUID.randomUUID(),
+ "pilot",
+ "integration-test",
+ "stripe",
+ ConnectionStatus.ACTIVE,
+ Instant.now()
Expand All @@ -131,10 +131,10 @@
+
+ String request = """
+ {
+ "externalEventId": "evt_rc1",
+ "externalEventId": "evt_codes",
+ "eventType": "payment.succeeded",
+ "payload": {
+ "paymentId": "pay_rc1"
+ "paymentId": "pay_codes"
+ }
+ }
+ """;
Expand Down
24 changes: 24 additions & 0 deletions compatibility/external/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"integrations": [
{
"id": "integration-reliability-platform",
"repository": "https://github.com/aalsanie/integration-reliability-platform.git",
"commit": "1f99ad8ec4b0b7da2a20ab20101a6adb677cb735",
"patch": "compatibility/external/integration-reliability-platform.patch",
"boundary": "Spring MVC",
"scope": "replace one handwritten duplicate-event HTTP mapping with codes-spring",
"dependency_notes": "The application uses Spring Boot 4.1.0 and owns Spring dependency management.",
"expected_identity": "io.github.aalsanie.codes.standard:ALREADY_EXISTS"
},
{
"id": "patient-mgmt-microservices",
"repository": "https://github.com/pratham2402/patient-mgmt-microservices.git",
"commit": "26645990986a4f17b755ec17ed390c9e90112d36",
"patch": "compatibility/external/patient-mgmt-microservices.patch",
"boundary": "Spring MVC + gRPC Java",
"scope": "exercise INVALID_ARGUMENT through Spring MVC and gRPC Java",
"dependency_notes": "The repository pins gRPC 1.69.0; the integration patch updates that line to the Codes 1.75.0 compatibility floor.",
"expected_identity": "io.github.aalsanie.codes.standard:INVALID_ARGUMENT"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(controllers = CodesValidationBoundaryTest.PilotController.class)
+@WebMvcTest(controllers = CodesValidationBoundaryTest.TestController.class)
+@Import(GlobalExceptionHandler.class)
+class CodesValidationBoundaryTest {
+
Expand All @@ -113,7 +113,7 @@
+ @Test
+ void validationKeepsCodesIdentityAndStructuredIssuesAtHttpBoundary()
+ throws Exception {
+ mockMvc.perform(post("/codes-pilot/validate")
+ mockMvc.perform(post("/codes-test/validate")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
Expand All @@ -130,13 +130,13 @@
+ }
+
+ @RestController
+ static class PilotController {
+ @PostMapping("/codes-pilot/validate")
+ void validate(@Valid @RequestBody PilotRequest request) {
+ static class TestController {
+ @PostMapping("/codes-test/validate")
+ void validate(@Valid @RequestBody TestRequest request) {
+ }
+ }
+
+ record PilotRequest(@NotBlank String email) {
+ record TestRequest(@NotBlank String email) {
+ }
+}
--- a/billing-service/pom.xml
Expand Down
Loading
Loading