feat(core:domain): typed Result / Error / DataError core (REDI-80)

- Error marker interface; Result<D, E: Error> (Success/Error) + EmptyResult typealias.
- Inline chainable helpers: map / onSuccess / onFailure / asEmptyResult.
- DataError sealed interface with full Network + Local case sets.
- Pure Kotlin, zero Android imports.
This commit is contained in:
2026-06-10 11:31:13 +02:00
parent a3bda3b601
commit 6bc4027cbb
3 changed files with 82 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
package com.example.architecture.core.domain
/**
* Errors raised by the data layer. [Network] for remote calls, [Local] for on-device storage.
* A repository that merges multiple sources can expose the [DataError] supertype.
*/
sealed interface DataError : Error {
enum class Network : DataError {
BAD_REQUEST,
REQUEST_TIMEOUT,
UNAUTHORIZED,
FORBIDDEN,
NOT_FOUND,
CONFLICT,
TOO_MANY_REQUESTS,
NO_INTERNET,
PAYLOAD_TOO_LARGE,
SERVER_ERROR,
SERVICE_UNAVAILABLE,
SERIALIZATION,
UNKNOWN,
}
enum class Local : DataError {
DISK_FULL,
NOT_FOUND,
UNKNOWN,
}
}

View File

@@ -0,0 +1,7 @@
package com.example.architecture.core.domain
/**
* Marker for every typed error in the app. Each layer/feature defines its own [Error]
* implementations (e.g. [DataError], or a feature validation enum) and pairs them with [Result].
*/
interface Error

View File

@@ -0,0 +1,46 @@
package com.example.architecture.core.domain
/**
* Typed result usable across every layer (data, domain, presentation, validation). Carries either
* success [data] or a typed [Error]. Prefer this over throwing for expected failures.
*/
sealed interface Result<out D, out E : Error> {
data class Success<out D>(val data: D) : Result<D, Nothing>
// The bound is fully qualified because inside this scope `Error` would resolve to this class.
data class Error<out E : com.example.architecture.core.domain.Error>(
val error: E,
) : Result<Nothing, E>
}
/** A [Result] that carries no success payload — for operations that either succeed or fail. */
typealias EmptyResult<E> = Result<Unit, E>
inline fun <T, E : Error, R> Result<T, E>.map(map: (T) -> R): Result<R, E> {
return when (this) {
is Result.Error -> Result.Error(error)
is Result.Success -> Result.Success(map(data))
}
}
inline fun <T, E : Error> Result<T, E>.onSuccess(action: (T) -> Unit): Result<T, E> {
return when (this) {
is Result.Error -> this
is Result.Success -> {
action(data)
this
}
}
}
inline fun <T, E : Error> Result<T, E>.onFailure(action: (E) -> Unit): Result<T, E> {
return when (this) {
is Result.Error -> {
action(error)
this
}
is Result.Success -> this
}
}
fun <T, E : Error> Result<T, E>.asEmptyResult(): EmptyResult<E> = map { }