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 UserErrorLukáš 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 Errorstojan
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) : Errorstojan
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
}