Optional Json Body Parser - scala

I'm trying to write a DRY CRUD restful service using PlayFramework. Here's the code for it.
def crudUsers(operation: String) = Action(parse.json) { request =>
(request.body).asOpt[User].map { jsonUser =>
try {
DBGlobal.db.withTransaction {
val queryResult = operation match {
case "create" =>
UsersTable.forInsert.insertAll(jsonUser)
Json.generate(Map("status" -> "Success", "message" -> "Account inserted"))
case "update" =>
val updateQ = UsersTable.where(_.email === jsonUser.email.bind).map(_.forInsert)
println(updateQ.selectStatement)
updateQ.update(jsonUser)
Json.generate(Map("status" -> "Success", "message" -> "Account updated"))
case "retrieve" =>
val retrieveQ = for(r <- UsersTable) yield r
println(retrieveQ.selectStatement)
Json.generate(retrieveQ.list)
case "delete" =>
val deleteQ = UsersTable.where(_.email === jsonUser.email)
deleteQ.delete
Json.generate(Map("status" -> "Success", "message" -> "Account deleted"))
}
Ok(queryResult)
}
} catch {
case _ =>
val errMsg: String = operation + " error"
BadRequest(Json.generate(Map("status" -> "Error", "message" -> errMsg)))
}
}.getOrElse(BadRequest(Json.generate(Map("status" -> "Error", "message" -> "error"))))
}
}
I've noticed that update, delete and create operations work nicely. However the retrieve operation fails with For request 'GET /1/users' [Invalid Json]. I'm pretty sure this is because the JSON parser is not tolerant of a GET request with no JSON passed in the body.
Is there a way to special case the GET/Retrieve operation without losing losing the DRY approach I've started here?

My guess would be that you split your methods so that you can create a different routes for the ones with and without body.
It seems that the design of the code would not work even if the empty string was parsed as JSON. The map method would not be executed because there would be no user. That would cause the match on operation to never be executed.
Update
Since you mentioned DRY, I would refactor it into something like this:
type Operations = PartialFunction[String, String]
val operations: Operations = {
case "retrieve" =>
println("performing retrieve")
"retrieved"
case "list" =>
println("performing list")
"listed"
}
def userOperations(user: User): Operations = {
case "create" =>
println("actual create operation")
"created"
case "delete" =>
println("actual delete operation")
"updated"
case "update" =>
println("actual update operation")
"updated"
}
def withoutUser(operation: String) = Action {
execute(operation, operations andThen successResponse)
}
def withUser(operation: String) = Action(parse.json) { request =>
request.body.asOpt[User].map { user =>
execute(operation, userOperations(user) andThen successResponse)
}
.getOrElse {
errorResponse("invalid user data")
}
}
def execute(operation: String, actualOperation: PartialFunction[String, Result]) =
if (actualOperation isDefinedAt operation) {
try {
DBGlobal.db.withTransaction {
actualOperation(operation)
}
} catch {
case _ => errorResponse(operation + " error")
}
} else {
errorResponse(operation + " not found")
}
val successResponse = createResponse(Ok, "Success", _: String)
val errorResponse = createResponse(BadRequest, "Error", _: String)
def createResponse(httpStatus: Status, status: String, message: String): Result =
httpStatus(Json.toJson(Map("status" -> status, "message" -> message)))

Related

Play framework ignore following map/flatmap

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.
}}
}

Async Action and Future

I'm using Play 2.5 and Scala 2.11. I'm trying to use async Action with Future to return json string but can't get it to work.
Here is my action :
def getData = Action.async { implicit request =>
val futureData = Future { daoObj.fetchData }
futureData onComplete {
case Success(data) =>
Ok(Json.prettyPrint(Json.obj("data" -> data.groupBy(_.name))))
case Failure(t) =>
BadRequest(Json.prettyPrint(Json.obj(
"status" -> "400",
"message" -> s"$t"
)))
}
}
The daoObj.fetchData returns scala.concurrent.Future[List[models.Data]]
Here is the models.Data :
case class Data(name: String, details: String)
I can have data like this
Data (name = "data1", details = "details1")
Data (name = "data1", details = "details1a")
Data (name = "data2", details = "details2")
that I can join on name to return a structure of the form
Map(data1 -> List("details1", "details11"),
data2 -> List("details2"))
I have a compilation error on groupBy :
value groupBy is not a member of scala.concurrent.Future[List[models.Data]]
1) How to get the value (List[models.Data]) from the Future in my action ?
2) It's my first Play Scala app so if you have any comment to improve my code, it's welcome.
You should not use Future.onComplete, which is a callback (with side-effect), but .map and .recover to turn your Future[T] into a Future[Result] which is what's expected by Action.async.
def getData = Action.async { implicit request =>
Future { daoObj.fetchData }.map { data =>
Json.obj("data" -> data.groupBy(_.name))
}.recover {
case error => BadRequest(Json.obj(
"status" -> "400",
"message" -> error.getMessage
))
}
}
There is no need to pretty-print the JsObject which can be directly written as result (except for debugging purposes maybe).

In Scala Play and Slick, how to get request to finish before sending response

