diff --git a/core/domain/src/main/kotlin/com/example/architecture/core/domain/DataError.kt b/core/domain/src/main/kotlin/com/example/architecture/core/domain/DataError.kt new file mode 100644 index 0000000..db01008 --- /dev/null +++ b/core/domain/src/main/kotlin/com/example/architecture/core/domain/DataError.kt @@ -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, + } +} diff --git a/core/domain/src/main/kotlin/com/example/architecture/core/domain/Error.kt b/core/domain/src/main/kotlin/com/example/architecture/core/domain/Error.kt new file mode 100644 index 0000000..15f48b1 --- /dev/null +++ b/core/domain/src/main/kotlin/com/example/architecture/core/domain/Error.kt @@ -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 diff --git a/core/domain/src/main/kotlin/com/example/architecture/core/domain/Result.kt b/core/domain/src/main/kotlin/com/example/architecture/core/domain/Result.kt new file mode 100644 index 0000000..c50b076 --- /dev/null +++ b/core/domain/src/main/kotlin/com/example/architecture/core/domain/Result.kt @@ -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 { + data class Success(val data: D) : Result + + // The bound is fully qualified because inside this scope `Error` would resolve to this class. + data class Error( + val error: E, + ) : Result +} + +/** A [Result] that carries no success payload — for operations that either succeed or fail. */ +typealias EmptyResult = Result + +inline fun Result.map(map: (T) -> R): Result { + return when (this) { + is Result.Error -> Result.Error(error) + is Result.Success -> Result.Success(map(data)) + } +} + +inline fun Result.onSuccess(action: (T) -> Unit): Result { + return when (this) { + is Result.Error -> this + is Result.Success -> { + action(data) + this + } + } +} + +inline fun Result.onFailure(action: (E) -> Unit): Result { + return when (this) { + is Result.Error -> { + action(error) + this + } + is Result.Success -> this + } +} + +fun Result.asEmptyResult(): EmptyResult = map { }