Blankman
Blankman

Reputation: 267120

Handling a case of when my future returns a None in my play controller action method,

I want to refactor by update action below to look a little more readable and also handle the failure case better

The userService has the following functions:

class UserService {
    def getUserByUsername: Future[Option[Int]] // which is the UserId
    def getUserById: Future[User]
}

My action looks like:

def update(userId: Int) = Action.async { implicit request =>
    request.body.validate[User] match {
        case JsSuccess(user, _) => {
            userService.getUserByUsername(user.username).map { userId => 
                userService.getUserById(userId.get).map { existingUser =>
                    userService.update(user.username)

                    Ok
                }
            }
        }
        case JsError(err) => Future.sucessful(BadRequest(err))
    }
}
  1. How do I handle the situation where getUserByUsername returns a None?
  2. Would this look cleaner if it was in a for comprehension, is it better style?

Upvotes: 0

Views: 70

Answers (2)

Tomer Shetah
Tomer Shetah

Reputation: 8529

First, you probably need to use Option.fold:

@inline final def fold[B](ifEmpty: => B)(f: A => B)

Then you can do something like this:

def update(userId: Int) = Action.async { implicit request =>
    def handleJsonErrors(errors: Seq[(JsPath, collection.Seq[JsonValidationError])]): Future[Result] = ???
    def updateUser(userWithoutId: User): Future[Result] = {
        for {
            userId <- userService.getUserByUsername(userWithoutId.username)
            _ <- userService.getUserById(userId.get)
            _ <- userService.update(userWithoutId.username)
        } yield {
            Ok
        }
    }

    request.body.asJson.fold {
        Future.successful(BadRequest("Bad json"))
    } {
        _.validate[User].fold(handleJsonErrors, updateUser).recover {
            case NonFatal(ex) =>
                InternalServerError
        }
    }
}

Upvotes: 0

Zvi Mints
Zvi Mints

Reputation: 1142

You have some missing data in your questions such as case classes for the User model, userService class. also better to attach the original function.

Anyways, I will do something as follows:

 def update(userId: Int) = Action { implicit request =>
    request.body.validate[User] match {
      case JsSuccess(user: User, _) => {
        val userId = getUserByUsername(user.username)
        userId match {
          case Some(userId) => {
            for {
              _ <- userService.getUserById(userId)
              _ <- userService.update(user.username)
            } yield Ok
          }.recover {
            case t: Throwable => 
              Metrics.errOnUpdate.increment() // Some metric to monitor 
              logger.error(s"update userId: $userId failed with ex: ${t.getMessage}") // log the error 
              InternalServerError(Json.toJson(Json.obj("error" -> "Failure occured on update"))) // return custom made exception to the client
          }
          case None => Future.successful(NotFound(s"No such user with ${user.username}"))
        }
      }
      case JsError(err) => Future.sucessful(BadRequest(err))
    }
  }

Note: If .update returns Future, you actually not waiting to update before returning Ok to the user, thus, if its fails, its still returns Ok. To fix that, use flatMap and then map the value of update response.

You can also separate the recovering for the getUserById and update if you prefer.


Edit:

  def update(userId: Int) = Action { implicit request =>
    request.body.validate[User] match {
      case JsSuccess(user: User, _) => {
          getUserByUsername(user.username).flatMap {
             case Some(userId) => for {
                _ <- userService.getUserById(userId)
                _ <- userService.update(user.username)
              } yield Ok
             case None => Future.successful(NotFound(s"No such user with ${user.username}"))
          }
        }.recover {
          case t: Throwable =>
            Metrics.errOnUpdate.increment() // Some metric to monitor 
            logger.error(s"update userId: $userId failed with ex: ${t.getMessage}") // log the error 
            InternalServerError(Json.toJson(Json.obj("error" -> "Failure occured on update"))) // return custom made exception to the client
        }
      }
      case JsError(err) => Future.sucessful(BadRequest(err))
    }
  }

Upvotes: 1

Related Questions