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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ graph TD

Each built-in code lives on its own type's companion object (e.g. `Succeeded.CREATED`, `Restricted.DENIED`), not on `Codes` — this keeps autocomplete scoped, typing `Restricted.` shows only `Restricted`'s own members. `Codes` is just the aggregate list + lookup layer over those instances; using it, or the codes at all, is optional — you can construct any `Passed`/`Failed` subtype directly for domain-specific outcomes.

Some examples: `SUCCESS`, `CREATED`, `NOT_FOUND`, `CONFLICT`, `RATE_LIMITED`, `UNEXPECTED` — see [`Codes.kt`](kiit-codes/src/commonMain/kotlin/kiit/codes/Codes.kt) and [`Status.kt`](kiit-codes/src/commonMain/kotlin/kiit/codes/Status.kt) for the full registry (54 codes across 8 categories).
Some examples: `SUCCESS`, `CREATED`, `NOT_FOUND`, `CONFLICT`, `RATE_LIMITED`, `UNEXPECTED` — see [`Codes.kt`](kiit-codes/src/commonMain/kotlin/kiit/codes/Codes.kt) and [`Status.kt`](kiit-codes/src/commonMain/kotlin/kiit/codes/Status.kt) for the full registry (57 codes across 8 categories).

A few pairs worth distinguishing on sight:

