Lukáš Kúšik
08/12/2022, 10:31 AMsealed interface Error
sealed interface ApiError : Error {
object NetworkError : ApiError
object Unauthorized : ApiError
object ServerError : ApiError
}
sealed interface DomainError : Error
sealed interface UserError : DomainError {
object AlreadyExists : UserError
}
sealed interface BookError : DomainError {
object AlreadyReserved : UserError
}
class RemoteApi {
fun createUser(): Either<ApiError, UserDTO>
}
class Repository {
fun createUser(): Either<UserError, UserDTO> (?) {
val response: Either<ApiError, UserDTO> = remoteApi.createUser()
// ... map known errors to UserError and return
}
}
Let's say that there's a Repository
which uses a RemoteApi
and I'd like to create a new user on the backend.
The RemoteApi
does the POST call using Ktor, and returns the result of the Either<ApiError, UserDTO>
type, where ApiError
encapsules any network exceptions, internal server errors, etc.
Now, I would like to parse expected errors (like UserAlreadyExists
) as UserError
of the common DomainError
sealed type (similar to the video).
I'd like the Repository
to get the`Either<ApiError, UserDTO>` from the RemoteApi
, process the known errors and return something like the Either<UserError, UserDTO>
type.
The problem with Either<UserError, UserDTO>
is, that when a network exception occurs, it is an ApiError
and not a UserError
. The Repository
would have to return something like Either<Either<ApiError, UserError>, UserDTO>
which looks weird, or?
You could return Either<Error, UserDTO>
to contain both ApiError
and UserError
, but then you would also allow the Error
to be a BookError
, which would get ugly in when statements.
Maybe using Either<Error<UserError>, UserDTO>
would be the best, but I'm having trouble modelling the data types like that.
Does anyone know of an elegant solutions for this, or this problem of modelling errors in repositories overall? Thank you.stojan
08/12/2022, 11:08 AMEither<ApiError, A>
to Either<UserError, A>
you can use mapLeft
to transform from ApiError
to UserError
Lukáš Kúšik
08/12/2022, 11:10 AMApiError
cannot really be mapped to a UserError
(what `UserError`should be returned when the network fails?), so you still kind of need to somehow keep the original ApiError
.stojan
08/12/2022, 11:14 AMDomainError : ApiError
instead of Error
stojan
08/12/2022, 11:14 AMLukáš Kúšik
08/12/2022, 11:29 AMApiError
to a UserError
.
val response: Either<ApiError, UserDTO> = serverApi.createUser(user)
// Map to domain error
val result: Either<UserError, UserDTO> = response.mapLeft { e ->
if (e is ApiError.Unexpected && e.statusCode == HttpStatusCode.Conflict) {
UserError.AlreadyExists // UserError
} else {
e // ApiError
}
} // Resolves to Either<ApiError, UserDTO> not Either<UserError, UserDTO>
UserError
would have to have an UserError.ApiError
subclass or something similar that could be used to get a UserError
here.Lukáš Kúšik
08/12/2022, 11:31 AMsealed class DomainError(val originalError: ApiError) : Error
stojan
08/12/2022, 11:33 AMLukáš Kúšik
08/12/2022, 11:38 AMApiError
cases in each DomainError
subclass, having lots of duplicates. https://youtrack.jetbrains.com/issue/KT-13108/Denotable-union-and-intersection-types#focus=Comments-27-4497857.0-0simon.vergauwen
08/12/2022, 12:48 PMokarm
08/14/2022, 8:44 PMsealed class UserError {
object E1 : UserError()
object E2 : UserError()
...
// Encapsulates the transport layer ApiErrors
// without polluting the top level UserError API
class UnmappableError(val cause: ApiError)
}
Lukáš Kúšik
08/15/2022, 7:07 AMsealed interface UserError : DomainError {
object E1 : UserError
object E2 : UserError
@JvmInline
value class ApiError(private val base: ApiError) : UserError, ApiError by base
}