Play framework ignore following map/flatmap - scala

Is there a way to ignore the following map/flatmap's without failed?
This is what I have:
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
serverService.findByIdAndUserId(serverId, user.id.get)
.flatMap{s =>
if (s.isEmpty) {
Future.failed(new DeleteFailedException)
// Can I return a `NotFound("...")` here instead of failed?
} else {
Future.successful(s.get)
}
}
.map{s =>
serverService.delete(s)
}.map{_ =>
Ok(Json.toJson(Map("success" -> "true")))
}
}
When I would return a NotFound("...") in the flatMap the following map would still be executed. Is there a way to ignore the following map/flatmap's?

Think so should be fine (I assumed that findByIdAndUserId returns Future[Option[_]], not an Option[_] as you answered in comment). In my approach I also removed usage of get and unnecessary map
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
request.user.flatMap(_.id).fold {
Future.successfull(NotFound("no user"))
} {userId =>
serverService.findByIdAndUserId(serverId, userId).map {
case None =>
NotFound("no server")
case Some(s) =>
serverService.delete(s)
Ok(Json.toJson(Map("success" -> "true")))
}
}
}

Instead of doing a Future.failed with an exception. you can return an Either. The good thing about either is that you can pattern match on it and then construct the appropriate http response.
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
serverService.findByIdAndUserId(serverId, user.id.get)
.flatMap{s =>
if (s.isEmpty) {
Left(new DeleteFailedException)
} else {
Right(s.get)
}
}
.map{s => s match {
case Left(notFound) =>
// construct the Json for failure
case Right(s) =>
serverService.delete(s)
// construct json for success.
}}
}

Related

Scala Playframework not all DB queries get executed

I'm sending via HTTP Post Request a Json to my Playframework backend.
In my backend I validate the Json to a Model. After that, I want to save the entries in my Model to my DB.
def parseJSON: Action[AnyContent] = Action.async {
request =>
Future {
request.body.asJson.map(_.validate[MyModel] match {
case JsSuccess(items, _) =>
itemsToDBController.saveItems(items)
Ok("Success")
case JsError(err) =>
println(err)
BadRequest("Json Parse Error")
}).getOrElse(BadRequest("Error"))
}
}
One Item consists out of several objects. To save all objects to my DB, I need to get some values. Therefore I'm using a for(..) yield(...):
def saveItems(items: MyModel) = {
items.SomeObject.map(obj => {
if (obj.value1.isDefined &&
obj.value2.isDefined ) {
val result = for (
value1Exists <- value1DTO.checkExists(obj.value1.name);
value1Entry <- getOrCreateValue1(value1Exists, obj);
value2Exists <- value2DTO.checkExists(obj.value2.name);
value2Entry <- getOrCreateValue2(value1Exists, obj)
) yield(value1Entry, value2Entry)
result.map({
case (value1Entry, value2Entry) => {
insertAllValue3(value1Entry, value2Entry)
Future.successful()
}
case _ => Future.failed(new Exception("Not all entries defined"))
})
}
else {
Future.successful("Not all objects defined - skipping")
}
})
}
My problem is, after all result.map({...}) have started, my parseJSON Action returns 200 - OK. But not all relevant items get stored to my DB. It seems like after the 200 - OK everything is stopped and it doesn't even throw an error.
I don't want to use Await.result or anything blocking in my Action.
Thanks in Advance
You are starting computations by calling itemsToDBController.saveItems(items) and then immediately return result with Ok("Success"). So exception may be thrown after request if completed.
To fix this issue you need to transform result of itemsToDBController.saveItems from List[Future[T]] to Future[List[T]] with help of Future.sequence. Then call map method on returned future. Call recover on this Future to find which error is thrown:
def parseJSON: Action[AnyContent] = Action.async { request =>
request.body.asJson
.map(_.validate[MyModel] match {
case JsSuccess(items, _) =>
Future
.sequence(itemsToDBController.saveItems(items))
.map(_ => Ok("Success"))
.recover {
case e: Exception => BadRequest(e.getMessage())
}
case JsError(err) =>
println(err)
Future.successful(BadRequest("Json Parse Error"))
})
.getOrElse(Future.successful(BadRequest("Error")))
}
Update
For running all inserts in one transaction you should combine DBIOAction instead of Future. For example you rewrite checkExists(name) as:
def checkExists(name: String): DBIO[Boolean] = {
Objects.filter(obj => obj.name === name).exists
}
getOrCreateValue(exists, obj) as:
def getOrCreateValue(exists: boolean, obj: Object): DBIO[Object] = {
if (exists) {
Objects.filter(o => o.name === name).result.head
} else {
(Objects returning Objects.map(_.id) into ((o, id) => o.copy(id = Some(id)))) += obj
}
}
Now you can run it in single transaction in the following way:
def saveItems(items: MyModel) = {
val insertActions = items.SomeObject.map(obj => {
if (obj.value1.isDefined && obj.value2.isDefined) {
val result = for {
value1Exists <- value1DTO.checkExists(obj.value1.name);
value1Entry <- getOrCreateValue1(value1Exists, obj);
value2Exists <- value2DTO.checkExists(obj.value2.name);
value2Entry <- getOrCreateValue2(value1Exists, obj)
} yield (value1Entry, value2Entry)
result.flatMap({
case (value1Entry, value2Entry) => {
insertAllValue3(value1Entry, value2Entry) // This also returns instance of `DBIOAction`
}
case _ =>
DBIO.failed(new Exception("Not all entries defined"))
})
} else {
DBIO.successful("Not all objects defined - skipping")
}
})
db.run(DBIO.sequence(inserActions).transactionally)
}
For mo info how to work with DBIO actions check this official docs

