Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composing `Future` result in Play Framework with Scala

I am trying to write a Play Framework asynchronous Action for the following URL:

POST /users/:userId/items

My database calls all return Future[...], where ... is Option[A] for find methods and Option[Id] for create methods.

I would like to check for the existence of the userId before trying to create the new item. I have a method Users.findById(userId) that returns a Future[Option[User]]. The result is Some(User) if the user exists and None if not. Items.create() also returns a Future[Option[itemId]].

I am trying to compose something using for:

for {
  user <- Users.findById(userId)
  if user.isDefined
} yield {
  Items.create(...) map { itemId => Ok(itemId) } getOrElse NotFound
}

I would like to return Ok(itemId) if the item is successfully created. I'm not sure how to handle the error case. I would like to return NotFound if either the userId is invalid or the item cannot be created (maybe a field conflicts with a unique value already in the database).

I'm not sure what to put after the for structure. I tried getOrElse, but that does not compile, since Future does not have a getOrElse method.

Ideally, I can handle URLs containing several ids to check, e.g.:

PUT /users/:userId/foo/:fooId/bar/:barId

and confirm that userId, fooId, and barId are all valid before doing the update. All of those calls (Users.findById, Foo.findById, and Bar.findById) will return Future[Option[A]].

like image 923
Ralph Avatar asked Jan 28 '14 18:01

Ralph


1 Answers

It's that double-nesting (Future of Option) that seems to get people every time. Things become a lot easier if you can flatten stuff out first.

In this case, Future already has a way of representing an error condition, it can wrap an Exception as well as a success value, that's something you can use...

// making this a Singleton avoids the cost of building a stack trace,
// which only happens when an Exception is constructed (not when it's thrown)
object NotFoundException extends RuntimeException("Empty Option")

// The map operation will trap any thrown exception and fail the Future
def squish[T](x: Future[Option[T]]) =
  x map { _.getOrElse(throw NotFoundException) }

It's now a lot easier to use those squished results in a comprehension:

val result = for {
  user <- squish(Users findById userId)
  itemId <- squish(Items.create(user, ...))
} yield {
  Ok(itemId)
} recover {
  case NotFoundException => NotFound
}

Which will, of course, evaluate to a Future. This is async programming, after all :)

Any exceptions other than NotFoundException will still be exposed.

like image 131
Kevin Wright Avatar answered Nov 13 '22 11:11

Kevin Wright