From 37d1e6969a87e1e62e515128c0389d3cab5c8926 Mon Sep 17 00:00:00 2001 From: t_shimomura Date: Thu, 23 May 2019 18:42:30 +0900 Subject: [PATCH 1/2] impl Either version usecase --- .../controller/WarriorEitherController.scala | 34 +++++++++++++++++++ .../http/controller/support/FormHelper.scala | 7 ++++ .../usecase/EquipWeaponToWarrior.scala | 2 +- .../usecase/EquipWeaponToWarriorEither.scala | 25 ++++++++++++++ .../usecase/FindWarriorEither.scala | 22 ++++++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/com/s10myk4/adapter/http/controller/WarriorEitherController.scala create mode 100644 src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala create mode 100644 src/main/scala/com/s10myk4/application/usecase/FindWarriorEither.scala diff --git a/src/main/scala/com/s10myk4/adapter/http/controller/WarriorEitherController.scala b/src/main/scala/com/s10myk4/adapter/http/controller/WarriorEitherController.scala new file mode 100644 index 0000000..56a7a6d --- /dev/null +++ b/src/main/scala/com/s10myk4/adapter/http/controller/WarriorEitherController.scala @@ -0,0 +1,34 @@ +package com.s10myk4.adapter.http.controller + +import cats.data.EitherT +import cats.effect.IO +import com.s10myk4.adapter.http.controller.support.{ActionSupport, FormHelper} +import com.s10myk4.adapter.http.form.EquipWeaponForm +import com.s10myk4.application.usecase._ +import com.s10myk4.domain.model.character.warrior.WarriorId +import play.api.mvc._ + +import scala.concurrent.ExecutionContext + +final class WarriorEitherController( + cc: ControllerComponents, + findWarrior: FindWarriorEither[IO], + warriorEquippedNewWeapon: EquipWeaponToWarriorEither[IO], + presenter: Presenter[UseCaseResult, Result], + val ec: ExecutionContext +) extends AbstractController(cc) + with FormHelper + with ActionSupport { + + def equipWeapon(id: Long): EssentialAction = Action.async { implicit r => + (for { + form <- EitherT(EquipWeaponForm.apply.bindEither[IO]) + (warriorId, weapon) = (WarriorId(id), form.weapon) + warrior <- EitherT(findWarrior.exec(warriorId)) + result <- EitherT(warriorEquippedNewWeapon.exec(warrior, weapon)) + } yield result) + .fold(abnormal => presenter.present(abnormal), normal => presenter.present(normal)) + .toFuture + } + +} diff --git a/src/main/scala/com/s10myk4/adapter/http/controller/support/FormHelper.scala b/src/main/scala/com/s10myk4/adapter/http/controller/support/FormHelper.scala index 583caee..deaf1e3 100644 --- a/src/main/scala/com/s10myk4/adapter/http/controller/support/FormHelper.scala +++ b/src/main/scala/com/s10myk4/adapter/http/controller/support/FormHelper.scala @@ -20,6 +20,13 @@ private[http] trait FormHelper { a => f(a) ) ) + + def bindEither[F[_]: Applicative](implicit req: Request[AnyContent]): F[Either[InvalidInputParameters, A]] = { + form.bindFromRequest.fold( + error => Applicative[F].pure(Left(InvalidInputParameters(errors = convertFormErrorsToMap(error.errors)))), + a => Applicative[F].pure(Right(a)) + ) + } } private def convertFormErrorsToMap(errors: Seq[FormError]): Map[String, String] = { diff --git a/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarrior.scala b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarrior.scala index c3a681e..4ed5c9e 100644 --- a/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarrior.scala +++ b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarrior.scala @@ -34,7 +34,7 @@ object EquipWeaponToWarrior { weapon: Weapon ) - private implicit def toUseCaseResult(domainErrors: NonEmptyList[WarriorError]): UseCaseResult = { + implicit def toUseCaseResult(domainErrors: NonEmptyList[WarriorError]): AbnormalCase = { val errors = domainErrors.toList.toSet errors match { case _ if errors == Set(DifferentAttributeError, NotOverLevelError) => DifferentAttributeAndNotOverLevel diff --git a/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala new file mode 100644 index 0000000..daf22b8 --- /dev/null +++ b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala @@ -0,0 +1,25 @@ +package com.s10myk4.application.usecase + +import cats.Monad +import cats.data.Validated.{Invalid, Valid} +import cats.syntax.functor._ +import com.s10myk4.domain.lifcycle.WarriorRepository +import com.s10myk4.domain.model.character.warrior.Warrior +import com.s10myk4.domain.model.weapon.Weapon + +import scala.language.higherKinds + +final class EquipWeaponToWarriorEither[F[_]: Monad]( + repository: WarriorRepository[F] +) { + + import EquipWeaponToWarrior._ + + def exec(warrior: Warrior, newWeapon: Weapon): F[Either[AbnormalCase, NormalCase.type]] = { + warrior.equip(newWeapon) match { + case Valid(w) => repository.update(w).map(_ => Right(NormalCase)) + case Invalid(err) => Monad[F].point(Left(err)) + } + } + +} diff --git a/src/main/scala/com/s10myk4/application/usecase/FindWarriorEither.scala b/src/main/scala/com/s10myk4/application/usecase/FindWarriorEither.scala new file mode 100644 index 0000000..c7717d6 --- /dev/null +++ b/src/main/scala/com/s10myk4/application/usecase/FindWarriorEither.scala @@ -0,0 +1,22 @@ +package com.s10myk4.application.usecase + +import cats.Monad +import cats.syntax.functor._ +import com.s10myk4.application.usecase.FindWarrior.WarriorNotFound +import com.s10myk4.domain.lifcycle.WarriorRepository +import com.s10myk4.domain.model.character.warrior.{Warrior, WarriorId} + +import scala.language.higherKinds + +final class FindWarriorEither[F[_]: Monad]( + repository: WarriorRepository[F] +) { + + def exec(id: WarriorId): F[Either[AbnormalCase, Warrior]] = { + repository.resolveBy(id).map { + case Some(w) => Right(w) + case None => Left(WarriorNotFound) + } + } + +} From abe1a57ba36eff0b0e82d42321fc0ebf8d2c68b3 Mon Sep 17 00:00:00 2001 From: t_shimomura Date: Sat, 25 May 2019 15:01:59 +0900 Subject: [PATCH 2/2] add EitherT compose test --- .../usecase/EquipWeaponToWarriorEither.scala | 10 ++++++++ .../support/EitherTComposeSpec.scala | 23 +++++++++++++++++++ .../application/support/UseCaseContSpec.scala | 10 ++++---- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/test/scala/com/s10myk4/application/support/EitherTComposeSpec.scala diff --git a/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala index daf22b8..c759c93 100644 --- a/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala +++ b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala @@ -1,6 +1,7 @@ package com.s10myk4.application.usecase import cats.Monad +import cats.data.EitherT import cats.data.Validated.{Invalid, Valid} import cats.syntax.functor._ import com.s10myk4.domain.lifcycle.WarriorRepository @@ -22,4 +23,13 @@ final class EquipWeaponToWarriorEither[F[_]: Monad]( } } + def hoge(warrior: Warrior, newWeapon: Weapon): EitherT[F, AbnormalCase, NormalCase.type] = { + warrior.equip(newWeapon) match { + case Valid(w) => + EitherT.right[AbnormalCase](repository.update(w).map(_ => NormalCase)) + case Invalid(err) => + EitherT.left[NormalCase.type](Monad[F].pure(toUseCaseResult(err))) + } + } + } diff --git a/src/test/scala/com/s10myk4/application/support/EitherTComposeSpec.scala b/src/test/scala/com/s10myk4/application/support/EitherTComposeSpec.scala new file mode 100644 index 0000000..70c595a --- /dev/null +++ b/src/test/scala/com/s10myk4/application/support/EitherTComposeSpec.scala @@ -0,0 +1,23 @@ +package com.s10myk4.application.support + +import cats.data.EitherT +import cats.effect.IO +import org.scalatest.FlatSpec + +class EitherTComposeSpec extends FlatSpec { + + it should "EitherT" in { + val res = for { + a <- EitherT[IO, String, String](IO(Right("a"))) + b <- EitherT[IO, String, String](IO(Left("break"))) + c <- EitherT[IO, String, String](IO(Right("c"))) + } yield a + b + c + res + .fold( + left => assert(left == "break"), + _ => false + ) + .unsafeRunSync() + } + +} diff --git a/src/test/scala/com/s10myk4/application/support/UseCaseContSpec.scala b/src/test/scala/com/s10myk4/application/support/UseCaseContSpec.scala index d6ccf9a..765e0b0 100644 --- a/src/test/scala/com/s10myk4/application/support/UseCaseContSpec.scala +++ b/src/test/scala/com/s10myk4/application/support/UseCaseContSpec.scala @@ -22,9 +22,11 @@ class UseCaseContSpec extends FlatSpec { it should "処理途中で異常系が発生した場合に処理を打ち切る" in { val res = for { a <- ContT[IO, String, String](f => f("a")) - b <- ContT[IO, String, String](_ => IO.raiseError(new Exception("b"))) - c <- ContT[IO, String, String](f => f("c")) - } yield a + b + c - assertThrows[Exception](res.run(Applicative[IO].pure).unsafeRunSync()) + b <- ContT[IO, String, String](f => f("b")) + c <- ContT[IO, String, String](_ => IO("break")) + d <- ContT[IO, String, String](f => f("d")) + } yield a + b + c + d + res.run(Applicative[IO].pure).map(r => assert(r == "break")).unsafeRunSync() } + }