Skip to content

Commit

Permalink
refactor(syntax): use Monad instead of IO for more abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
yoshinorin committed Dec 4, 2023
1 parent 199e160 commit b7ed267
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 57 deletions.
6 changes: 3 additions & 3 deletions src/main/scala/net/yoshinorin/qualtet/auth/Jwt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
12 changes: 7 additions & 5 deletions src/main/scala/net/yoshinorin/qualtet/syntax/eitherT.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/net/yoshinorin/qualtet/syntax/validator.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/main/scala/net/yoshinorin/qualtet/validator/Validator.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.yoshinorin.qualtet.validator

import cats.Monad
import cats.data.EitherT
import cats.effect.IO

object Validator {

Expand All @@ -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))
}
}

Expand All @@ -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))
}
}

Expand Down
33 changes: 18 additions & 15 deletions src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down Expand Up @@ -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"
Expand All @@ -59,42 +62,42 @@ 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)
}

"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)
}

"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)
Expand Down
20 changes: 12 additions & 8 deletions src/test/scala/net/yoshinorin/qualtet/syntax/validatorSpec.scala
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
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

// 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()
}
}
}
Expand Down
35 changes: 19 additions & 16 deletions src/test/scala/net/yoshinorin/qualtet/validator/ValidatorSpec.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
}
}
Expand Down

0 comments on commit b7ed267

Please sign in to comment.