Obsługa błędów

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

Obsługa błędów

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ą

Obsługa błędów

    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ą

Obsługa błędów

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"

Obsługa błędów

@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)

//...
}

Obsługa błędów

    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

Obsługa błędów

  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

Obsługa błędów

    implicit val userError: HttpErrorLike[UserError] = HttpErrorLike.hierarchy[UserError]

Behold!

Obsługa błędów

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

Iterators Lightning Talk

By Marcin Rzeźnicki

Iterators Lightning Talk

  • 1,067