Unable to use for comprehension to resolve Future

2019-09-02 07:47发布

I have this Action which should return a Future[Result] but I am unable to code it using for comprehension. This is the first time I am using for comprehension so I am also not sure if this is how I should use for.

Also, would someone comment on whether the usage of for is correct?

def verifyUser(token:String) = Action.async{
    implicit request => { //the function takes a token
      val tokenFutureOption:Future[Option[UserToken]] = userTokenRepo.find(UserTokenKey(UUID.fromString(token))) //checkc if the token exists in the db (db returns a Future)
      for(tokenOption<- tokenFutureOption) yield { //resolve the future
        tokenOption match {  
          case Some(userToken) =>{//token exists
            val userOptionFuture = userRepo.findUser(userToken.loginInfo)//find user to which the token belongs. Another db request which returns a Future
            for(userOption <- userOptionFuture) yield {//resolve future
              userOption match {
                case Some(user) =>{//user exists
                  val newInternalProfile = user.profile.internalProfileDetails.get.copy(confirmed=true) //modify user's profile
                  val newProfile = UserProfile(Some(newInternalProfile),user.profile.externalProfileDetails)
                  val confirmedUser = user.copy(profile=newProfile)
                  val userOptionFuture :Future[Option[User]] = userRepo.updateUser(confirmedUser) //update profile with new value. Another db operation with returns a Future
                  for(userOption <- userOptionFuture) yield {//resolve future
                  userTokenRepo.remove(UserTokenKey(UUID.fromString(token)))//remove the token
              //      Ok("user verified") //I WANT TO RETURN SUCCESS RESPONSE HERE BUT CODE DOESN'T COMPILE IF I UNCOMMENT THIS
                  }
                }
                case None =>{ //user doesn't exist
            //      Ok("user verified") //I WANT TO RETURN FAILURE RESPONSE HERE BUT CODE DOESN'T COMPILE IF I UNCOMMENT THIS
                }
              }
            }    
          }
          case None =>{//INVALID TOKEN RECEIVED
            Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config //I CAN RETURN Redirect (WHICH IS OF SAME TYPE AS OK I.E. RESULT) BUT WHY AM I NOT ABLE TO USE OK ABOVE
          }
        }

      }//THIS IS THE END OF FIRST FOR LOOP. HOW DO I HANDLE FAILURES IN THE FUTURE?

    }
  }

1条回答
Root(大扎)
2楼-- · 2019-09-02 07:53

You are using Future, Option which are Monads and the idea behind them being helpful to sequence the computations just like Functors but also with capability to specify what happens next. .flatMap is what Monads have allow that which can be used as for yield for readability.

So in your example you can compose the api using sequence of operations.

  object Api {

    final case class UserTokenKey(uuid: UUID)
    final case class UserToken(loginInfo: String)

    object userTokenRepo {
      def find(u: UserTokenKey) = {
        Future.successful(Some(UserToken(loginInfo = "foundLoginInfo")))
      }

      def remove(u: UserTokenKey) = {
        Future.successful(Some(UserToken(loginInfo = "")))
      }
    }

    final case class User(profile: String)

    object userRepo {
      def findUser(u: String) = {
        Future.successful(Some(User("profile")))
      }

      def updateUser(u: User) = {
        Future.successful(Some(User("updated profile")))
      }
    }

    def verifyUser(token: String)(implicit executionContext: ExecutionContext) = {

      val user: Future[Option[UserToken]] = for {
        tokenMaybe: Option[UserToken] <- userTokenRepo.find(UserTokenKey(UUID.fromString(token)))
        userMaybe: Option[User] <- 
          tokenMaybe match {
            case Some(t) => userRepo.findUser(t.loginInfo)
            case _ => Future.successful(Option.empty[User])
          }
        updatedUserMaybe: Option[User] <- 
          userMaybe match {
          case Some(u) => userRepo.updateUser(u)
          case _ => Future.successful(Option.empty[User])
        }
        removedUserMaybe: Option[UserToken] <- userTokenRepo.remove(UserTokenKey(UUID.fromString(token)))
      } yield removedUserMaybe

      user
    }

    def httpLayer(request: String) = {
      import scala.concurrent.ExecutionContext.Implicits.global
      import play.api.mvc.Results

      verifyUser(request).onComplete {
        case Success(Some(user)) =>
          println("user verified")
          Results.Ok("user verified")
        case Success(None) =>
          println("user does not exist")
          Results.Ok("user does not exist")
        case Failure(f) =>
          println("unknown error")
          Results.Ok("unknown error")
      }

    }

  }

Now you can test your api as below

  def main(args: Array[String]): Unit = {

    import Api._

    httpLayer(UUID.randomUUID().toString) // user verified

  }

Note1: you might want to use api to respond Future[Either[Error, User]] to better handle errors and use Error to determine what to respond back to the http consumers.

Note2: Since you are working with two monads Future[Option[a]], you can combine them using Monad Transformation OptionT[OuterMonad, Value Type] in scalaz or cats mtl library. Which gives a single monad OptionT.

def verifyUserV2(tokenString: String)(implicit executionContext: ExecutionContext) = {

      import scalaz._
      import Scalaz._

      // import cats.implicits._
      // import cats.data.OptionT

      val userStack = for {
        token       <- OptionT(userTokenRepo.find(UserTokenKey(UUID.fromString(tokenString))))
        user        <- OptionT(userRepo.findUser(token.loginInfo))
        updatedUser <- OptionT(userRepo.updateUser(user))
        removedUser <- OptionT(userTokenRepo.remove(UserTokenKey(UUID.fromString(tokenString))))
      } yield removedUser

      userStack.run
      //cats unpack stack
      // userStack.value
}

Useful reads:

查看更多
登录 后发表回答