In Scala Play and Slick, I wish to send a OK response only after a record has been created in the database, so far I've got:
def createItem = Action(BodyParsers.parse.json) {
request => {
val result = request.body.validate[Item]
result.fold(
invalid => {
val problem = new Problem(BAD_REQUEST, "Invalid Item JSON", invalid.toString)
returnProblemResult(BadRequest, problem)
},
item => {
service.create(item)
// TODO check for success before sending ok
Ok.as(ContentTypes("DEFAULT"))
}
)
}
}
And inside the service.create method:
def create(item: Item): Future[Unit] = {
exists(item.id).map {
case true =>
case _ => db.run(Item.table += cc)
}
}
Currently, the OK response get sent even if no new item is created. I would like it to only return OK if an item is created. If the item already exists, or if there're other errors (e.g. database errors), the createItem method should know what kind of problem occurred and return a Problem result with error message.
Try this:-
For service, change the method to
def create(item: Item): Future[Int] = {
exists(item.id).map {
case true => 0
case _ => db.run(Item.table += cc)
}
}
In controller, do
service.create(item).map {
case i:Int=> Ok.as(ContentTypes("DEFAULT"))
}.recoverWith {
// To handle the error in the processing
case ex: Throwable => InternalServerError(ex)
}

Scala save future value to variable

my function to insert person:
def InsertPerson = Action(BodyParsers.parse.json) { request =>
val b = request.body.validate[Person]
b.fold(
//errorForm => Future.successful(Ok(views.html.index(errorForm, Seq.empty[User]))),
errors => {
BadRequest(Json.obj("status" -> "error", "message" -> JsError.toJson(errors)))
},
person => {
val f = ApplTagService.insertPerson(person)
f.map(res => "User successfully added").recover {
case ex: Error => ex.getMessage
}
f onFailure {
case t => println("An error has occured: " + t.getMessage)
}
Ok(Json.obj("status" -> f.toString() ))
})}
I send Json from CI to scala for insert person, then I want to give an alert if that insert is success or not, I need to save ex.getMessage or t.getMessage on a variable and send it to CI
what I get now on f.toString() in CI is scala.concurrent.impl.Promise$DefaultPromise#793e6657
How can I show ex.getMessage or t.getMessage in CI?
You can use Action.async(BodyParsers.parse.json) instead of Action(BodyParsers.parse.json) when you want to return a Future[Result] instead of a Result
In this case your match will look like this
b.fold(
errors => Future.successful(BadRequest(Json.obj("status" -> "error", "message" -> JsError.toJson(errors)))),
person => {
val f = ApplTagService.insertPerson(person)
val fMessage = f.map(res => "User successfully added").recover {
case ex: Error => ex.getMessage
}
f onFailure {
case t => println("An error has occured: " + t.getMessage)
}
fMessage.map(message => Ok(Json.obj("status" -> message )))
})

PlayFramework: Check existing of record before creation

I want to create a registration Action in play framework application. The question is how to implement check for already existing email?
object AccountController extends Controller {
case class AccountInfo(email: String, password: String)
val accountInfoForm = Form(
mapping(
"email" -> email,
"password" -> nonEmptyText
)(AccountInfo.apply)(AccountInfo.unapply)
)
def createAccount = Action {
implicit request => {
accountInfoForm.bindFromRequest fold (
formWithErrors => {
Logger.info("Validation errors")
BadRequest(accountInfoForm.errorsAsJson)
},
accountInfo => {
AccountService.findByEmail(accountInfo.email) map {
case accountOpt: Option[Account] => accountOpt match {
case Some(acc) => {
Logger.info("Email is already in use")
BadRequest(Json.toJson(Json.obj("message" -> "Email is already in use")))
}
case None => {
Logger.info("Created account")
val account = Account.createAccount(accountInfo)
val accountToSave = account copy (password=Account.encryptPassword(accountInfo.password))
Created(Json.toJson(AccountService.add(accountToSave)))
}
}
case _ => {
Logger.info("DB connection error")
InternalServerError(Json.toJson(Json.obj("message" -> "DB connection error")))
}
}
Ok("Ok")
})
}
}
}
AccountService.findByEmail - returns Future[Option[Account]]
Unfortunately my solution always return 'Ok'
Since findByEmail returns a Future, you should use Action.async instead. This is because when you map the Future[Option[Account]], you're mapping it to a Future[Result] instead of Result. Note how I had to use Future.successful with formWithErrors to keep the return type the same.
When returning a simple Result use Action. When returning a Future[Result], use Action.async.
def createAccount = Action.async {
implicit request => {
accountInfoForm.bindFromRequest fold (
formWithErrors => {
Logger.info("Validation errors")
Future.successful(BadRequest(accountInfoForm.errorsAsJson))
},
accountInfo => {
AccountService.findByEmail(accountInfo.email) map {
case accountOpt: Option[Account] => accountOpt match {
case Some(acc) => {
Logger.info("Email is already in use")
BadRequest(Json.toJson(Json.obj("message" -> "Email is already in use")))
}
case None => {
Logger.info("Created account")
val account = Account.createAccount(accountInfo)
val accountToSave = account copy (password=Account.encryptPassword(accountInfo.password))
Created(Json.toJson(AccountService.add(accountToSave)))
}
}
case _ => {
Logger.info("DB connection error")
InternalServerError(Json.toJson(Json.obj("message" -> "DB connection error")))
}
}
})
}
}