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..c759c93 --- /dev/null +++ b/src/main/scala/com/s10myk4/application/usecase/EquipWeaponToWarriorEither.scala @@ -0,0 +1,35 @@ +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 +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)) + } + } + + 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/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) + } + } + +} 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() } + }