sealed abstract class UserError extends Product with Serializable
sealed trait TokenResetError extends UserError
sealed trait EmailRegistrationError extends UserError
sealed trait EmailConfirmationError extends UserError
sealed trait PasswordChangeError extends UserError
object UserError {
case object InvalidEmailOrPassword extends TokenResetError with PasswordChangeError
case object InvalidEmailOrToken extends EmailConfirmationError
case object TokenExpired extends EmailConfirmationError
case object TokenNotExpired extends TokenResetError
case object AlreadyExists extends EmailRegistrationError
case object NotFound extends PasswordChangeError
final case class PasswordTooWeak(error: NonEmptyList[PasswordValidationError])
extends EmailRegistrationError
with PasswordChangeError
}
Problem: skomplikowana hierarchia
trait UserService {
import UserError._
def changeEmailPassword[M[_]](uid: UID,
email: Email,
oldPassword: PlainPassword,
newPassword: PlainPassword)(
implicit users: Users[M],
ME: MonadError[M, PasswordChangeError]): M[User] = ???
def registerUser[M[_]](email: Email, token: String)(
implicit users: Users[M],
ME: MonadError[M, EmailConfirmationError]): M[User] = ???
//...
}
Problem: monady obsługujące błędy (MonadError, EitherT) są inwariantne
MonadError[M, UserError] nie jest wspólną implementacją
findEmail(email)
.flatMap(maybeEmail => ME.fromOption(maybeEmail, InvalidEmailOrPassword))
.map(_.reset(plainPassword, confirmationTokenTtl).leftMap[TokenResetError] {
case x => InvalidEmailOrPassword
case y => TokenNotExpired
})
.rethrow
Problem: słabo z inferencją
protected val createUser: Route = (post & entity(as[CreateUserRequest])) {
onSuccess(...) {
case Right(_) => complete(Accepted)
case Left(e) => completeWithError(e)
}
}
def completeWithError[E](e: E)(implicit error: HttpErrorLike[E]) =
complete(error.entity(e))
Idealnie by było tak jak powyżej:
Zwracamy jeden ogólny typ błędu i jakoś (jak?) automatycznie mapujemy go w kontekście HTTP.
Zalety: nie trzeba myśleć
Wady: zwracamy "za dużo"
@implicitNotFound("Do not know how to map ${E} to the Http Response.")
sealed abstract class HttpErrorLike[E] {
def entity(e: E): (StatusCode, JsObject)
}
object HttpErrorLike {
private class WithReason[E](val statusCode: StatusCode, val errorCode: String)
extends HttpErrorLike[E] {
protected final val constantResponse = JsObject(Seq("reason" -> JsString(errorCode)))
override def entity(e: E) = statusCode -> constantResponse
}
def apply[E: ClassTag](statusCode: StatusCode): HttpErrorLike[E] =
new WithReasonFromClass[E](statusCode)
def notFound[E: ClassTag] = apply[E](StatusCodes.NotFound)
def conflict[E: ClassTag] = apply[E](StatusCodes.Conflict)
def unprocessableEntity[E: ClassTag] = apply[E](StatusCodes.UnprocessableEntity)
//...
}
implicit val invalidEmailOrPasswordLike: HttpErrorLike[InvalidEmailOrPassword.type] =
unprocessableEntity
implicit val alreadyExistsErrorLike: HttpErrorLike[AlreadyExists.type] = conflict
implicit val invalidEmailOrTokenLike: HttpErrorLike[InvalidEmailOrToken.type] =
unprocessableEntity
implicit val tokenExpiredLike: HttpErrorLike[TokenExpired.type] = unprocessableEntity
implicit val tokenNotExpiredLike: HttpErrorLike[TokenNotExpired.type] = unprocessableEntity
implicit val notFoundLike: HttpErrorLike[NotFound.type] = notFound
To jakby działa, ale dalej nie możemy mapować dowolnego UserError tj. chcemy powiedzieć, że implicit dla UserError istnieje jeśli istnieje implicit dla każdej podklasy
import shapeless._
implicit val cnilHttpErrorLike: HttpErrorLike[CNil] = new HttpErrorLike[CNil] {
override def entity(e: CNil) = e.impossible
}
implicit def coproductHttpErrorLike[H, T <: Coproduct](
implicit hErrorLike: HttpErrorLike[H],
tErrorLike: HttpErrorLike[T]): HttpErrorLike[H :+: T] =
new HttpErrorLike[H :+: T] {
override def entity(e: H :+: T) =
e.eliminate(hErrorLike.entity, tErrorLike.entity)
}
sealed abstract class GenericHttpErrorLike[A] extends HttpErrorLike[A] {
type Repr <: Coproduct
def toGenericRepr(a: A): Repr
def reprErrorLike: HttpErrorLike[Repr]
override final def entity(e: A) = reprErrorLike.entity(toGenericRepr(e))
}
implicit def genericHttpErrorLike[ADT, Repr0 <: Coproduct](
implicit generic: Generic.Aux[ADT, Repr0],
hierarchy: HttpErrorLike[Repr0]): GenericHttpErrorLike[ADT] =
new GenericHttpErrorLike[ADT]() {
override type Repr = Repr0
override def toGenericRepr(a: ADT) = generic.to(a)
override val reprErrorLike = hierarchy
}
def hierarchy[ADT](implicit gen: GenericHttpErrorLike[ADT]): HttpErrorLike[ADT] = gen
implicit val userError: HttpErrorLike[UserError] = HttpErrorLike.hierarchy[UserError]
Behold!
protected val createUser: Route = (post & entity(as[CreateUserRequest])) {
onSuccess(...) {
case Right(_) => complete(Accepted)
case Left(e) => completeWithError(e)
}
}
Zalety: zwracamy jeden ogólny typ błędu i jakoś (jak?) automatycznie mapujemy go w kontekście HTTP