Combining/chaining futures in scala play framework async action

I'm a scala newbie trying to write a Rest Api using play framework. I have the following 3 data access methods
getDataDict: (dsType:String, name:String) => Future[Option[DatasetDictionary]]
getDatasetData: (DatasetDictionary) => Future[List[DatasetData]]
getMetadata: (DatasetDictionary) => Future[List[Metadata]]
I need to use these 3 methods to get the result of my async action method.
def index(dstype:String, name:String, metadata:Option[Boolean]) = Action.async{
/*
1. val result = getDataDict(type, name)
2. If result is Some(d) call getDatasetData
3.1 if metadata = Some(true)
call getMetadata function
return Ok((dict, result, metadata))
3.2 if metadata is None or Some(false)
return Ok(result)
4. If result is None
return BadRequest("Dataset not found")
*/
}
I got the steps 1 and 2 working as follows
def index1(dsType:String, dsName: String, metadata:Option[Boolean]) = Action.async {
getDataDict(dsType, dsName) flatMap {
case Some(x) => getDatasetData(x) map (x => Ok(Json.toJson(x)))
case None => Future.successful(BadRequest("Dataset not found"))
}
}
I'm stuck at how to get the metadata part working.
First of all, it is not very clear (d, result, x) what you really want to return. Hopefully I guessed it correctly:
def index(dstype:String, name:String, metadata:Option[Boolean]) = Action.async {
getDataDict(dstype, name) flatMap {
case Some(datasetDictionary) =>
getDatasetData(datasetDictionary) flatMap { datasetDataList =>
if (metadata == Some(true)) {
getMetadata(datasetDictionary) map { metadataList =>
Ok(Json.toJson((datasetDictionary, datasetDataList, metadataList)))
}
} else {
Future.successful(Ok(Json.toJson(datasetDataList)))
}
}
case None => Future.successful(BadRequest("Dataset not found"))
}
}

playframework scala returning post request

in my scala playframework application I want to return the via Post submitted and then with slick stored object back to frontend as json
I tried this:
def createClient = Action.async { implicit request =>
request.body.asJson.map(_.validate[ClientModel] match {
case JsSuccess(client) =>
clientDTO.createClient(client).map { clients =>
Ok(Json.toJson(clients))
}
})
}
but I get this error:
what could be my problem?
NEW ERROR
Try with something along these lines:
def createClient = Action.async { implicit request =>
request.body.asJson match {
case None => // do something that returns a Future[Result] ~ such as NotFound or
case Some(js) =>
js.validate[ClientModel] match {
case client: JsSuccess[ClientModel] =>
clientDTO.createClient(client).map { clients =>
Ok(Json.toJson(clients))
}
case e: JsError => // do something that returns a Future[Result] ~ such as InternalServerError
}
}
}
the .async, as the name suggests require a Future type.
You have 2 options:
Remove the .async, this will make your def synchronous (deprecated)
Leave the .async but return a Future result
def createClient = Action.async { implicit request =>
request.body.asJson.map(_.validate[ClientModel] match {
case JsSuccess(client) =>
clientDTO.createClient(client).map { clients =>
Future(Ok(Json.toJson(clients)))
}
})
}
But you still need to add the case of validation error:
case JsSuccess(client) =>
{
clientDTO.createClient(client).map
{
clients => Future(Ok(Json.toJson(clients)))
}
}
case _ => Future(BadRequest(""))
This should work and, in all the cases apart JsSuccess, the function will return a future BadRequest response.
A better solution is to change the _ with JsError:
case e: JsError =>
{
Println(e)
Future(BadRequest(.....))
}
This will also print the error.
You can read more here: https://www.playframework.com/documentation/2.6.x/ScalaJson
(Using validation chapter)
More about future in scala: https://docs.scala-lang.org/overviews/core/futures.html

Play Scala 2.4 linking asynchronous calls

