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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ src_managed/
project/boot/
project/plugins/project/

# kotlin
.kotlin/
.gradle

# test/

Expand Down
267 changes: 158 additions & 109 deletions src/core/codes/README.md

Large diffs are not rendered by default.

302 changes: 172 additions & 130 deletions src/core/codes/src/commonMain/kotlin/kiit/codes/Codes.kt

Large diffs are not rendered by default.

205 changes: 108 additions & 97 deletions src/core/codes/src/commonMain/kotlin/kiit/codes/Status.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,172 +12,183 @@
package kiit.codes

/**
* Platform-agnostic status type describing the outcome of any operation.
* Platform-agnostic status type describing the outcome of any operation — a service call,
* a background job step, an API request, or a CLI command.
*
* Shape (maps directly to JSON / API error responses):
* { "name": "TOKEN_EXPIRED", "code": 400009, "message": "Session token expired", "success": false }
*
* Hierarchy:
* Status = Passed | Failed
* Passed = Succeeded | Pending | Filtered | Ignored
* Failed = Denied | Invalid | Errored | Unknown
* Hierarchy. Categories are closed/sealed and fixed by design, to enforce a consistent taxonomy
* across every consumer. Individual codes *within* a category are open — create new domain codes
* by constructing a [Passed] or [Failed] subtype directly (see [Codes] for the built-in set):
*
* Status = Passed | Failed
* Passed = Succeeded | Pending | Filtered | Information
* Failed = Denied | Invalid | Errored | Unserviceable
*
* NOTE: [code] ranges are grouped conceptually the way HTTP groups 2xx/4xx/5xx, but this is a
* conceptual similarity only — [code] is NOT a literal HTTP status. Always convert via a
* [CodeLookup] implementation such as [CodesToHttp] to obtain a real HTTP status code; never
* infer one from the numeric prefix.
*/
interface Status {
sealed interface Status {
/**
* Unique domain label, e.g. "TOKEN_EXPIRED", "RATE_LIMITED".
* Should be SCREAMING_SNAKE_CASE and stable — used as a searchable key in logs.
* SCREAMING_SNAKE_CASE, stable — used as a searchable/aggregable key in logs and metrics.
*/
val name: String

/**
* Numeric code whose defaults align with HTTP status codes (200, 400, 500 ranges).
* Flexible for non-HTTP runtimes — convert via [Codes.toHttp].
* Numeric code. Grouped by category by convention (see [Codes]) but NOT a literal HTTP code.
*/
val code: Int

/**
* Human-readable description of this status.
* Must be a constant — never constructed from runtime data.
* Human-readable, constant description — never constructed from runtime data. Per-instance /
* runtime detail (e.g. "field X was invalid because...") belongs on whatever wraps this
* Status (an error/result type one layer up), not here — that keeps [message] safe to use
* as an aggregation key across every occurrence of this status.
*/
val message: String

/**
* Boolean shortcut — true for [Passed.Succeeded] and [Passed.Pending],
* false for [Passed.Filtered], [Passed.Ignored], and all [Failed] subtypes.
* Callers that don't need to narrow the sealed type can use this directly.
* True for all [Passed] subtypes, false for all [Failed] subtypes. Callers that don't need
* to narrow the sealed type can branch on this directly instead of pattern matching.
*/
val success: Boolean

/** Returns a copy of this status with an updated [msg], preserving name and code. */
fun copyMessage(msg: String): Status

/** Returns a copy of this status with an updated [msg] and [code], preserving name. */
fun copyAll(msg: String, code: Int): Status

companion object {
/**
* Returns a copy of [defaultStatus] with an updated [msg] and/or [code].
* Optimised to return the original instance when nothing would change.
* Returns a copy of [defaultStatus] with [msg] and/or [code] overridden, or the original
* instance unchanged if neither override actually differs from the default. A null or
* blank [msg] is treated as "no message override" (so it can be safely omitted by callers
* without extra null-handling).
*/
@Suppress("UNCHECKED_CAST")
fun <T : Status> ofCode(msg: String?, code: Int?, defaultStatus: T): T {
if (code == null && msg == null || msg == "") return defaultStatus
if (code == defaultStatus.code && msg == null) return defaultStatus
if (code == defaultStatus.code && msg == defaultStatus.message) return defaultStatus
return defaultStatus.copyAll(msg ?: defaultStatus.message, code ?: defaultStatus.code) as T
val resolvedMsg = msg.takeUnless { it.isNullOrEmpty() } ?: defaultStatus.message
val resolvedCode = code ?: defaultStatus.code

return if (resolvedMsg == defaultStatus.message && resolvedCode == defaultStatus.code) {
defaultStatus
} else {
defaultStatus.copyAll(resolvedMsg, resolvedCode) as T
}
}

/**
* Returns a copy of [status] with an updated [msg] and/or the override from [rawStatus].
* Optimised to return the original instance when nothing would change.
* Resolves a status from an optional [msg] override and an optional [rawStatus] override,
* falling back to [status] when neither is supplied. [rawStatus], if present, is used as
* the base instead of [status]; [msg], if present, is then applied on top of that base.
*/
@Suppress("UNCHECKED_CAST")
fun <T : Status> ofStatus(msg: String?, rawStatus: T?, status: T): T {
if (msg == null && rawStatus == null) return status
if (msg == null && rawStatus != null) return rawStatus
if (msg != null && rawStatus == null) return status.copyMessage(msg) as T
if (msg != null && rawStatus != null) return rawStatus.copyMessage(msg) as T
return status
val base = rawStatus ?: status
return if (msg == null) base else base.copyMessage(msg) as T
}

fun toType(status: Status): String {
return when (status) {
/** Returns the lowercase category discriminant for a status, e.g. "denied", "errored". */
fun toType(status: Status): String =
when (status) {
is Passed.Succeeded -> "succeeded"
is Passed.Pending -> "pending"
is Passed.Filtered -> "filtered"
is Passed.Ignored -> "ignored"
is Passed.Information -> "information"
is Failed.Denied -> "denied"
is Failed.Invalid -> "invalid"
is Failed.Errored -> "errored"
is Failed.Unknown -> "unknown"
else -> "unknown"
is Failed.Unserviceable -> "unserviceable"
}
}
}
}

/**
* Parent sealed type for all non-failure statuses.
* Subtypes: [Succeeded], [Pending], [Filtered], [Ignored].
* Parent sealed type for all non-failure statuses (success = true for every subtype).
* Subtypes: [Succeeded], [Pending], [Filtered], [Information].
*/
sealed class Passed : Status {
/** Operation completed successfully. */
data class Succeeded(override val name: String, override val code: Int, override val message: String) : Passed() {
override val success = true
}
final override val success: Boolean get() = true

/** Operation accepted but not yet fully processed (e.g. queued, waiting). */
data class Pending(override val name: String, override val code: Int, override val message: String) : Passed() {
override val success = true
}
/** Operation's primary purpose completed (e.g. a value was created, fetched, updated). */
data class Succeeded(override val name: String, override val code: Int, override val message: String) : Passed()

/** Item was intentionally excluded from processing (e.g. deduplicated, out of scope). */
data class Filtered(override val name: String, override val code: Int, override val message: String) : Passed() {
override val success = true
}
/** Operation accepted but not yet fully processed (e.g. queued, waiting, confirmed). */
data class Pending(override val name: String, override val code: Int, override val message: String) : Passed()

/** Item was processed but its result was deliberately discarded or suppressed. */
data class Ignored(override val name: String, override val code: Int, override val message: String) : Passed() {
override val success = true
}
/**
* Item was excluded from the operation's normal output. Covers both:
* - not processed at all (e.g. SKIPPED — screened out before any work happened), and
* - processed, then its result was deliberately discarded (e.g. DISCARDED).
* The distinction is carried by [name]/[code], not by separate types — see [Codes.SKIPPED]
* and [Codes.DISCARDED].
*/
data class Filtered(override val name: String, override val code: Int, override val message: String) : Passed()

override fun copyAll(msg: String, code: Int): Status {
return when (this) {
is Succeeded -> this.copy(code = code, message = msg)
is Pending -> this.copy(code = code, message = msg)
is Filtered -> this.copy(code = code, message = msg)
is Ignored -> this.copy(code = code, message = msg)
/**
* Informational / metadata response — no primary operation was performed.
* E.g. HELP, ABOUT, VERSION output from a CLI command.
*/
data class Information(override val name: String, override val code: Int, override val message: String) : Passed()

override fun copyAll(msg: String, code: Int): Status =
when (this) {
is Succeeded -> copy(code = code, message = msg)
is Pending -> copy(code = code, message = msg)
is Filtered -> copy(code = code, message = msg)
is Information -> copy(code = code, message = msg)
}
}

override fun copyMessage(msg: String): Status {
return when (this) {
is Succeeded -> this.copy(message = msg)
is Pending -> this.copy(message = msg)
is Filtered -> this.copy(message = msg)
is Ignored -> this.copy(message = msg)
override fun copyMessage(msg: String): Status =
when (this) {
is Succeeded -> copy(message = msg)
is Pending -> copy(message = msg)
is Filtered -> copy(message = msg)
is Information -> copy(message = msg)
}
}
}

/**
* Parent sealed type for all failure statuses (success = false for all subtypes).
* Subtypes: [Denied], [Invalid], [Errored], [Unknown].
* Parent sealed type for all failure statuses (success = false for every subtype).
* Subtypes: [Denied], [Invalid], [Errored], [Unserviceable].
*/
sealed class Failed : Status {
/** Security / access-control failure — the caller is not permitted to perform this action. */
data class Denied(override val name: String, override val code: Int, override val message: String) : Failed() {
override val success = false
}
final override val success: Boolean get() = false

/** The input data is malformed or fails validation rules. */
data class Invalid(override val name: String, override val code: Int, override val message: String) : Failed() {
override val success = false
}
/** Security / access-control failure — the caller is not permitted to perform this action. */
data class Denied(override val name: String, override val code: Int, override val message: String) : Failed()

/** A known business-rule failure — expected, handled, and recoverable. */
data class Errored(override val name: String, override val code: Int, override val message: String) : Failed() {
override val success = false
}
/** The request as given cannot be satisfied — malformed input, invalid values, or not found. */
data class Invalid(override val name: String, override val code: Int, override val message: String) : Failed()

/** An unexpected or unhandled failure — equivalent to an uncaught exception path. */
data class Unknown(override val name: String, override val code: Int, override val message: String) : Failed() {
override val success = false
}
/** A known, expected business-rule failure — understood and handled by the caller. */
data class Errored(override val name: String, override val code: Int, override val message: String) : Failed()

override fun copyAll(msg: String, code: Int): Status {
return when (this) {
is Denied -> this.copy(name = name, code = code, message = msg)
is Invalid -> this.copy(name = name, code = code, message = msg)
is Errored -> this.copy(name = name, code = code, message = msg)
is Unknown -> this.copy(name = name, code = code, message = msg)
/**
* The request is valid and permitted, but cannot be serviced right now for reasons unrelated
* to what was sent — capacity, timeout, an unimplemented/unsupported capability, planned
* maintenance, or a genuinely unexpected/unhandled failure (see [Codes.UNEXPECTED]).
*/
data class Unserviceable(override val name: String, override val code: Int, override val message: String) : Failed()

override fun copyAll(msg: String, code: Int): Status =
when (this) {
is Denied -> copy(code = code, message = msg)
is Invalid -> copy(code = code, message = msg)
is Errored -> copy(code = code, message = msg)
is Unserviceable -> copy(code = code, message = msg)
}
}

override fun copyMessage(msg: String): Status {
return when (this) {
is Denied -> this.copy(message = msg)
is Invalid -> this.copy(message = msg)
is Errored -> this.copy(message = msg)
is Unknown -> this.copy(message = msg)
override fun copyMessage(msg: String): Status =
when (this) {
is Denied -> copy(message = msg)
is Invalid -> copy(message = msg)
is Errored -> copy(message = msg)
is Unserviceable -> copy(message = msg)
}
}
}
15 changes: 11 additions & 4 deletions src/core/codes/src/commonMain/kotlin/kiit/codes/StatusException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ package kiit.codes
* ```kotlin
* throw StatusException(Codes.UNAUTHORIZED)
*
* try { ... } catch (e: StatusException) {
* // with a cause
* throw StatusException(Codes.TIMEOUT, cause = ioException)
*
* try {
* // ...
* } catch (e: StatusException) {
* when (e.status) {
* is Failed.Denied -> // handle auth failure
* is Failed.Errored -> // handle business error
* else -> // ...
* is Failed.Denied -> // handle auth failure
* is Failed.Invalid -> // handle bad input
* is Failed.Errored -> // handle known business-rule failure
* is Failed.Unserviceable -> // handle capacity / timeout / unimplemented / unexpected
* is Passed -> // n/a — Passed statuses aren't normally thrown
* }
* }
* ```
Expand Down
Loading
Loading