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]]
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With