I'm trying to figure out how to link multiple asynchronous calls and return a result. I am currently trying to asynchronously user data first, and update user data asynchronously and return result, but it seems like it is not working :(
i used map { result => Ok(result)}, but play still thinks that I am returning an object. any help?
def updateUserData() = Action.async { implicit request =>
updateUserForm.bindFromRequest.fold(
errors => Future.successful(BadRequest(views.html.authenticated.settings.settings_hero(errors, Option(""), Option("")))),
{
case (userData) =>
request.session.get("email") match {
case Some(email) =>
getUser(email, userData.curent_password) map { userCheck =>
if (userCheck) {
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
//val e = updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map {result => Ok("")}
// user is valid now update the user data
// call removeAuth to log out
// redirect to home
///Ok (updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result => result})
//Redirect(routes.settings.index()).addingToSession("email" -> email)
} else {
BadRequest(views.html.authenticated.settings.settings_hero(updateUserForm.bindFromRequest.withGlobalError(Messages("error.login", email)), Option(""), Option("")))
}
}
}
})
}
The main part that i am having issue is this part. I think it is matter of some syntax. Could someone help?
Thanks
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
The issue is with your types and that they don't match up with the required ones.
.fold has to result in Future[Result] in both branches (the error and the success ones).
In the successful form bind branch you have this:
case (userData) => ... // The ... must evaluate to Future[Result]
Looking at your first operation we see:
request.session.get("email") match {
case Some(email) => ...
}
One big issue here is that the None case is not handled! (but this is does not cause the types not matching up). Having something like the following will solve this: case None => Future.successful(BadRequest(...))
So moving on: in the Some you have the following:
getUser(email, userData.curent_password) map { userCheck =>
if (userCheck) {
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
} else {
BadRequest(views.html.authenticated.settings.settings_hero(updateUserForm.bindFromRequest.withGlobalError(Messages("error.login", email)), Option(""), Option("")))
}
}
This is where the issue is:
getUser will return with a Future[X] and when you map over it you will have Future[Y] where Y will be what userCheck => ... evaluates to.
In this case the types are totally mixed up, since when you do if(usercheck) on the true branch you have Future[Result] on the false branch you have Result. So the types don't align on both branches which is a big issue and the compiler will infer Any from this.
To fix this, in the false branch create a future: Future.successful(BadRequest(....))
Ok, now that we fixed the most inner type issues, let's start going backwards. Inside we have Future[Result], if we go back one level (before the getUser()) then we will have Future[Future[Result]]. Again this is not what we want, because we need Future[Result].
The solution to this is to flatMap instead of map, because with flatMap when you need to return with the same container type and it flattens it. A quick example to understand this:
Seq(1, 2, 3).flatMap(i => Seq(i, i))
// res0: Seq[Int] = List(1, 1, 2, 2, 3, 3)
In the case of Futures:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
Future(1).flatMap(i => Future(i*2))
// res1: scala.concurrent.Future[Int] = [....]
So we see that we don't have double nesting, but just a single Future.
Going back to your example this would be my updated code that would work better:
def updateUserData() = Action.async { implicit request =>
updateUserForm.bindFromRequest.fold(
errors => Future.successful(BadRequest(views.html.authenticated.settings.settings_hero(errors, Option(""), Option("")))),
{
case (userData) =>
request.session.get("email") match {
case Some(email) =>
getUser(email, userData.curent_password).flatMap { userCheck =>
if (userCheck) {
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
} else {
Future.successful(BadRequest(views.html.authenticated.settings.settings_hero(updateUserForm.bindFromRequest.withGlobalError(Messages("error.login", email)), Option(""), Option(""))))
}
}
case None => Future.successful(BadRequest) // FIXME: Implement as you wish
}
})
}

Handle services returning Try values in Spray

I am working on a codebase where calling my Spray API need to synchronously call a service that returns a Try which Spray need to format and return over HTTP.
My initial attempt looked like this :
// Assume myService has a run method that returns a Try[Unit]
lazy val myService = new Service()
val routes =
path("api") {
get {
tryToComplete {
myService.run()
}
}
} ~
path("api" / LongNumber) { (num: Long) =>
get {
tryToComplete {
myService.run(num)
}
}
}
def tryToComplete(result: => Try[Unit]): routing.Route = result match {
case Failure(t) => complete(StatusCodes.BadRequest, t.getMessage)
case Success(_) => complete("success")
}
However this caused myService.run() to be called when the application started. I am not sure why this method was called as there was no HTTP call made.
So I have two questions :
Why is my service being called as part of initialising the routes?
What is the cleanest way to handle this case? Imagine that there are a few other end points following a similar pattern. So I need to be able to handle this consistently.
Even though you have result parameter as call-by-name, it'll immediately get evaluated as you're doing
result match {
For it not to get evaluated, it has to be within the complete, ie your code should look something like (haven't tried to compile this):
def tryToComplete(result: => Try[Unit]): routing.Route = complete {
result match {
case Failure(t) => StatusCodes.BadRequest, t.getMessage
case Success(_) => "success"
}
}
The way I solved this was do do the following :
lazy val myService = new Service()
val routes =
path("api") {
get {
complete {
handleTry {
myService.run()
}
}
}
} ~
path("api" / LongNumber) { (num: Long) =>
get {
complete {
handleTry {
myService.run(num)
}
}
}
}
private def handleTry(operation: Try[_]):HttpResponse = operation match {
case Failure(t) =>
HttpResponse(status = StatusCodes.BadRequest, entity = t.getMessage)
case Success(_) =>
HttpResponse(status = StatusCodes.OK, entity = successMessage)
}