From b7ed2670415ce07b979cbf7f615036575287f6ea Mon Sep 17 00:00:00 2001 From: yoshinorin Date: Tue, 5 Dec 2023 02:28:04 +0900 Subject: [PATCH] refactor(syntax): use `Monad` instead of `IO` for more abstraction --- .../net/yoshinorin/qualtet/auth/Jwt.scala | 6 ++-- .../yoshinorin/qualtet/syntax/eitherT.scala | 12 ++++--- .../yoshinorin/qualtet/syntax/validator.scala | 6 ++-- .../qualtet/validator/Validator.scala | 14 ++++---- .../net/yoshinorin/qualtet/auth/JwtSpec.scala | 33 +++++++++-------- .../qualtet/syntax/validatorSpec.scala | 20 ++++++----- .../qualtet/validator/ValidatorSpec.scala | 35 ++++++++++--------- 7 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/main/scala/net/yoshinorin/qualtet/auth/Jwt.scala b/src/main/scala/net/yoshinorin/qualtet/auth/Jwt.scala index c4989c0b..e442d65c 100644 --- a/src/main/scala/net/yoshinorin/qualtet/auth/Jwt.scala +++ b/src/main/scala/net/yoshinorin/qualtet/auth/Jwt.scala @@ -83,9 +83,9 @@ class Jwt(config: JwtConfig, algorithm: JwtAsymmetricAlgorithm, keyPair: KeyPair IO(Left(t)) case Right(jc) => { (for { - _ <- jc.toEitherIO(x => x.aud === config.aud)(Unauthorized()) - _ <- jc.toEitherIO(x => x.iss === config.iss)(Unauthorized()) - result <- jc.toEitherIO(x => x.exp > Instant.now.getEpochSecond)(Unauthorized()) + _ <- jc.toEitherF(x => x.aud === config.aud)(Unauthorized()) + _ <- jc.toEitherF(x => x.iss === config.iss)(Unauthorized()) + result <- jc.toEitherF(x => x.exp > Instant.now.getEpochSecond)(Unauthorized()) } yield result).value } } diff --git a/src/main/scala/net/yoshinorin/qualtet/syntax/eitherT.scala b/src/main/scala/net/yoshinorin/qualtet/syntax/eitherT.scala index 6e9e653a..3ca56e00 100644 --- a/src/main/scala/net/yoshinorin/qualtet/syntax/eitherT.scala +++ b/src/main/scala/net/yoshinorin/qualtet/syntax/eitherT.scala @@ -1,16 +1,18 @@ package net.yoshinorin.qualtet.syntax +import cats.{Monad, MonadError} import cats.data.EitherT -import cats.effect.IO +import cats.syntax.flatMap.toFlatMapOps + import scala.reflect.ClassTag trait eitherT { - extension [A: ClassTag, F <: Throwable](v: EitherT[IO, F, A]) { - def andThrow: IO[A] = { + extension [A: ClassTag, B <: Throwable, F[_]: Monad](v: EitherT[F, B, A]) { + def andThrow(implicit me: MonadError[F, Throwable]): F[A] = { v.value.flatMap { - case Right(v) => IO(v) - case Left(t: Throwable) => IO.raiseError(t) + case Right(v) => Monad[F].pure(v) + case Left(t: Throwable) => MonadError[F, Throwable].raiseError(t) } } } diff --git a/src/main/scala/net/yoshinorin/qualtet/syntax/validator.scala b/src/main/scala/net/yoshinorin/qualtet/syntax/validator.scala index a65c6959..1bcd7a59 100644 --- a/src/main/scala/net/yoshinorin/qualtet/syntax/validator.scala +++ b/src/main/scala/net/yoshinorin/qualtet/syntax/validator.scala @@ -1,13 +1,13 @@ package net.yoshinorin.qualtet.syntax +import cats.Monad import cats.data.EitherT -import cats.effect.IO import net.yoshinorin.qualtet.validator.Validator trait validator { - extension [A, F](a: A) { - def toEitherIO(cond: A => Boolean)(left: F): EitherT[IO, F, A] = { + extension [A, B, F[_]: Monad](a: A) { + def toEitherF(cond: A => Boolean)(left: B): EitherT[F, B, A] = { Validator.validate(a)(cond)(left) } } diff --git a/src/main/scala/net/yoshinorin/qualtet/validator/Validator.scala b/src/main/scala/net/yoshinorin/qualtet/validator/Validator.scala index 6a2df53b..555b936f 100644 --- a/src/main/scala/net/yoshinorin/qualtet/validator/Validator.scala +++ b/src/main/scala/net/yoshinorin/qualtet/validator/Validator.scala @@ -1,7 +1,7 @@ package net.yoshinorin.qualtet.validator +import cats.Monad import cats.data.EitherT -import cats.effect.IO object Validator { @@ -11,11 +11,11 @@ object Validator { * @param fail Instance for Fail. * @return validation result with EitherT */ - def validate[A, F](a: A)(cond: A => Boolean)(fail: F): EitherT[IO, F, A] = { + def validate[A, B, F[_]: Monad](a: A)(cond: A => Boolean)(fail: B): EitherT[F, B, A] = { if (cond(a)) { - EitherT.right(IO(a)) + EitherT.right(Monad[F].pure(a)) } else { - EitherT.left(IO(fail)) + EitherT.left(Monad[F].pure(fail)) } } @@ -27,11 +27,11 @@ object Validator { * @param fail Instance for Fail. * @return validation result with EitherT */ - def validateUnless[A, F](a: A)(cond: A => Boolean)(fail: F): EitherT[IO, F, A] = { + def validateUnless[A, B, F[_]: Monad](a: A)(cond: A => Boolean)(fail: B): EitherT[F, B, A] = { if (cond(a)) { - EitherT.left(IO(fail)) + EitherT.left(Monad[F].pure(fail)) } else { - EitherT.right(IO(a)) + EitherT.right(Monad[F].pure(a)) } } diff --git a/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala b/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala index 8ed16110..d775311c 100644 --- a/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala @@ -1,5 +1,6 @@ package net.yoshinorin.qualtet.auth +import cats.effect.IO import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorId, AuthorName} import net.yoshinorin.qualtet.message.Fail.Unauthorized import net.yoshinorin.qualtet.Modules.* @@ -51,6 +52,8 @@ class JwtSpec extends AnyWordSpec { } + val ioInstance = implicitly[cats.Monad[IO]] + "be throw exception caused by not signed JSON" in { val maybeJwtClaims = jwtInstance.decode( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" @@ -59,22 +62,22 @@ class JwtSpec extends AnyWordSpec { } "be return Right if JWT is correct" in { - assert(Validator.validate(jc)(x => x.aud === config.jwt.aud)(Unauthorized()).value.unsafeRunSync().isRight) - assert(Validator.validate(jc)(x => x.iss === config.jwt.iss)(Unauthorized()).value.unsafeRunSync().isRight) - assert(Validator.validate(jc)(x => x.exp > Instant.now.getEpochSecond - 1000)(Unauthorized()).value.unsafeRunSync().isRight) + assert(Validator.validate(jc)(x => x.aud === config.jwt.aud)(Unauthorized())(ioInstance).value.unsafeRunSync().isRight) + assert(Validator.validate(jc)(x => x.iss === config.jwt.iss)(Unauthorized())(ioInstance).value.unsafeRunSync().isRight) + assert(Validator.validate(jc)(x => x.exp > Instant.now.getEpochSecond - 1000)(Unauthorized())(ioInstance).value.unsafeRunSync().isRight) } "be return Left if JWT is incorrect" in { - assert(Validator.validate(jc)(x => x.aud === "incorrect aud")(Unauthorized()).value.unsafeRunSync().isLeft) - assert(Validator.validate(jc)(x => x.iss === "incorrect iss")(Unauthorized()).value.unsafeRunSync().isLeft) - assert(Validator.validate(jc)(x => x.exp > x.exp + 1)(Unauthorized()).value.unsafeRunSync().isLeft) + assert(Validator.validate(jc)(x => x.aud === "incorrect aud")(Unauthorized())(ioInstance).value.unsafeRunSync().isLeft) + assert(Validator.validate(jc)(x => x.iss === "incorrect iss")(Unauthorized())(ioInstance).value.unsafeRunSync().isLeft) + assert(Validator.validate(jc)(x => x.exp > x.exp + 1)(Unauthorized())(ioInstance).value.unsafeRunSync().isLeft) } "be return Left if JWT is incorrect in for-comprehension: pattern-one" in { val r = for { - _ <- Validator.validate(jc)(x => x.aud === config.jwt.aud)(Unauthorized()) - _ <- Validator.validate(jc)(x => x.iss === config.jwt.iss)(Unauthorized()) - result <- Validator.validate(jc)(x => x.exp > (x.exp + config.jwt.expiration + 1))(Unauthorized()) + _ <- Validator.validate(jc)(x => x.aud === config.jwt.aud)(Unauthorized())(ioInstance) + _ <- Validator.validate(jc)(x => x.iss === config.jwt.iss)(Unauthorized())(ioInstance) + result <- Validator.validate(jc)(x => x.exp > (x.exp + config.jwt.expiration + 1))(Unauthorized())(ioInstance) } yield result assert(r.value.unsafeRunSync().isLeft) @@ -82,9 +85,9 @@ class JwtSpec extends AnyWordSpec { "be return Left if JWT is incorrect in for-comprehension: pattern-two" in { val r = for { - _ <- Validator.validate(jc)(x => x.aud === config.jwt.aud)(Unauthorized()) - _ <- Validator.validate(jc)(x => x.iss === "incorrect iss")(Unauthorized()) - result <- Validator.validate(jc)(x => x.exp > Instant.now.getEpochSecond)(Unauthorized()) + _ <- Validator.validate(jc)(x => x.aud === config.jwt.aud)(Unauthorized())(ioInstance) + _ <- Validator.validate(jc)(x => x.iss === "incorrect iss")(Unauthorized())(ioInstance) + result <- Validator.validate(jc)(x => x.exp > Instant.now.getEpochSecond)(Unauthorized())(ioInstance) } yield result assert(r.value.unsafeRunSync().isLeft) @@ -92,9 +95,9 @@ class JwtSpec extends AnyWordSpec { "be return Left if JWT is incorrect in for-comprehension: pattern-three" in { val r = for { - _ <- Validator.validate(jc)(x => x.aud === "incorrect aud")(Unauthorized()) - _ <- Validator.validate(jc)(x => x.iss === config.jwt.iss)(Unauthorized()) - result <- Validator.validate(jc)(x => x.exp > Instant.now.getEpochSecond)(Unauthorized()) + _ <- Validator.validate(jc)(x => x.aud === "incorrect aud")(Unauthorized())(ioInstance) + _ <- Validator.validate(jc)(x => x.iss === config.jwt.iss)(Unauthorized())(ioInstance) + result <- Validator.validate(jc)(x => x.exp > Instant.now.getEpochSecond)(Unauthorized())(ioInstance) } yield result assert(r.value.unsafeRunSync().isLeft) diff --git a/src/test/scala/net/yoshinorin/qualtet/syntax/validatorSpec.scala b/src/test/scala/net/yoshinorin/qualtet/syntax/validatorSpec.scala index 249e482e..2ddbfa30 100644 --- a/src/test/scala/net/yoshinorin/qualtet/syntax/validatorSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/syntax/validatorSpec.scala @@ -1,6 +1,7 @@ package net.yoshinorin.qualtet.syntax import cats.implicits.catsSyntaxEq +import cats.effect.IO import org.scalatest.wordspec.AnyWordSpec import net.yoshinorin.qualtet.message.Fail.{Unauthorized, UnprocessableEntity} import cats.effect.unsafe.implicits.global @@ -8,32 +9,35 @@ import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.syntax.ValidatorSpec class ValidatorSpec extends AnyWordSpec { + val ioInstance = implicitly[cats.Monad[IO]] + "validate" should { "be return right" in { + // NOTE: Workaround avoid compile error when use `===`. So, use `eqv` instead of it. - assert("a".toEitherIO(x => x eqv "a")(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isRight) - assert(1.toEitherIO(x => x eqv 1)(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isRight) + assert("a".toEitherF(x => x eqv "a")(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isRight) + assert(1.toEitherF(x => x eqv 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isRight) } "not be throw if isRight" in { - val s = "a".toEitherIO(x => x eqv "a")(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + val s = "a".toEitherF(x => x eqv "a")(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() assert(s eqv "a") - val i = 1.toEitherIO(x => x eqv 1)(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + val i = 1.toEitherF(x => x eqv 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() assert(i eqv 1) } "be return left" in { - assert("a".toEitherIO(x => x =!= "a")(Unauthorized()).value.unsafeRunSync().isLeft) - assert(1.toEitherIO(x => x =!= 1)(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isLeft) + assert("a".toEitherF(x => x =!= "a")(Unauthorized())(ioInstance).value.unsafeRunSync().isLeft) + assert(1.toEitherF(x => x =!= 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isLeft) } "be throw if isLeft" in { assertThrows[Unauthorized] { - "a".toEitherIO(x => x =!= "a")(Unauthorized()).andThrow.unsafeRunSync() + "a".toEitherF(x => x =!= "a")(Unauthorized())(ioInstance).andThrow.unsafeRunSync() } assertThrows[UnprocessableEntity] { - 1.toEitherIO(x => x =!= 1)(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + 1.toEitherF(x => x =!= 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() } } } diff --git a/src/test/scala/net/yoshinorin/qualtet/validator/ValidatorSpec.scala b/src/test/scala/net/yoshinorin/qualtet/validator/ValidatorSpec.scala index f876a053..9ba833c8 100644 --- a/src/test/scala/net/yoshinorin/qualtet/validator/ValidatorSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/validator/ValidatorSpec.scala @@ -1,6 +1,7 @@ package net.yoshinorin.qualtet.validator import cats.implicits.catsSyntaxEq +import cats.effect.IO import net.yoshinorin.qualtet.message.Fail.{Unauthorized, UnprocessableEntity} import net.yoshinorin.qualtet.syntax.* import org.scalatest.wordspec.AnyWordSpec @@ -9,61 +10,63 @@ import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.validator.ValidatorSpec class ValidatorSpec extends AnyWordSpec { + val ioInstance = implicitly[cats.Monad[IO]] + "validate" should { "be return right" in { // NOTE: Workaround avoid compile error when use `===`. So, use `eqv` instead of it. - assert(Validator.validate("a")(x => x eqv "a")(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isRight) - assert(Validator.validate(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isRight) + assert(Validator.validate("a")(x => x eqv "a")(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isRight) + assert(Validator.validate(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isRight) } "not be throw if isRight" in { - val s = Validator.validate("a")(x => x eqv "a")(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + val s = Validator.validate("a")(x => x eqv "a")(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() assert(s eqv "a") - val i = Validator.validate(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + val i = Validator.validate(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() assert(i eqv 1) } "be return left" in { - assert(Validator.validate("a")(x => x =!= "a")(Unauthorized()).value.unsafeRunSync().isLeft) - assert(Validator.validate(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isLeft) + assert(Validator.validate("a")(x => x =!= "a")(Unauthorized())(ioInstance).value.unsafeRunSync().isLeft) + assert(Validator.validate(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isLeft) } "be throw if isLeft" in { assertThrows[Unauthorized] { - Validator.validate("a")(x => x =!= "a")(Unauthorized()).andThrow.unsafeRunSync() + Validator.validate("a")(x => x =!= "a")(Unauthorized())(ioInstance).andThrow.unsafeRunSync() } assertThrows[UnprocessableEntity] { - Validator.validate(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + Validator.validate(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() } } } "validateUnless" should { "be return right" in { - assert(Validator.validateUnless("a")(x => x =!= "a")(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isRight) - assert(Validator.validateUnless(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isRight) + assert(Validator.validateUnless("a")(x => x =!= "a")(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isRight) + assert(Validator.validateUnless(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isRight) } "not be throw if isRight" in { - val s = Validator.validateUnless("a")(x => x =!= "a")(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + val s = Validator.validateUnless("a")(x => x =!= "a")(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() assert(s eqv "a") - val i = Validator.validateUnless(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + val i = Validator.validateUnless(1)(x => x =!= 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() assert(i eqv 1) } "be return left" in { - assert(Validator.validateUnless("a")(x => x eqv "a")(Unauthorized()).value.unsafeRunSync().isLeft) - assert(Validator.validateUnless(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!")).value.unsafeRunSync().isLeft) + assert(Validator.validateUnless("a")(x => x eqv "a")(Unauthorized())(ioInstance).value.unsafeRunSync().isLeft) + assert(Validator.validateUnless(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).value.unsafeRunSync().isLeft) } "be throw if isLeft" in { assertThrows[Unauthorized] { - Validator.validateUnless("a")(x => x eqv "a")(Unauthorized()).andThrow.unsafeRunSync() + Validator.validateUnless("a")(x => x eqv "a")(Unauthorized())(ioInstance).andThrow.unsafeRunSync() } assertThrows[UnprocessableEntity] { - Validator.validateUnless(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!")).andThrow.unsafeRunSync() + Validator.validateUnless(1)(x => x eqv 1)(UnprocessableEntity("unprocessable!!"))(ioInstance).andThrow.unsafeRunSync() } } }