David Kubecka
01/17/2024, 11:43 AMDavid Kubecka
01/17/2024, 11:49 AMinterface ApiError {
fun toApi(): Nothing
}
sealed interface DomainXError : ApiError
sealed interface DomainYError : ApiError
data object UnknownError : DomainXError {
override fun toApi() = error(HttpStatus.UNPROCESSABLE_ENTITY.value())
}
data object NotFound : DomainXError, DomainYError {
override fun toApi() = error(HttpStatus.NOT_FOUND.value())
}
data object Unauthorized : DomainYError {
override fun toApi() = error(HttpStatus.UNAUTHORIZED.value())
}
fun callServiceX(): DomainXError = UnknownError
fun callServiceY(): DomainYError = NotFound
fun main() {
val resultX = callServiceX()
resultX.toApi()
}
While this works, I would like to restrict the toApi
calls only to the controller. The dispatch/context receivers seem a natural solution to this problem but I run into issues. TBCDavid Kubecka
01/17/2024, 11:57 AMinterface ApiScope<T> {
fun T.toApi(): ApplicationException
}
object UnknownErrorScope : ApiScope<UnknownError> {
override fun UnknownError.toApi() = error(HttpStatus.UNPROCESSABLE_ENTITY.value())
}
// similarly for the other scopes
val domainXErrorScope = object : ApiScope<DomainXError> {
override fun DomainXError.toApi() =
when (this) {
is UnknownError -> this.toApi()
is NotFound -> this.toApi()
}
}
fun main() = with(domainXErrorScope) {
val resultX = callServiceX()
resultX.toApi()
}
But I'm unable to delegate to the concrete toApi
implementations in the DomainXError.toApi
. Is there a way out?David Kubecka
01/17/2024, 3:19 PMwhen (this) {
is UnknownError -> this.toApi() // I need UnknownErrorScope here
is NotFound -> this.toApi()
}
David Kubecka
01/17/2024, 3:34 PMval apiScope = object : ApiScope<ApiError> {
override fun ApiError.toApi() =
when (this) {
is UnknownError -> error(HttpStatus.UNPROCESSABLE_ENTITY.value())
is NotFound -> error(HttpStatus.NOT_FOUND.value())
is Unauthorized -> error(HttpStatus.UNAUTHORIZED.value())
}
}
fun main() = with(apiScope) {
val resultX = callServiceX()
resultX.toApi()
}
Daniel Pitts
01/17/2024, 4:40 PMinterface ApiError {
fun ApiController.toApi(): Nothing
}
sealed interface DomainXError : ApiError
sealed interface DomainYError : ApiError
data object UnknownError : DomainXError {
override fun ApiController.toApi() = error("Unknown error")
}
data object NotFound : DomainXError, DomainYError {
override fun ApiController.toApi() = error("Not found")
}
data object Unauthorized : DomainYError {
override fun ApiController.toApi() = error("Unauthorized")
}
fun callServiceX(): DomainXError = UnknownError
fun callServiceY(): DomainYError = NotFound
class ApiController {
fun doSomething() {
with(callServiceX()) { toApi() }
}
}
Daniel Pitts
01/17/2024, 4:42 PMwith
syntax, to make the ApiError instance an implicit receiver. You could also use callServiceX().run { toApi() }
David Kubecka
01/17/2024, 4:50 PMApiController
is again a god object. What if I have (and typically I do) more controllers? Let's say ControllerA handles (or combines) domains X,Y, while ControllerB handles domain Z. Would I need to to write
interface ApiError {
fun ControllerA.toApi(): Nothing
fun ControllerB.toApi(): Nothing
}
?
Perhaps that's unavoidable given the lack of union types. In fact even the definition of Domain*Error unions is kind of inverted...Daniel Pitts
01/17/2024, 4:51 PMtoApi
to be called from certain contexts is flawed to begin with.