Expand Down Expand Up @@ -270,7 +270,7 @@ grpc.toCode(Restricted.DENIED) // 7
grpc.toStatus(9)?.name // "PRECONDITION_FAILED" — deterministic canonical winner for that code
```

One gap worth knowing: gRPC's `ABORTED` (10) has no dedicated `Status`, so `toStatus(10)` returns `null` — callers need to handle that explicitly.
Every gRPC code (0-16) resolves to a `Status` — the one gap (`ABORTED`) closed once `Unserved.ABORTED` was added.

## 🧾 Err & Checked

Expand Down
57 changes: 33 additions & 24 deletions assets/kiit-codes.drawio

Large diffs are not rendered by default.

Binary file modified assets/kiit-codes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions kiit-codes/src/commonMain/kotlin/kiit/codes/Codes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ object Codes {
Rejected.PRECONDITION_FAILED, Rejected.EXPIRED, Rejected.GONE,
Unserved.UNEXPECTED, Unserved.UNSUPPORTED, Unserved.TIMEOUT, Unserved.RATE_LIMITED,
Unserved.RESOURCE_LIMITED, Unserved.UNREACHABLE, Unserved.UNDER_MAINTENANCE,
Unserved.INTERNAL, Unserved.DATA_LOSS,
Unserved.INTERNAL, Unserved.DATA_LOSS, Unserved.DEGRADED, Unserved.LEGAL_BLOCK, Unserved.ABORTED,
)

private val byId: Map<String, Status> = all.associateBy { it.id }
Expand Down Expand Up @@ -152,6 +152,7 @@ open class CodesToHttp(
// same axis as RATE_LIMITED — HTTP doesn't distinguish the two
Unserved.RESOURCE_LIMITED.id to 429,
Unserved.UNEXPECTED.id to 500,
Unserved.LEGAL_BLOCK.id to 451,
)

/**
Expand Down Expand Up @@ -181,7 +182,7 @@ open class CodesToHttp(
* Category -> gRPC default: Passed (all) -> 0 (OK) Restricted -> 7 (PERMISSION_DENIED)
* Invalid -> 3 (INVALID_ARGUMENT) Rejected -> 9 (FAILED_PRECONDITION) Unserved -> 13 (INTERNAL)
*
* gRPC's `ABORTED` (10) has no dedicated [Status]; [toStatus] returns null for it.
* gRPC's `ABORTED` (10) maps to [Unserved.ABORTED] — previously an honest `null` gap, now closed.
*/
open class CodesToGrpc(
private val overrides: Map<String, Int> = DEFAULT_OVERRIDES,
Expand Down Expand Up @@ -234,6 +235,10 @@ open class CodesToGrpc(
Unserved.DATA_LOSS.id to 15,
// RESOURCE_EXHAUSTED — widely used real-world convention, not an official mapping
Invalid.PAYLOAD_TOO_LARGE.id to 8,
// exact match, closes the previously honest null gap at 10
Unserved.ABORTED.id to 10,
// DEGRADED and LEGAL_BLOCK have no closer gRPC equivalent — fall through to
// Unserved's own category default (13, INTERNAL)
)

/** One canonical winner per gRPC code with more than one resolving [Status] — see [toStatus]. */
Expand Down
75 changes: 59 additions & 16 deletions kiit-codes/src/commonMain/kotlin/kiit/codes/Status.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,19 @@ sealed class Passed : Status {
is Information -> "Information"
}

/** Operation's primary purpose completed (e.g. a value was created, fetched, updated). */
/** Runtime-accessible version of each category's meaning — see the subtypes' own KDoc for detail. */
val groupDescription: String
get() =
when (this) {
is Succeeded -> "The operation's primary purpose was completed successfully."
is Pending -> "The operation was accepted, but has not yet fully resolved."
is Excluded ->
"The item was left out of the operation's normal output, never processed, " +
"processed and discarded, or excluded for another reason."
is Information -> "An informational or metadata response; no primary operation was performed."
}

/** See [Passed.groupDescription] for this category's definition. */
data class Succeeded(
override val name: String,
override val message: String,
Expand Down Expand Up @@ -152,7 +164,7 @@ sealed class Passed : Status {
}
}

/** Operation accepted but not yet fully processed (e.g. queued, waiting, confirmed). */
/** See [Passed.groupDescription] for this category's definition. */
data class Pending(
override val name: String,
override val message: String,
Expand Down Expand Up @@ -194,10 +206,10 @@ sealed class Passed : Status {
}

/**
* Item was excluded from the operation's normal output — not processed at all (e.g.
* SKIPPED), processed then discarded (e.g. DISCARDED), or omitted for any other reason.
* The distinction is carried by [name], not by separate types — see [Excluded.SKIPPED]
* and [Excluded.DISCARDED].
* See [Passed.groupDescription] for this category's definition.
*
* The distinction between e.g. [Excluded.SKIPPED] and [Excluded.DISCARDED] is carried by
* [name], not by separate types.
*/
data class Excluded(
override val name: String,
Expand Down Expand Up @@ -234,10 +246,7 @@ sealed class Passed : Status {
}
}

/**
* Informational / metadata response — no primary operation was performed.
* E.g. HELP, ABOUT, VERSION output from a CLI command.
*/
/** See [Passed.groupDescription] for this category's definition. */
data class Information(
override val name: String,
override val message: String,
Expand Down Expand Up @@ -288,7 +297,21 @@ sealed class Failed : Status {
is Unserved -> "Unserved"
}

/** Security / access-control failure — the caller is not permitted to perform this action. */
/** Runtime-accessible version of each category's meaning — see the subtypes' own KDoc for detail. */
val groupDescription: String
get() =
when (this) {
is Restricted -> "An access-control failure — the caller is not permitted to perform this action."
is Invalid ->
"The request as given cannot be satisfied — malformed input, invalid values, " +
"or an unknown route."
is Rejected -> "A known, expected business-rule failure — understood and permitted, but refused."
is Unserved ->
"The request is valid and permitted, but cannot be serviced right now, " +
"for reasons unrelated to what was sent."
}

/** See [Failed.groupDescription] for this category's definition. */
data class Restricted(
override val name: String,
override val message: String,
Expand Down Expand Up @@ -321,7 +344,7 @@ sealed class Failed : Status {
}
}

/** The request as given cannot be satisfied — malformed input, invalid values, or not found. */
/** See [Failed.groupDescription] for this category's definition. */
data class Invalid(
override val name: String,
override val message: String,
Expand Down Expand Up @@ -359,7 +382,7 @@ sealed class Failed : Status {
}
}

/** A known, expected business-rule failure — understood and handled by the caller. */
/** See [Failed.groupDescription] for this category's definition. */
data class Rejected(
override val name: String,
override val message: String,
Expand Down Expand Up @@ -402,9 +425,11 @@ sealed class Failed : Status {
}

/**
* The request is valid and permitted, but cannot be serviced right now for reasons unrelated
* to what was sent — capacity, timeout, an unsupported capability, planned
* maintenance, or a genuinely unexpected/unhandled failure (see [Unserved.UNEXPECTED]).
* See [Failed.groupDescription] for this category's definition.
*
* E.g. capacity, timeout, an unsupported capability, planned maintenance, a degraded or
* aborted dependency, a legal/regulatory block, or a genuinely unexpected/unhandled failure
* (see [Unserved.UNEXPECTED]).
*/
data class Unserved(
override val name: String,
Expand Down Expand Up @@ -442,6 +467,24 @@ sealed class Failed : Status {
)
val DATA_LOSS =
Unserved("DATA_LOSS", "Unrecoverable data loss or corruption occurred.", origin = StatusConstants.KIIT)
val DEGRADED =
Unserved(
"DEGRADED",
"This dependency is degraded; some calls may be refused.",
origin = StatusConstants.KIIT,
)
val LEGAL_BLOCK =
Unserved(
"LEGAL_BLOCK",
"Access is restricted due to legal or regulatory requirements.",
origin = StatusConstants.KIIT,
)
val ABORTED =
Unserved(
"ABORTED",
"The operation was aborted before it could complete; retrying may succeed.",
origin = StatusConstants.KIIT,
)
}
}
}
Expand Down
34 changes: 31 additions & 3 deletions kiit-codes/src/commonTest/kotlin/kiit/codes/CodesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ class CodesTest {
assertTrue(Excluded.DISQUALIFIED is Passed.Excluded)
}

@Test
fun degradedLegalBlockAndAbortedAreUnserved() {
assertFalse(Unserved.DEGRADED.success)
assertFalse(Unserved.LEGAL_BLOCK.success)
assertFalse(Unserved.ABORTED.success)
assertTrue(Unserved.DEGRADED is Failed.Unserved)
assertTrue(Unserved.LEGAL_BLOCK is Failed.Unserved)
assertTrue(Unserved.ABORTED is Failed.Unserved)
}

@Test
fun everyBuiltInCodeHasKiitOrigin() {
assertTrue(Codes.all.all { it.origin == StatusConstants.KIIT })
Expand Down Expand Up @@ -222,6 +232,16 @@ class CodesToHttpTest {
assertEquals(500, http.toCode(Unserved.UNEXPECTED))
}

@Test fun overrideLegalBlock() {
assertEquals(451, http.toCode(Unserved.LEGAL_BLOCK))
}

@Test fun categoryDefaultCoversDegradedAndAborted() {
// Neither has a closer HTTP equivalent — deliberately fall through to Unserved's default.
assertEquals(503, http.toCode(Unserved.DEGRADED))
assertEquals(503, http.toCode(Unserved.ABORTED))
}

/**
* A custom, unregistered status still resolves via its category's default rather than a
* guessed/literal fallback.
Expand Down Expand Up @@ -396,6 +416,9 @@ class CodesToGrpcTest {
@Test fun categoryDefaultUnservedIsInternal() {
// UNSUPPORTED is overridden to 12 (see overrideUnsupported) and no longer falls here.
assertEquals(13, grpc.toCode(Unserved.UNDER_MAINTENANCE))
// No closer gRPC equivalent for either — deliberately fall through to the category default.
assertEquals(13, grpc.toCode(Unserved.DEGRADED))
assertEquals(13, grpc.toCode(Unserved.LEGAL_BLOCK))
}

@Test fun overrideCancelled() {
Expand Down Expand Up @@ -443,6 +466,11 @@ class CodesToGrpcTest {
assertEquals(15, grpc.toCode(Unserved.DATA_LOSS))
}

/** Exact match — closes what used to be an honest null gap at gRPC's own ABORTED (10). */
@Test fun overrideAborted() {
assertEquals(10, grpc.toCode(Unserved.ABORTED))
}

/** Not an official gRPC mapping, shares RESOURCE_EXHAUSTED (8) with RATE_LIMITED deliberately. */
@Test
fun overridePayloadTooLargeSharesResourceExhausted() {
Expand Down Expand Up @@ -510,10 +538,10 @@ class CodesToGrpcTest {
assertSame(Unserved.UNSUPPORTED, grpc.toStatus(12))
}

/** ABORTED (10) has no dedicated Status and nothing falls through to it by default; null. */
/** ABORTED now maps directly to gRPC 10, closing what used to be an honest null gap. */
@Test
fun toStatus10ReturnsNullSinceAbortedHasNoDedicatedCode() {
assertNull(grpc.toStatus(10))
fun toStatus10ResolvesToAborted() {
assertSame(Unserved.ABORTED, grpc.toStatus(10))
}

@Test
Expand Down
26 changes: 26 additions & 0 deletions kiit-codes/src/commonTest/kotlin/kiit/codes/StatusTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ class StatusTest {
assertEquals("Unserved", Failed.Unserved("U", "U").group)
}

// -------------------------------------------------------------------------
// groupDescription — runtime-accessible version of each category's meaning
// -------------------------------------------------------------------------

@Test
fun groupDescriptionIsNonBlankAndDistinctForAllSubtypes() {
val descriptions =
listOf(
Passed.Succeeded("S", "S").groupDescription,
Passed.Pending("P", "P").groupDescription,
Passed.Excluded("F", "F").groupDescription,
Passed.Information("N", "N").groupDescription,
Failed.Restricted("R", "R").groupDescription,
Failed.Invalid("I", "I").groupDescription,
Failed.Rejected("E", "E").groupDescription,
Failed.Unserved("U", "U").groupDescription,
)
assertTrue(descriptions.all { it.isNotBlank() })
assertEquals(descriptions.size, descriptions.toSet().size)
}

@Test
fun groupDescriptionIsConsistentAcrossInstancesOfTheSameSubtype() {
assertEquals(Failed.Restricted("A", "A").groupDescription, Failed.Restricted("B", "B").groupDescription)
}

// -------------------------------------------------------------------------
// id — "$origin.$name", derived, usable as a map/lookup key
// -------------------------------------------------------------------------
Expand Down
Loading